diff --git a/Cargo.lock b/Cargo.lock index 5c02b560e..f19420521 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2435,8 +2435,6 @@ dependencies = [ [[package]] name = "rawdb" version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be3e770fd2d36882cf78a62f211de8342fe8619801e8d39f4273ee87837e4bae" dependencies = [ "libc", "log", @@ -3255,8 +3253,6 @@ checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23" [[package]] name = "vecdb" version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e212dbc82f5651d33e30b677df3ff789f2d903b917af482078487872cac76d89" dependencies = [ "ctrlc", "log", @@ -3276,8 +3272,6 @@ dependencies = [ [[package]] name = "vecdb_derive" version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704e0810d01c394ff35112f861c4889ec155279a7f0a55cb37e2a33682343dd4" dependencies = [ "quote", "syn", diff --git a/Cargo.toml b/Cargo.toml index ba8a7f487..1847a8c4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,8 +83,8 @@ tokio = { version = "1.49.0", features = ["rt-multi-thread"] } tracing = { version = "0.1", default-features = false, features = ["std"] } tower-http = { version = "0.6.8", features = ["catch-panic", "compression-br", "compression-gzip", "compression-zstd", "cors", "normalize-path", "timeout", "trace"] } tower-layer = "0.3" -vecdb = { version = "0.6.7", features = ["derive", "serde_json", "pco", "schemars"] } -# vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] } +# vecdb = { version = "0.6.7", features = ["derive", "serde_json", "pco", "schemars"] } +vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] } [workspace.metadata.release] shared-version = true diff --git a/crates/brk_computer/src/distribution/address/mod.rs b/crates/brk_computer/src/distribution/address/mod.rs index 43776b473..b9c9972a9 100644 --- a/crates/brk_computer/src/distribution/address/mod.rs +++ b/crates/brk_computer/src/distribution/address/mod.rs @@ -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; diff --git a/crates/brk_computer/src/distribution/cohorts/address/groups.rs b/crates/brk_computer/src/distribution/cohorts/address/groups.rs index f372a31b7..718164bda 100644 --- a/crates/brk_computer/src/distribution/cohorts/address/groups.rs +++ b/crates/brk_computer/src/distribution/cohorts/address/groups.rs @@ -56,52 +56,44 @@ impl AddressCohorts { })) } + /// Apply a function to each aggregate cohort with its source cohorts. + fn for_each_aggregate(&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::>(), - ) - }) - .collect::>(), - // 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::>(), - ) - }) - .collect::>(), - ] - .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( diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs b/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs index cad58cd60..b7424bc29 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs @@ -152,70 +152,84 @@ impl UTXOCohorts { })) } + /// Apply a function to each aggregate cohort with its source cohorts. + fn for_each_aggregate(&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::>())] + .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::>())] - .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::>(), - ) - })) - .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::>(), - ) - })) - .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::>(), - ) - })) - .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::>(), - ) - })) - .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::>(), - ) - })) - .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( diff --git a/crates/brk_computer/src/distribution/compute/aggregates.rs b/crates/brk_computer/src/distribution/compute/aggregates.rs index b23f1d5cf..045c85ad2 100644 --- a/crates/brk_computer/src/distribution/compute/aggregates.rs +++ b/crates/brk_computer/src/distribution/compute/aggregates.rs @@ -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(()) } diff --git a/crates/brk_computer/src/distribution/metrics/mod.rs b/crates/brk_computer/src/distribution/metrics/mod.rs index 9aca56cdf..720efac02 100644 --- a/crates/brk_computer/src/distribution/metrics/mod.rs +++ b/crates/brk_computer/src/distribution/metrics/mod.rs @@ -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, diff --git a/crates/brk_computer/src/distribution/metrics/relative.rs b/crates/brk_computer/src/distribution/metrics/relative.rs index 5590f4345..8a1cba8f3 100644 --- a/crates/brk_computer/src/distribution/metrics/relative.rs +++ b/crates/brk_computer/src/distribution/metrics/relative.rs @@ -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::( + LazyBinaryFromHeightLast::from_computed_height_date_and_binary_block::( &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::( + LazyBinaryFromHeightLast::from_computed_height_date_and_binary_block::( &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::( + LazyBinaryFromHeightLast::from_computed_height_date_and_binary_block::( &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::( + LazyBinaryFromHeightLast::from_both_binary_block::( &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, ) diff --git a/crates/brk_computer/src/distribution/metrics/unrealized.rs b/crates/brk_computer/src/distribution/metrics/unrealized.rs index 5f780e625..6d6d6fe10 100644 --- a/crates/brk_computer/src/distribution/metrics/unrealized.rs +++ b/crates/brk_computer/src/distribution/metrics/unrealized.rs @@ -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, )?; diff --git a/crates/brk_computer/src/internal/single/transform/mod.rs b/crates/brk_computer/src/internal/single/transform/mod.rs index df206b6cb..f22352029 100644 --- a/crates/brk_computer/src/internal/single/transform/mod.rs +++ b/crates/brk_computer/src/internal/single/transform/mod.rs @@ -4,10 +4,10 @@ mod close_price_times_sats; mod difference_f32; mod dollar_halve; mod dollar_identity; -mod dollars_to_sats_fract; mod dollar_minus; mod dollar_plus; mod dollar_times_tenths; +mod dollars_to_sats_fract; mod f32_identity; mod half_close_price_times_sats; mod ohlc; @@ -19,7 +19,6 @@ mod percentage_u32_f32; mod percentage_u64_f32; mod price_times_ratio; mod ratio32; -mod ratio32_neg; mod ratio_f32; mod ratio_u64_f32; mod return_f32_tenths; @@ -47,10 +46,10 @@ pub use close_price_times_sats::*; pub use difference_f32::*; pub use dollar_halve::*; pub use dollar_identity::*; -pub use dollars_to_sats_fract::*; pub use dollar_minus::*; pub use dollar_plus::*; pub use dollar_times_tenths::*; +pub use dollars_to_sats_fract::*; pub use f32_identity::*; pub use half_close_price_times_sats::*; pub use ohlc::*; @@ -61,10 +60,9 @@ pub use percentage_sats_f64::*; pub use percentage_u32_f32::*; pub use percentage_u64_f32::*; pub use price_times_ratio::*; -pub use ratio32::*; -pub use ratio32_neg::*; pub use ratio_f32::*; pub use ratio_u64_f32::*; +pub use ratio32::*; pub use return_f32_tenths::*; pub use return_i8::*; pub use return_u16::*; @@ -79,7 +77,7 @@ pub use sat_to_bitcoin::*; pub use sats_times_close_price::*; pub use u16_to_years::*; pub use u64_plus::*; +pub use volatility_sqrt7::*; pub use volatility_sqrt30::*; pub use volatility_sqrt365::*; -pub use volatility_sqrt7::*; pub use weight_to_fullness::*; diff --git a/crates/brk_computer/src/internal/single/transform/ratio32_neg.rs b/crates/brk_computer/src/internal/single/transform/ratio32_neg.rs deleted file mode 100644 index 5b294f749..000000000 --- a/crates/brk_computer/src/internal/single/transform/ratio32_neg.rs +++ /dev/null @@ -1,13 +0,0 @@ -use brk_types::{Dollars, StoredF32}; -use vecdb::BinaryTransform; - -/// (Dollars, Dollars) -> -StoredF32 (negated ratio) -/// Computes -(a/b) directly to avoid lazy-from-lazy chains. -pub struct NegRatio32; - -impl BinaryTransform for NegRatio32 { - #[inline(always)] - fn apply(numerator: Dollars, denominator: Dollars) -> StoredF32 { - -StoredF32::from(numerator / denominator) - } -} diff --git a/website/scripts/chart/index.js b/website/scripts/chart/index.js index ee8519a28..24153f824 100644 --- a/website/scripts/chart/index.js +++ b/website/scripts/chart/index.js @@ -110,6 +110,10 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { }), }; + // Generation counter - incremented on any context change (index, blueprints, unit) + // Used to detect and ignore stale operations (in-flight fetches, etc.) + let generation = 0; + // Range state: localStorage stores all ranges per-index, URL stores current range only /** @typedef {{ from: number, to: number }} Range */ const ranges = createPersistedValue({ @@ -514,6 +518,7 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { active.value ? show() : hide(); + const seriesGeneration = generation; let hasData = false; let lastTime = -Infinity; /** @type {string | null} */ @@ -671,7 +676,10 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { .setVisibleLogicalRange({ from: -1, to: data.length }); } // Delay until chart has applied the range - requestAnimationFrame(() => blueprints.onDataLoaded?.()); + requestAnimationFrame(() => { + if (seriesGeneration !== generation) return; + blueprints.onDataLoaded?.(); + }); } else { // Incremental update: only process new data points for (let i = startIdx; i < length; i++) { @@ -687,9 +695,9 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { getTimeEndpoint(idx).slice(-10000).fetch(), valuesEndpoint.slice(-10000).fetch(), ]); - if (valuesResult.stamp === lastStamp) { - return; - } + // Ignore stale fetches from series that have been replaced + if (seriesGeneration !== generation) return; + if (valuesResult.stamp === lastStamp) return; lastStamp = valuesResult.stamp; if (timeResult.data.length && valuesResult.data.length) { processData(timeResult.data, valuesResult.data); @@ -735,8 +743,9 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { defaultActive, options, }) { - const upColor = customColors?.[0] ?? colors.bi.profitLoss[0]; - const downColor = customColors?.[1] ?? colors.bi.profitLoss[1]; + const seriesGeneration = generation; + const upColor = customColors?.[0] ?? colors.bi.p1[0]; + const downColor = customColors?.[1] ?? colors.bi.p1[1]; /** @type {CandlestickISeries} */ const candlestickISeries = /** @type {any} */ ( @@ -836,6 +845,7 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { const lineData = data.map((d) => ({ time: d.time, value: d.close })); lineISeries.setData(lineData); requestAnimationFrame(() => { + if (seriesGeneration !== generation) return; const range = ichart.timeScale().getVisibleLogicalRange(); if (range) { showLine = shouldShowLine(range.to - range.from); @@ -876,7 +886,7 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { metric, name, key, - color = colors.bi.profitLoss, + color = colors.bi.p1, order, unit, paneIndex = 0, @@ -1189,8 +1199,8 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { unit, paneIndex: _paneIndex, defaultActive, - topColor = colors.bi.profitLoss[0], - bottomColor = colors.bi.profitLoss[1], + topColor = colors.bi.p1[0], + bottomColor = colors.bi.p1[1], options, }) { const paneIndex = _paneIndex ?? 0; @@ -1273,6 +1283,127 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { return series; }, + + /** + * Add a DotsBaseline series (baseline with point markers instead of line) + * @param {Object} args + * @param {AnyMetricPattern} args.metric + * @param {string} args.name + * @param {string} [args.key] + * @param {number} args.order + * @param {Unit} args.unit + * @param {number} [args.paneIndex] + * @param {boolean} [args.defaultActive] + * @param {Color} [args.topColor] + * @param {Color} [args.bottomColor] + * @param {BaselineSeriesPartialOptions} [args.options] + */ + addDotsBaseline({ + metric, + name, + key, + order, + unit, + paneIndex: _paneIndex, + defaultActive, + topColor = colors.bi.p1[0], + bottomColor = colors.bi.p1[1], + options, + }) { + const paneIndex = _paneIndex ?? 0; + + /** @type {BaselineISeries} */ + const iseries = /** @type {any} */ ( + ichart.addSeries( + /** @type {SeriesDefinition<'Baseline'>} */ (BaselineSeries), + { + lineWidth, + baseValue: { + price: options?.baseValue?.price ?? 0, + }, + ...options, + priceLineVisible: false, + bottomFillColor1: "transparent", + bottomFillColor2: "transparent", + topFillColor1: "transparent", + topFillColor2: "transparent", + lineVisible: false, + pointMarkersVisible: true, + pointMarkersRadius: 1, + }, + paneIndex, + ) + ); + + let active = defaultActive !== false; + let highlighted = true; + let radius = getDotsRadius(visibleBarsCount); + + function update() { + iseries.applyOptions({ + visible: active, + lastValueVisible: highlighted, + topLineColor: topColor.highlight(highlighted), + bottomLineColor: bottomColor.highlight(highlighted), + pointMarkersRadius: radius, + }); + } + update(); + + /** @type {ZoomChangeCallback} */ + function handleZoom(count) { + const newRadius = getDotsRadius(count); + if (newRadius === radius) return; + radius = newRadius; + iseries.applyOptions({ pointMarkersRadius: radius }); + } + onZoomChange.add(handleZoom); + const removeSeriesThemeListener = onThemeChange(update); + + const series = serieses.create({ + colors: [topColor, bottomColor], + name, + key, + order, + paneIndex, + unit, + defaultActive, + metric, + setOrder: (order) => iseries.setSeriesOrder(order), + show() { + if (active) return; + active = true; + update(); + }, + hide() { + if (!active) return; + active = false; + update(); + }, + highlight() { + if (highlighted) return; + highlighted = true; + update(); + }, + tame() { + if (!highlighted) return; + highlighted = false; + update(); + }, + setData: (data) => iseries.setData(data), + update: (data) => iseries.update(data), + getData: () => iseries.data(), + onRemove: () => { + onZoomChange.delete(handleZoom); + removeSeriesThemeListener(); + ichart.removeSeries(iseries); + }, + }); + + panes.register(paneIndex, series, [iseries]); + + return series; + }, }; /** @@ -1373,6 +1504,23 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { ); break; } + case "DotsBaseline": { + pane.series.push( + serieses.addDotsBaseline({ + metric: blueprint.metric, + name: blueprint.title, + key: blueprint.key, + defaultActive: blueprint.defaultActive, + paneIndex, + unit, + topColor: blueprint.colors?.[0] ?? blueprint.color, + bottomColor: blueprint.colors?.[1] ?? blueprint.color, + options, + order, + }), + ); + break; + } case "Histogram": { pane.series.push( serieses.addHistogram({ @@ -1448,6 +1596,7 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { }, rebuild() { + generation++; this.rebuildPane(0); this.rebuildPane(1); }, @@ -1507,6 +1656,7 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { toLabel: (u) => u.name, sorted: true, onChange(unit) { + generation++; persistedUnit.set(unit.id); blueprints.panes[paneIndex].unit = unit; blueprints.rebuildPane(paneIndex); @@ -1522,6 +1672,10 @@ export function createChart({ parent, id: chartId, brk, fitContent }) { }, destroy() { + debouncedSetRange.cancel(); + serieses.all.forEach((s) => s.remove()); + index.onChange.clear(); + onZoomChange.clear(); removeThemeListener(); clearInterval(refreshInterval); ichart.remove(); diff --git a/website/scripts/options/distribution/activity.js b/website/scripts/options/distribution/activity.js new file mode 100644 index 000000000..261da97c3 --- /dev/null +++ b/website/scripts/options/distribution/activity.js @@ -0,0 +1,803 @@ +/** + * Activity section builders + * + * Structure: + * - Volume: Sent volume (Sum, Cumulative, 14d EMA) + * - SOPR: Spent Output Profit Ratio (30d > 7d > raw) + * - Sell Side Risk: Risk ratio + * - Value: Flows, Created & Destroyed, Breakdown + * - Coins Destroyed: Coinblocks/Coindays (Sum, Cumulative) + * + * For cohorts WITH adjusted values: Additional Normal/Adjusted sub-sections + */ + +import { Unit } from "../../utils/units.js"; +import { line, baseline, dotsBaseline } from "../series.js"; +import { satsBtcUsd } from "../shared.js"; +import { colors } from "../../utils/colors.js"; +import { + createSingleSellSideRiskSeries, + createGroupedSellSideRiskSeries, + createSingleValueCreatedDestroyedSeries, + createSingleCapitulationProfitFlowSeries, +} from "./shared.js"; + +// ============================================================================ +// Shared Helpers +// ============================================================================ + +/** + * Create SOPR series from realized pattern (30d > 7d > raw order) + * @param {{ sopr: AnyMetricPattern, sopr7dEma: AnyMetricPattern, sopr30dEma: AnyMetricPattern }} realized + * @param {string} rawName - Name for the raw SOPR series + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function soprSeries(realized, rawName = "SOPR") { + return [ + baseline({ + metric: realized.sopr30dEma, + name: "30d EMA", + color: colors.bi.p3, + unit: Unit.ratio, + base: 1, + }), + baseline({ + metric: realized.sopr7dEma, + name: "7d EMA", + color: colors.bi.p2, + unit: Unit.ratio, + base: 1, + }), + dotsBaseline({ + metric: realized.sopr, + name: rawName, + color: colors.bi.p1, + unit: Unit.ratio, + base: 1, + }), + ]; +} + +/** + * Create grouped SOPR chart entries (Raw, 7d EMA, 30d EMA) + * @template {{ color: Color, name: string }} T + * @param {readonly T[]} list + * @param {(item: T) => AnyMetricPattern} getSopr + * @param {(item: T) => AnyMetricPattern} getSopr7d + * @param {(item: T) => AnyMetricPattern} getSopr30d + * @param {(metric: string) => string} title + * @param {string} titlePrefix + * @returns {PartialOptionsTree} + */ +function groupedSoprCharts( + list, + getSopr, + getSopr7d, + getSopr30d, + title, + titlePrefix, +) { + return [ + { + name: "Raw", + title: title(`${titlePrefix}SOPR`), + bottom: list.map((item) => + baseline({ + metric: getSopr(item), + name: item.name, + color: item.color, + unit: Unit.ratio, + base: 1, + }), + ), + }, + { + name: "7d EMA", + title: title(`${titlePrefix}SOPR 7d EMA`), + bottom: list.map((item) => + baseline({ + metric: getSopr7d(item), + name: item.name, + color: item.color, + unit: Unit.ratio, + base: 1, + }), + ), + }, + { + name: "30d EMA", + title: title(`${titlePrefix}SOPR 30d EMA`), + bottom: list.map((item) => + baseline({ + metric: getSopr30d(item), + name: item.name, + color: item.color, + unit: Unit.ratio, + base: 1, + }), + ), + }, + ]; +} + +/** + * Create value breakdown tree (Profit/Loss Created/Destroyed) + * @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function valueBreakdownTree(list, title) { + return [ + { + name: "Profit", + tree: [ + { + name: "Created", + title: title("Profit Value Created"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.realized.profitValueCreated, + name, + color, + unit: Unit.usd, + }), + ), + }, + { + name: "Destroyed", + title: title("Profit Value Destroyed"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.realized.profitValueDestroyed, + name, + color, + unit: Unit.usd, + }), + ), + }, + ], + }, + { + name: "Loss", + tree: [ + { + name: "Created", + title: title("Loss Value Created"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.realized.lossValueCreated, + name, + color, + unit: Unit.usd, + }), + ), + }, + { + name: "Destroyed", + title: title("Loss Value Destroyed"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.realized.lossValueDestroyed, + name, + color, + unit: Unit.usd, + }), + ), + }, + ], + }, + ]; +} + +/** + * Create coins destroyed tree (Sum/Cumulative with Coinblocks/Coindays) + * @template {{ color: Color, name: string, tree: { activity: { coinblocksDestroyed: CountPattern, coindaysDestroyed: CountPattern } } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function coinsDestroyedTree(list, title) { + return [ + { + name: "Sum", + title: title("Coins Destroyed"), + bottom: list.flatMap(({ color, name, tree }) => [ + line({ + metric: tree.activity.coinblocksDestroyed.sum, + name, + color, + unit: Unit.coinblocks, + }), + line({ + metric: tree.activity.coindaysDestroyed.sum, + name, + color, + unit: Unit.coindays, + }), + ]), + }, + { + name: "Cumulative", + title: title("Cumulative Coins Destroyed"), + bottom: list.flatMap(({ color, name, tree }) => [ + line({ + metric: tree.activity.coinblocksDestroyed.cumulative, + name, + color, + unit: Unit.coinblocks, + }), + line({ + metric: tree.activity.coindaysDestroyed.cumulative, + name, + color, + unit: Unit.coindays, + }), + ]), + }, + ]; +} + +// ============================================================================ +// SOPR Helpers +// ============================================================================ + +/** + * Create SOPR series for single cohort (30d > 7d > raw order) + * @param {UtxoCohortObject | CohortWithoutRelative} cohort + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createSingleSoprSeries(cohort) { + return soprSeries(cohort.tree.realized); +} + +/** + * Create SOPR tree with normal and adjusted sub-sections + * @param {CohortAll | CohortFull | CohortWithAdjusted} cohort + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function createSingleSoprTreeWithAdjusted(cohort, title) { + const { realized } = cohort.tree; + return [ + { + name: "Normal", + title: title("SOPR"), + bottom: soprSeries(realized), + }, + { + name: "Adjusted", + title: title("Adjusted SOPR"), + bottom: soprSeries( + { + sopr: realized.adjustedSopr, + sopr7dEma: realized.adjustedSopr7dEma, + sopr30dEma: realized.adjustedSopr30dEma, + }, + "Adjusted SOPR", + ), + }, + ]; +} + +/** + * Create grouped SOPR tree with separate charts for each variant + * @template {readonly (UtxoCohortObject | CohortWithoutRelative)[]} T + * @param {T} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function createGroupedSoprTree(list, title) { + return groupedSoprCharts( + list, + (c) => c.tree.realized.sopr, + (c) => c.tree.realized.sopr7dEma, + (c) => c.tree.realized.sopr30dEma, + title, + "", + ); +} + +/** + * Create grouped SOPR tree with Normal and Adjusted sub-sections + * @param {readonly (CohortAll | CohortFull | CohortWithAdjusted)[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function createGroupedSoprTreeWithAdjusted(list, title) { + return [ + { + name: "Normal", + tree: groupedSoprCharts( + list, + (c) => c.tree.realized.sopr, + (c) => c.tree.realized.sopr7dEma, + (c) => c.tree.realized.sopr30dEma, + title, + "", + ), + }, + { + name: "Adjusted", + tree: groupedSoprCharts( + list, + (c) => c.tree.realized.adjustedSopr, + (c) => c.tree.realized.adjustedSopr7dEma, + (c) => c.tree.realized.adjustedSopr30dEma, + title, + "Adjusted ", + ), + }, + ]; +} + +// ============================================================================ +// Single Cohort Activity Section +// ============================================================================ + +/** + * Base activity section builder for single cohorts + * @param {Object} args + * @param {UtxoCohortObject | CohortWithoutRelative} args.cohort + * @param {(metric: string) => string} args.title + * @param {AnyFetchedSeriesBlueprint[]} [args.valueMetrics] - Optional additional value metrics + * @param {PartialOptionsTree} [args.soprTree] - Optional SOPR tree override + * @returns {PartialOptionsGroup} + */ +export function createActivitySection({ + cohort, + title, + valueMetrics = [], + soprTree, +}) { + const { tree, color } = cohort; + + return { + name: "Activity", + tree: [ + { + name: "Volume", + tree: [ + { + name: "Sum", + title: title("Sent Volume"), + bottom: [ + line({ + metric: tree.activity.sent14dEma.sats, + name: "14d EMA", + color: colors.ma._14d, + unit: Unit.sats, + defaultActive: false, + }), + line({ + metric: tree.activity.sent14dEma.bitcoin, + name: "14d EMA", + color: colors.ma._14d, + unit: Unit.btc, + defaultActive: false, + }), + line({ + metric: tree.activity.sent14dEma.dollars, + name: "14d EMA", + color: colors.ma._14d, + unit: Unit.usd, + defaultActive: false, + }), + line({ + metric: tree.activity.sent.sats.sum, + name: "sum", + color, + unit: Unit.sats, + }), + line({ + metric: tree.activity.sent.bitcoin.sum, + name: "sum", + color, + unit: Unit.btc, + }), + line({ + metric: tree.activity.sent.dollars.sum, + name: "sum", + color, + unit: Unit.usd, + }), + ], + }, + { + name: "Cumulative", + title: title("Sent Volume (Total)"), + bottom: [ + line({ + metric: tree.activity.sent.sats.cumulative, + name: "all-time", + color, + unit: Unit.sats, + }), + line({ + metric: tree.activity.sent.bitcoin.cumulative, + name: "all-time", + color, + unit: Unit.btc, + }), + line({ + metric: tree.activity.sent.dollars.cumulative, + name: "all-time", + color, + unit: Unit.usd, + }), + ], + }, + ], + }, + soprTree + ? { name: "SOPR", tree: soprTree } + : { + name: "SOPR", + title: title("SOPR"), + bottom: createSingleSoprSeries(cohort), + }, + { + name: "Sell Side Risk", + title: title("Sell Side Risk Ratio"), + bottom: createSingleSellSideRiskSeries(tree), + }, + { + name: "Value", + tree: [ + { + name: "Flows", + title: title("Profit & Capitulation Flows"), + bottom: createSingleCapitulationProfitFlowSeries(tree), + }, + { + name: "Created & Destroyed", + title: title("Value Created & Destroyed"), + bottom: [ + ...createSingleValueCreatedDestroyedSeries(tree), + ...valueMetrics, + ], + }, + { + name: "Breakdown", + tree: [ + { + name: "Profit", + title: title("Profit Value Created & Destroyed"), + bottom: [ + line({ + metric: tree.realized.profitValueCreated, + name: "Created", + color: colors.profit, + unit: Unit.usd, + }), + line({ + metric: tree.realized.profitValueDestroyed, + name: "Destroyed", + color: colors.loss, + unit: Unit.usd, + }), + ], + }, + { + name: "Loss", + title: title("Loss Value Created & Destroyed"), + bottom: [ + line({ + metric: tree.realized.lossValueCreated, + name: "Created", + color: colors.profit, + unit: Unit.usd, + }), + line({ + metric: tree.realized.lossValueDestroyed, + name: "Destroyed", + color: colors.loss, + unit: Unit.usd, + }), + ], + }, + ], + }, + ], + }, + { + name: "Coins Destroyed", + tree: [ + { + name: "Sum", + title: title("Coins Destroyed"), + bottom: [ + line({ + metric: tree.activity.coinblocksDestroyed.sum, + name: "Coinblocks", + color, + unit: Unit.coinblocks, + }), + line({ + metric: tree.activity.coindaysDestroyed.sum, + name: "Coindays", + color, + unit: Unit.coindays, + }), + ], + }, + { + name: "Cumulative", + title: title("Cumulative Coins Destroyed"), + bottom: [ + line({ + metric: tree.activity.coinblocksDestroyed.cumulative, + name: "Coinblocks", + color, + unit: Unit.coinblocks, + }), + line({ + metric: tree.activity.coindaysDestroyed.cumulative, + name: "Coindays", + color, + unit: Unit.coindays, + }), + ], + }, + ], + }, + ], + }; +} + +/** + * Activity section with adjusted values (for cohorts with RealizedPattern3/4) + * @param {{ cohort: CohortAll | CohortFull | CohortWithAdjusted, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createActivitySectionWithAdjusted({ cohort, title }) { + const { tree } = cohort; + return createActivitySection({ + cohort, + title, + soprTree: createSingleSoprTreeWithAdjusted(cohort, title), + valueMetrics: [ + line({ + metric: tree.realized.adjustedValueCreated, + name: "Adjusted Created", + color: colors.adjustedCreated, + unit: Unit.usd, + defaultActive: false, + }), + line({ + metric: tree.realized.adjustedValueDestroyed, + name: "Adjusted Destroyed", + color: colors.adjustedDestroyed, + unit: Unit.usd, + defaultActive: false, + }), + ], + }); +} + +// ============================================================================ +// Grouped Cohort Activity Section +// ============================================================================ + +/** + * Create grouped flows tree (Profit Flow, Capitulation Flow) + * @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function groupedFlowsTree(list, title) { + return [ + { + name: "Profit", + title: title("Profit Flow"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.realized.profitFlow, + name, + color, + unit: Unit.usd, + }), + ), + }, + { + name: "Capitulation", + title: title("Capitulation Flow"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.realized.capitulationFlow, + name, + color, + unit: Unit.usd, + }), + ), + }, + ]; +} + +/** + * Create grouped value tree (Flows, Created, Destroyed, Breakdown) + * @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function createGroupedValueTree(list, title) { + return [ + { name: "Flows", tree: groupedFlowsTree(list, title) }, + { + name: "Created", + title: title("Value Created"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.realized.valueCreated, + name, + color, + unit: Unit.usd, + }), + ), + }, + { + name: "Destroyed", + title: title("Value Destroyed"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.realized.valueDestroyed, + name, + color, + unit: Unit.usd, + }), + ), + }, + { name: "Breakdown", tree: valueBreakdownTree(list, title) }, + ]; +} + +/** + * Generic grouped activity section builder + * @template {readonly (UtxoCohortObject | CohortWithoutRelative)[]} T + * @param {Object} args + * @param {T} args.list + * @param {(metric: string) => string} args.title + * @param {PartialOptionsTree} [args.soprTree] - Optional SOPR tree override + * @param {PartialOptionsTree} [args.valueTree] - Optional value tree (defaults to basic created/destroyed) + * @returns {PartialOptionsGroup} + */ +export function createGroupedActivitySection({ + list, + title, + soprTree, + valueTree, +}) { + return { + name: "Activity", + tree: [ + { + name: "Volume", + tree: [ + { + name: "14d EMA", + title: title("Sent Volume 14d EMA"), + bottom: list.flatMap(({ color, name, tree }) => + satsBtcUsd({ pattern: tree.activity.sent14dEma, name, color }), + ), + }, + { + name: "Sum", + title: title("Sent Volume"), + bottom: list.flatMap(({ color, name, tree }) => + satsBtcUsd({ + pattern: { + sats: tree.activity.sent.sats.sum, + bitcoin: tree.activity.sent.bitcoin.sum, + dollars: tree.activity.sent.dollars.sum, + }, + name, + color, + }), + ), + }, + ], + }, + { + name: "SOPR", + tree: soprTree ?? createGroupedSoprTree(list, title), + }, + { + name: "Sell Side Risk", + title: title("Sell Side Risk Ratio"), + bottom: createGroupedSellSideRiskSeries(list), + }, + { + name: "Value", + tree: valueTree ?? createGroupedValueTree(list, title), + }, + { name: "Coins Destroyed", tree: coinsDestroyedTree(list, title) }, + ], + }; +} + +/** + * Create grouped value tree with adjusted values (Flows, Normal, Adjusted, Breakdown) + * @param {readonly (CohortAll | CohortFull | CohortWithAdjusted)[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function createGroupedValueTreeWithAdjusted(list, title) { + return [ + { name: "Flows", tree: groupedFlowsTree(list, title) }, + { + name: "Normal", + tree: [ + { + name: "Created", + title: title("Value Created"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.realized.valueCreated, + name, + color, + unit: Unit.usd, + }), + ), + }, + { + name: "Destroyed", + title: title("Value Destroyed"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.realized.valueDestroyed, + name, + color, + unit: Unit.usd, + }), + ), + }, + ], + }, + { + name: "Adjusted", + tree: [ + { + name: "Created", + title: title("Adjusted Value Created"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.realized.adjustedValueCreated, + name, + color, + unit: Unit.usd, + }), + ), + }, + { + name: "Destroyed", + title: title("Adjusted Value Destroyed"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.realized.adjustedValueDestroyed, + name, + color, + unit: Unit.usd, + }), + ), + }, + ], + }, + { name: "Breakdown", tree: valueBreakdownTree(list, title) }, + ]; +} + +/** + * Grouped activity section with adjusted values (for cohorts with RealizedPattern3/4) + * @param {{ list: readonly (CohortAll | CohortFull | CohortWithAdjusted)[], title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createGroupedActivitySectionWithAdjusted({ list, title }) { + return createGroupedActivitySection({ + list, + title, + soprTree: createGroupedSoprTreeWithAdjusted(list, title), + valueTree: createGroupedValueTreeWithAdjusted(list, title), + }); +} diff --git a/website/scripts/options/distribution/address.js b/website/scripts/options/distribution/address.js index f1f7965b5..88d335747 100644 --- a/website/scripts/options/distribution/address.js +++ b/website/scripts/options/distribution/address.js @@ -291,13 +291,6 @@ function createRealizedPnlSection(args, title) { unit: Unit.usd, defaultActive: false, }), - line({ - metric: realized.totalRealizedPnl, - name: "Total", - color: colors.default, - unit: Unit.usd, - defaultActive: false, - }), baseline({ metric: realized.realizedProfitRelToRealizedCap.sum, name: "Profit", @@ -324,6 +317,13 @@ function createRealizedPnlSection(args, title) { unit: Unit.pctRcap, defaultActive: false, }), + line({ + metric: realized.totalRealizedPnl, + name: "Total", + color: colors.default, + unit: Unit.usd, + defaultActive: false, + }), ], }, { @@ -450,6 +450,24 @@ function createRealizedPnlSection(args, title) { { name: "Sent In P/L", tree: [ + { + name: "In Profit 14d EMA", + title: title("Sent In Profit 14d EMA"), + bottom: satsBtcUsd({ + pattern: realized.sentInProfit14dEma, + name: "14d EMA", + color: colors.profit, + }), + }, + { + name: "In Loss 14d EMA", + title: title("Sent In Loss 14d EMA"), + bottom: satsBtcUsd({ + pattern: realized.sentInLoss14dEma, + name: "14d EMA", + color: colors.loss, + }), + }, { name: "In Profit", title: title("Sent In Profit"), @@ -540,24 +558,6 @@ function createRealizedPnlSection(args, title) { }), ], }, - { - name: "In Profit 14d EMA", - title: title("Sent In Profit 14d EMA"), - bottom: satsBtcUsd({ - pattern: realized.sentInProfit14dEma, - name: "14d EMA", - color: colors.profit, - }), - }, - { - name: "In Loss 14d EMA", - title: title("Sent In Loss 14d EMA"), - bottom: satsBtcUsd({ - pattern: realized.sentInLoss14dEma, - name: "14d EMA", - color: colors.loss, - }), - }, ], }, ]; @@ -678,6 +678,28 @@ function createGroupedRealizedPnlSection(list, title) { { name: "Sent In P/L", tree: [ + { + name: "In Profit 14d EMA", + title: title("Sent In Profit 14d EMA"), + bottom: list.flatMap(({ color, name, tree }) => + satsBtcUsd({ + pattern: tree.realized.sentInProfit14dEma, + name, + color, + }), + ), + }, + { + name: "In Loss 14d EMA", + title: title("Sent In Loss 14d EMA"), + bottom: list.flatMap(({ color, name, tree }) => + satsBtcUsd({ + pattern: tree.realized.sentInLoss14dEma, + name, + color, + }), + ), + }, { name: "In Profit", title: title("Sent In Profit"), @@ -774,28 +796,6 @@ function createGroupedRealizedPnlSection(list, title) { }), ]), }, - { - name: "In Profit 14d EMA", - title: title("Sent In Profit 14d EMA"), - bottom: list.flatMap(({ color, name, tree }) => - satsBtcUsd({ - pattern: tree.realized.sentInProfit14dEma, - name, - color, - }), - ), - }, - { - name: "In Loss 14d EMA", - title: title("Sent In Loss 14d EMA"), - bottom: list.flatMap(({ color, name, tree }) => - satsBtcUsd({ - pattern: tree.realized.sentInLoss14dEma, - name, - color, - }), - ), - }, ], }, ]; @@ -811,7 +811,7 @@ function createGroupedRealizedPnlSection(list, title) { function createUnrealizedSection(list, useGroupName, title) { return [ { - name: "Unrealized", + name: "Profitability", tree: [ { name: "Profit", @@ -962,7 +962,7 @@ function createUnrealizedSection(list, useGroupName, title) { metric: tree.relative.investedCapitalInProfitPct, name: useGroupName ? name : "In Profit", color: useGroupName ? color : colors.profit, - unit: Unit.pctRcap, + unit: Unit.pctOwnRcap, }), ]), }, @@ -974,7 +974,7 @@ function createUnrealizedSection(list, useGroupName, title) { metric: tree.relative.investedCapitalInLossPct, name: useGroupName ? name : "In Loss", color: useGroupName ? color : colors.loss, - unit: Unit.pctRcap, + unit: Unit.pctOwnRcap, }), ]), }, @@ -1104,6 +1104,13 @@ function createActivitySection(args, title) { { name: "Sent", tree: [ + { + name: "14d EMA", + title: title("Sent 14d EMA"), + bottom: list.flatMap(({ color, name, tree }) => + satsBtcUsd({ pattern: tree.activity.sent14dEma, name, color }), + ), + }, { name: "Sum", title: title("Sent"), @@ -1119,13 +1126,6 @@ function createActivitySection(args, title) { }), ), }, - { - name: "14d EMA", - title: title("Sent 14d EMA"), - bottom: list.flatMap(({ color, name, tree }) => - satsBtcUsd({ pattern: tree.activity.sent14dEma, name, color }), - ), - }, ], }, ], diff --git a/website/scripts/options/distribution/data.js b/website/scripts/options/distribution/data.js index 586410a8c..ce7e671c7 100644 --- a/website/scripts/options/distribution/data.js +++ b/website/scripts/options/distribution/data.js @@ -123,13 +123,17 @@ export function buildCohortData() { // Addresses above amount const addressesAboveAmount = entries(addressCohorts.geAmount).map( - ([key, tree]) => { + ([key, cohort]) => { const names = GE_AMOUNT_NAMES[key]; return { name: names.short, title: `Addresses ${names.long}`, color: colors.amount[key], - tree, + tree: cohort, + addrCount: { + count: cohort.addrCount, + _30dChange: cohort.addrCount30dChange, + }, }; }, ); @@ -147,13 +151,17 @@ export function buildCohortData() { // Addresses under amount const addressesUnderAmount = entries(addressCohorts.ltAmount).map( - ([key, tree]) => { + ([key, cohort]) => { const names = LT_AMOUNT_NAMES[key]; return { name: names.short, title: `Addresses ${names.long}`, color: colors.amount[key], - tree, + tree: cohort, + addrCount: { + count: cohort.addrCount, + _30dChange: cohort.addrCount30dChange, + }, }; }, ); @@ -173,13 +181,17 @@ export function buildCohortData() { // Addresses amount ranges const addressesAmountRanges = entries(addressCohorts.amountRange).map( - ([key, tree]) => { + ([key, cohort]) => { const names = AMOUNT_RANGE_NAMES[key]; return { name: names.short, title: `Addresses ${names.long}`, color: colors.amountRange[key], - tree, + tree: cohort, + addrCount: { + count: cohort.addrCount, + _30dChange: cohort.addrCount30dChange, + }, }; }, ); diff --git a/website/scripts/options/distribution/holdings.js b/website/scripts/options/distribution/holdings.js index 1f6adaa7d..4a6027089 100644 --- a/website/scripts/options/distribution/holdings.js +++ b/website/scripts/options/distribution/holdings.js @@ -16,14 +16,89 @@ import { Unit } from "../../utils/units.js"; import { line, baseline } from "../series.js"; import { satsBtcUsd, satsBtcUsdBaseline } from "../shared.js"; +import { colors } from "../../utils/colors.js"; +import { priceLines } from "../constants.js"; /** * @param {UtxoCohortObject | CohortWithoutRelative} cohort * @returns {AnyFetchedSeriesBlueprint[]} */ function createSingleSupplySeries(cohort) { - const { color, tree } = cohort; - return [...satsBtcUsd({ pattern: tree.supply.total, name: "Supply", color })]; + const { tree } = cohort; + return [ + ...satsBtcUsd({ + pattern: tree.supply.total, + name: "Total", + color: colors.default, + }), + ...satsBtcUsd({ + pattern: tree.unrealized.supplyInProfit, + name: "In Profit", + color: colors.profit, + }), + ...satsBtcUsd({ + pattern: tree.unrealized.supplyInLoss, + name: "In Loss", + color: colors.loss, + }), + // Halved supply (sparse line) + ...satsBtcUsd({ + pattern: tree.supply.halved, + name: "Halved", + color: colors.gray, + style: 4, + }), + ]; +} + +/** + * Supply series for CohortAll (has % of Own Supply but not % of Circulating) + * @param {CohortAll} cohort + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createSingleSupplySeriesAll(cohort) { + const { tree } = cohort; + return [ + ...satsBtcUsd({ + pattern: tree.supply.total, + name: "Total", + color: colors.default, + }), + ...satsBtcUsd({ + pattern: tree.unrealized.supplyInProfit, + name: "In Profit", + color: colors.profit, + }), + ...satsBtcUsd({ + pattern: tree.unrealized.supplyInLoss, + name: "In Loss", + color: colors.loss, + }), + // Halved supply (sparse line) + ...satsBtcUsd({ + pattern: tree.supply.halved, + name: "Halved", + color: colors.gray, + style: 4, + }), + // % of Own Supply + line({ + metric: tree.relative.supplyInProfitRelToOwnSupply, + name: "In Profit", + color: colors.profit, + unit: Unit.pctOwn, + }), + line({ + metric: tree.relative.supplyInLossRelToOwnSupply, + name: "In Loss", + color: colors.loss, + unit: Unit.pctOwn, + }), + ...priceLines({ + numbers: [100, 50, 0], + unit: Unit.pctOwn, + }), + ]; } /** @@ -31,7 +106,10 @@ function createSingleSupplySeries(cohort) { * @returns {AnyFetchedSeriesBlueprint[]} */ function createSingle30dChangeSeries(cohort) { - return satsBtcUsdBaseline({ pattern: cohort.tree.supply._30dChange, name: "30d Change" }); + return satsBtcUsdBaseline({ + pattern: cohort.tree.supply._30dChange, + name: "30d Change", + }); } /** @@ -79,18 +157,121 @@ function createSingleAddrCount30dChangeSeries(cohort) { } /** + * Create supply series with % of Circulating (for cohorts with relative data) * @param {CohortFull | CohortWithAdjusted | CohortBasicWithMarketCap | CohortMinAge} cohort * @returns {AnyFetchedSeriesBlueprint[]} */ -function createSingleRelativeSeries(cohort) { - const { color, tree } = cohort; +function createSingleSupplySeriesWithRelative(cohort) { + const { tree } = cohort; return [ + ...satsBtcUsd({ + pattern: tree.supply.total, + name: "Total", + color: colors.default, + }), + ...satsBtcUsd({ + pattern: tree.unrealized.supplyInProfit, + name: "In Profit", + color: colors.profit, + }), + ...satsBtcUsd({ + pattern: tree.unrealized.supplyInLoss, + name: "In Loss", + color: colors.loss, + }), + // Halved supply (sparse line) + ...satsBtcUsd({ + pattern: tree.supply.halved, + name: "Halved", + color: colors.gray, + style: 4, + }), + // % of Circulating Supply line({ metric: tree.relative.supplyRelToCirculatingSupply, - name: "% of Circulating", - color, + name: "Total", + color: colors.default, unit: Unit.pctSupply, }), + line({ + metric: tree.relative.supplyInProfitRelToCirculatingSupply, + name: "In Profit", + color: colors.profit, + unit: Unit.pctSupply, + }), + line({ + metric: tree.relative.supplyInLossRelToCirculatingSupply, + name: "In Loss", + color: colors.loss, + unit: Unit.pctSupply, + }), + // % of Own Supply + line({ + metric: tree.relative.supplyInProfitRelToOwnSupply, + name: "In Profit", + color: colors.profit, + unit: Unit.pctOwn, + }), + line({ + metric: tree.relative.supplyInLossRelToOwnSupply, + name: "In Loss", + color: colors.loss, + unit: Unit.pctOwn, + }), + ...priceLines({ + numbers: [100, 50, 0], + unit: Unit.pctOwn, + }), + ]; +} + +/** + * Supply series with % of Own Supply only (for cohorts without % of Circulating) + * @param {CohortAgeRange | CohortBasicWithoutMarketCap} cohort + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createSingleSupplySeriesWithOwnSupply(cohort) { + const { tree } = cohort; + return [ + ...satsBtcUsd({ + pattern: tree.unrealized.supplyInProfit, + name: "In Profit", + color: colors.profit, + }), + ...satsBtcUsd({ + pattern: tree.unrealized.supplyInLoss, + name: "In Loss", + color: colors.loss, + }), + ...satsBtcUsd({ + pattern: tree.supply.total, + name: "Total", + color: colors.default, + }), + // Halved supply (sparse line) + ...satsBtcUsd({ + pattern: tree.supply.halved, + name: "Halved", + color: colors.gray, + style: 4, + }), + // % of Own Supply + line({ + metric: tree.relative.supplyInProfitRelToOwnSupply, + name: "In Profit", + color: colors.profit, + unit: Unit.pctOwn, + }), + line({ + metric: tree.relative.supplyInLossRelToOwnSupply, + name: "In Loss", + color: colors.loss, + unit: Unit.pctOwn, + }), + ...priceLines({ + numbers: [100, 50, 0], + unit: Unit.pctOwn, + }), ]; } @@ -132,17 +313,18 @@ export function createHoldingsSection({ cohort, title }) { } /** - * @param {{ cohort: CohortFull | CohortWithAdjusted | CohortBasicWithMarketCap | CohortMinAge, title: (metric: string) => string }} args + * Holdings section with % of Own Supply only (for cohorts without % of Circulating) + * @param {{ cohort: CohortAgeRange | CohortBasicWithoutMarketCap, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ -export function createHoldingsSectionWithRelative({ cohort, title }) { +export function createHoldingsSectionWithOwnSupply({ cohort, title }) { return { name: "Holdings", tree: [ { name: "Supply", title: title("Supply"), - bottom: createSingleSupplySeries(cohort), + bottom: createSingleSupplySeriesWithOwnSupply(cohort), }, { name: "UTXO Count", @@ -164,10 +346,42 @@ export function createHoldingsSectionWithRelative({ cohort, title }) { }, ], }, + ], + }; +} + +/** + * @param {{ cohort: CohortFull | CohortWithAdjusted | CohortBasicWithMarketCap | CohortMinAge, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createHoldingsSectionWithRelative({ cohort, title }) { + return { + name: "Holdings", + tree: [ { - name: "Relative", - title: title("Relative to Circulating Supply"), - bottom: createSingleRelativeSeries(cohort), + name: "Supply", + title: title("Supply"), + bottom: createSingleSupplySeriesWithRelative(cohort), + }, + { + name: "UTXO Count", + title: title("UTXO Count"), + bottom: createSingleUtxoCountSeries(cohort), + }, + { + name: "30d Changes", + tree: [ + { + name: "Supply", + title: title("Supply 30d Change"), + bottom: createSingle30dChangeSeries(cohort), + }, + { + name: "UTXO Count", + title: title("UTXO Count 30d Change"), + bottom: createSingleUtxoCount30dChangeSeries(cohort), + }, + ], }, ], }; @@ -184,7 +398,7 @@ export function createHoldingsSectionAll({ cohort, title }) { { name: "Supply", title: title("Supply"), - bottom: createSingleSupplySeries(cohort), + bottom: createSingleSupplySeriesAll(cohort), }, { name: "UTXO Count", @@ -238,7 +452,62 @@ export function createHoldingsSectionAddress({ cohort, title }) { { name: "Supply", title: title("Supply"), - bottom: createSingleSupplySeries(cohort), + bottom: createSingleSupplySeriesWithOwnSupply(cohort), + }, + { + name: "UTXO Count", + title: title("UTXO Count"), + bottom: createSingleUtxoCountSeries(cohort), + }, + { + name: "Address Count", + title: title("Address Count"), + bottom: [ + line({ + metric: cohort.addrCount.count, + name: "Address Count", + color: cohort.color, + unit: Unit.count, + }), + ], + }, + { + name: "30d Changes", + tree: [ + { + name: "Supply", + title: title("Supply 30d Change"), + bottom: createSingle30dChangeSeries(cohort), + }, + { + name: "UTXO Count", + title: title("UTXO Count 30d Change"), + bottom: createSingleUtxoCount30dChangeSeries(cohort), + }, + { + name: "Address Count", + title: title("Address Count 30d Change"), + bottom: createSingleAddrCount30dChangeSeries(cohort), + }, + ], + }, + ], + }; +} + +/** + * Holdings section for address amount cohorts (has relative supply + address count) + * @param {{ cohort: AddressCohortObject, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createHoldingsSectionAddressAmount({ cohort, title }) { + return { + name: "Holdings", + tree: [ + { + name: "Supply", + title: title("Supply"), + bottom: createSingleSupplySeriesWithRelative(cohort), }, { name: "UTXO Count", @@ -291,10 +560,67 @@ export function createGroupedHoldingsSectionAddress({ list, title }) { tree: [ { name: "Supply", - title: title("Supply"), - bottom: list.flatMap(({ name, color, tree }) => - satsBtcUsd({ pattern: tree.supply.total, name, color }), - ), + tree: [ + { + name: "Total", + title: title("Supply"), + bottom: list.flatMap(({ name, color, tree }) => + satsBtcUsd({ pattern: tree.supply.total, name, color }), + ), + }, + { + name: "In Profit", + title: title("Supply In Profit"), + bottom: [ + ...list.flatMap(({ name, color, tree }) => + satsBtcUsd({ + pattern: tree.unrealized.supplyInProfit, + name, + color, + }), + ), + // % of Own Supply + ...list.map(({ name, color, tree }) => + line({ + metric: tree.relative.supplyInProfitRelToOwnSupply, + name, + color, + unit: Unit.pctOwn, + }), + ), + ...priceLines({ + numbers: [100, 50, 0], + unit: Unit.pctOwn, + }), + ], + }, + { + name: "In Loss", + title: title("Supply In Loss"), + bottom: [ + ...list.flatMap(({ name, color, tree }) => + satsBtcUsd({ + pattern: tree.unrealized.supplyInLoss, + name, + color, + }), + ), + // % of Own Supply + ...list.map(({ name, color, tree }) => + line({ + metric: tree.relative.supplyInLossRelToOwnSupply, + name, + color, + unit: Unit.pctOwn, + }), + ), + ...priceLines({ + numbers: [100, 50, 0], + unit: Unit.pctOwn, + }), + ], + }, + ], }, { name: "UTXO Count", @@ -322,7 +648,176 @@ export function createGroupedHoldingsSectionAddress({ list, title }) { name: "Supply", title: title("Supply 30d Change"), bottom: list.flatMap(({ name, color, tree }) => - satsBtcUsdBaseline({ pattern: tree.supply._30dChange, name, color }), + satsBtcUsdBaseline({ + pattern: tree.supply._30dChange, + name, + color, + }), + ), + }, + { + name: "UTXO Count", + title: title("UTXO Count 30d Change"), + bottom: list.map(({ name, color, tree }) => + baseline({ + metric: tree.outputs.utxoCount30dChange, + name, + unit: Unit.count, + color, + }), + ), + }, + { + name: "Address Count", + title: title("Address Count 30d Change"), + bottom: list.map(({ name, color, addrCount }) => + baseline({ + metric: addrCount._30dChange, + name, + unit: Unit.count, + color, + }), + ), + }, + ], + }, + ], + }; +} + +/** + * Grouped holdings section for address amount cohorts (has relative supply + address count) + * @param {{ list: readonly AddressCohortObject[], title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createGroupedHoldingsSectionAddressAmount({ list, title }) { + return { + name: "Holdings", + tree: [ + { + name: "Supply", + tree: [ + { + name: "Total", + title: title("Supply"), + bottom: [ + ...list.flatMap(({ name, color, tree }) => + satsBtcUsd({ pattern: tree.supply.total, name, color }), + ), + // % of Circulating + ...list.map(({ name, color, tree }) => + line({ + metric: tree.relative.supplyRelToCirculatingSupply, + name, + color, + unit: Unit.pctSupply, + }), + ), + ], + }, + { + name: "In Profit", + title: title("Supply In Profit"), + bottom: [ + ...list.flatMap(({ name, color, tree }) => + satsBtcUsd({ + pattern: tree.unrealized.supplyInProfit, + name, + color, + }), + ), + // % of Circulating + ...list.map(({ name, color, tree }) => + line({ + metric: tree.relative.supplyInProfitRelToCirculatingSupply, + name, + color, + unit: Unit.pctSupply, + }), + ), + // % of Own Supply + ...list.map(({ name, color, tree }) => + line({ + metric: tree.relative.supplyInProfitRelToOwnSupply, + name, + color, + unit: Unit.pctOwn, + }), + ), + ...priceLines({ + numbers: [100, 50, 0], + unit: Unit.pctOwn, + }), + ], + }, + { + name: "In Loss", + title: title("Supply In Loss"), + bottom: [ + ...list.flatMap(({ name, color, tree }) => + satsBtcUsd({ + pattern: tree.unrealized.supplyInLoss, + name, + color, + }), + ), + // % of Circulating + ...list.map(({ name, color, tree }) => + line({ + metric: tree.relative.supplyInLossRelToCirculatingSupply, + name, + color, + unit: Unit.pctSupply, + }), + ), + // % of Own Supply + ...list.map(({ name, color, tree }) => + line({ + metric: tree.relative.supplyInLossRelToOwnSupply, + name, + color, + unit: Unit.pctOwn, + }), + ), + ...priceLines({ + numbers: [100, 50, 0], + unit: Unit.pctOwn, + }), + ], + }, + ], + }, + { + name: "UTXO Count", + title: title("UTXO Count"), + bottom: list.map(({ name, color, tree }) => + line({ + metric: tree.outputs.utxoCount, + name, + color, + unit: Unit.count, + }), + ), + }, + { + name: "Address Count", + title: title("Address Count"), + bottom: list.map(({ name, color, addrCount }) => + line({ metric: addrCount.count, name, color, unit: Unit.count }), + ), + }, + { + name: "30d Changes", + tree: [ + { + name: "Supply", + title: title("Supply 30d Change"), + bottom: list.flatMap(({ name, color, tree }) => + satsBtcUsdBaseline({ + pattern: tree.supply._30dChange, + name, + color, + }), ), }, { @@ -366,10 +861,37 @@ export function createGroupedHoldingsSection({ list, title }) { tree: [ { name: "Supply", - title: title("Supply"), - bottom: list.flatMap(({ name, color, tree }) => - satsBtcUsd({ pattern: tree.supply.total, name, color }), - ), + tree: [ + { + name: "Total", + title: title("Supply"), + bottom: list.flatMap(({ name, color, tree }) => + satsBtcUsd({ pattern: tree.supply.total, name, color }), + ), + }, + { + name: "In Profit", + title: title("Supply In Profit"), + bottom: list.flatMap(({ name, color, tree }) => + satsBtcUsd({ + pattern: tree.unrealized.supplyInProfit, + name, + color, + }), + ), + }, + { + name: "In Loss", + title: title("Supply In Loss"), + bottom: list.flatMap(({ name, color, tree }) => + satsBtcUsd({ + pattern: tree.unrealized.supplyInLoss, + name, + color, + }), + ), + }, + ], }, { name: "UTXO Count", @@ -390,7 +912,128 @@ export function createGroupedHoldingsSection({ list, title }) { name: "Supply", title: title("Supply 30d Change"), bottom: list.flatMap(({ name, color, tree }) => - satsBtcUsdBaseline({ pattern: tree.supply._30dChange, name, color }), + satsBtcUsdBaseline({ + pattern: tree.supply._30dChange, + name, + color, + }), + ), + }, + { + name: "UTXO Count", + title: title("UTXO Count 30d Change"), + bottom: list.map(({ name, color, tree }) => + baseline({ + metric: tree.outputs.utxoCount30dChange, + name, + unit: Unit.count, + color, + }), + ), + }, + ], + }, + ], + }; +} + +/** + * Grouped holdings section with % of Own Supply only (for cohorts without % of Circulating) + * @param {{ list: readonly (CohortAgeRange | CohortBasicWithoutMarketCap)[], title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createGroupedHoldingsSectionWithOwnSupply({ list, title }) { + return { + name: "Holdings", + tree: [ + { + name: "Supply", + tree: [ + { + name: "In Profit", + title: title("Supply In Profit"), + bottom: [ + ...list.flatMap(({ name, color, tree }) => + satsBtcUsd({ + pattern: tree.unrealized.supplyInProfit, + name, + color, + }), + ), + // % of Own Supply + ...list.map(({ name, color, tree }) => + line({ + metric: tree.relative.supplyInProfitRelToOwnSupply, + name, + color, + unit: Unit.pctOwn, + }), + ), + ...priceLines({ + numbers: [100, 50, 0], + unit: Unit.pctOwn, + }), + ], + }, + { + name: "In Loss", + title: title("Supply In Loss"), + bottom: [ + ...list.flatMap(({ name, color, tree }) => + satsBtcUsd({ + pattern: tree.unrealized.supplyInLoss, + name, + color, + }), + ), + // % of Own Supply + ...list.map(({ name, color, tree }) => + line({ + metric: tree.relative.supplyInLossRelToOwnSupply, + name, + color, + unit: Unit.pctOwn, + }), + ), + ...priceLines({ + numbers: [100, 50, 0], + unit: Unit.pctOwn, + }), + ], + }, + { + name: "Total", + title: title("Supply"), + bottom: list.flatMap(({ name, color, tree }) => + satsBtcUsd({ pattern: tree.supply.total, name, color }), + ), + }, + ], + }, + { + name: "UTXO Count", + title: title("UTXO Count"), + bottom: list.map(({ name, color, tree }) => + line({ + metric: tree.outputs.utxoCount, + name, + color, + unit: Unit.count, + }), + ), + }, + { + name: "30d Changes", + tree: [ + { + name: "Supply", + title: title("Supply 30d Change"), + bottom: list.flatMap(({ name, color, tree }) => + satsBtcUsdBaseline({ + pattern: tree.supply._30dChange, + name, + color, + }), ), }, { @@ -421,10 +1064,96 @@ export function createGroupedHoldingsSectionWithRelative({ list, title }) { tree: [ { name: "Supply", - title: title("Supply"), - bottom: list.flatMap(({ name, color, tree }) => - satsBtcUsd({ pattern: tree.supply.total, name, color }), - ), + tree: [ + { + name: "Total", + title: title("Supply"), + bottom: [ + ...list.flatMap(({ name, color, tree }) => + satsBtcUsd({ pattern: tree.supply.total, name, color }), + ), + // % of Circulating + ...list.map(({ name, color, tree }) => + line({ + metric: tree.relative.supplyRelToCirculatingSupply, + name, + color, + unit: Unit.pctSupply, + }), + ), + ], + }, + { + name: "In Profit", + title: title("Supply In Profit"), + bottom: [ + ...list.flatMap(({ name, color, tree }) => + satsBtcUsd({ + pattern: tree.unrealized.supplyInProfit, + name, + color, + }), + ), + // % of Circulating + ...list.map(({ name, color, tree }) => + line({ + metric: tree.relative.supplyInProfitRelToCirculatingSupply, + name, + color, + unit: Unit.pctSupply, + }), + ), + // % of Own Supply + ...list.map(({ name, color, tree }) => + line({ + metric: tree.relative.supplyInProfitRelToOwnSupply, + name, + color, + unit: Unit.pctOwn, + }), + ), + ...priceLines({ + numbers: [100, 50, 0], + unit: Unit.pctOwn, + }), + ], + }, + { + name: "In Loss", + title: title("Supply In Loss"), + bottom: [ + ...list.flatMap(({ name, color, tree }) => + satsBtcUsd({ + pattern: tree.unrealized.supplyInLoss, + name, + color, + }), + ), + // % of Circulating + ...list.map(({ name, color, tree }) => + line({ + metric: tree.relative.supplyInLossRelToCirculatingSupply, + name, + color, + unit: Unit.pctSupply, + }), + ), + // % of Own Supply + ...list.map(({ name, color, tree }) => + line({ + metric: tree.relative.supplyInLossRelToOwnSupply, + name, + color, + unit: Unit.pctOwn, + }), + ), + ...priceLines({ + numbers: [100, 50, 0], + unit: Unit.pctOwn, + }), + ], + }, + ], }, { name: "UTXO Count", @@ -445,7 +1174,11 @@ export function createGroupedHoldingsSectionWithRelative({ list, title }) { name: "Supply", title: title("Supply 30d Change"), bottom: list.flatMap(({ name, color, tree }) => - satsBtcUsdBaseline({ pattern: tree.supply._30dChange, name, color }), + satsBtcUsdBaseline({ + pattern: tree.supply._30dChange, + name, + color, + }), ), }, { @@ -462,18 +1195,6 @@ export function createGroupedHoldingsSectionWithRelative({ list, title }) { }, ], }, - { - name: "Relative", - title: title("Relative to Circulating Supply"), - bottom: list.map(({ name, color, tree }) => - line({ - metric: tree.relative.supplyRelToCirculatingSupply, - name, - color, - unit: Unit.pctSupply, - }), - ), - }, ], }; } diff --git a/website/scripts/options/distribution/index.js b/website/scripts/options/distribution/index.js index 4be0516d7..ade64d2af 100644 --- a/website/scripts/options/distribution/index.js +++ b/website/scripts/options/distribution/index.js @@ -1,26 +1,75 @@ /** * Cohort module - exports all cohort-related functionality + * + * Folder builders compose sections from building blocks: + * - holdings.js: Supply, UTXO Count, Address Count + * - valuation.js: Realized Cap, Market Cap, MVRV + * - prices.js: Realized Price, ratios + * - cost-basis.js: Cost basis percentiles + * - profitability.js: Unrealized/Realized P&L, Invested Capital + * - activity.js: SOPR, Volume, Lifespan */ -// Cohort data builder +import { formatCohortTitle } from "../shared.js"; + +// Section builders +import { + createHoldingsSection, + createHoldingsSectionAll, + createHoldingsSectionAddress, + createHoldingsSectionAddressAmount, + createHoldingsSectionWithRelative, + createHoldingsSectionWithOwnSupply, + createGroupedHoldingsSection, + createGroupedHoldingsSectionAddress, + createGroupedHoldingsSectionAddressAmount, + createGroupedHoldingsSectionWithRelative, + createGroupedHoldingsSectionWithOwnSupply, +} from "./holdings.js"; +import { + createValuationSection, + createValuationSectionFull, + createGroupedValuationSection, + createGroupedValuationSectionWithOwnMarketCap, +} from "./valuation.js"; +import { + createPricesSectionFull, + createPricesSectionBasic, + createGroupedPricesSection, +} from "./prices.js"; +import { + createCostBasisSection, + createCostBasisSectionWithPercentiles, + createGroupedCostBasisSection, + createGroupedCostBasisSectionWithPercentiles, +} from "./cost-basis.js"; +import { + createProfitabilitySection, + createProfitabilitySectionAll, + createProfitabilitySectionFull, + createProfitabilitySectionWithNupl, + createProfitabilitySectionWithPeakRegret, + createProfitabilitySectionWithInvestedCapitalPct, + createProfitabilitySectionBasicWithInvestedCapitalPct, + createProfitabilitySectionLongTerm, + createGroupedProfitabilitySection, + createGroupedProfitabilitySectionWithNupl, + createGroupedProfitabilitySectionWithPeakRegret, + createGroupedProfitabilitySectionWithInvestedCapitalPct, + createGroupedProfitabilitySectionBasicWithInvestedCapitalPct, + createGroupedProfitabilitySectionLongTerm, +} from "./profitability.js"; +import { + createActivitySection, + createActivitySectionWithAdjusted, + createGroupedActivitySection, + createGroupedActivitySectionWithAdjusted, +} from "./activity.js"; + +// Re-export data builder export { buildCohortData } from "./data.js"; -// Cohort folder builders (type-safe!) -export { - createCohortFolderAll, - createCohortFolderFull, - createCohortFolderWithAdjusted, - createCohortFolderWithNupl, - createCohortFolderAgeRange, - createCohortFolderMinAge, - createCohortFolderBasicWithMarketCap, - createCohortFolderBasicWithoutMarketCap, - createCohortFolderWithoutRelative, - createCohortFolderAddress, -} from "./utxo.js"; -export { createAddressCohortFolder } from "./address.js"; - -// Shared helpers +// Re-export shared helpers export { createSingleSupplySeries, createGroupedSupplyTotalSeries, @@ -33,3 +82,426 @@ export { createRealizedCapSeries, createCostBasisPercentilesSeries, } from "./shared.js"; + +// ============================================================================ +// Folder Builders +// ============================================================================ + +/** + * All folder: for the special "All" cohort (adjustedSopr + percentiles + RelToMarketCap) + * @param {CohortAll} args + * @returns {PartialOptionsGroup} + */ +export function createCohortFolderAll(args) { + const title = formatCohortTitle(args.name); + return { + name: args.name || "all", + tree: [ + createHoldingsSectionAll({ cohort: args, title }), + createValuationSectionFull({ cohort: args, title }), + createPricesSectionFull({ cohort: args, title }), + createCostBasisSectionWithPercentiles({ cohort: args, title }), + createProfitabilitySectionAll({ cohort: args, title }), + createActivitySectionWithAdjusted({ cohort: args, title }), + ], + }; +} + +/** + * Full folder: adjustedSopr + percentiles + RelToMarketCap (term.short only) + * @param {CohortFull | CohortGroupFull} args + * @returns {PartialOptionsGroup} + */ +export function createCohortFolderFull(args) { + if ("list" in args) { + const { list } = args; + const title = formatCohortTitle(args.title); + return { + name: args.name || "all", + tree: [ + createGroupedHoldingsSectionWithRelative({ list, title }), + createGroupedValuationSectionWithOwnMarketCap({ list, title }), + createGroupedPricesSection({ list, title }), + createGroupedCostBasisSectionWithPercentiles({ list, title }), + createGroupedProfitabilitySectionWithNupl({ list, title }), + createGroupedActivitySectionWithAdjusted({ list, title }), + ], + }; + } + const title = formatCohortTitle(args.name); + return { + name: args.name || "all", + tree: [ + createHoldingsSectionWithRelative({ cohort: args, title }), + createValuationSectionFull({ cohort: args, title }), + createPricesSectionFull({ cohort: args, title }), + createCostBasisSectionWithPercentiles({ cohort: args, title }), + createProfitabilitySectionFull({ cohort: args, title }), + createActivitySectionWithAdjusted({ cohort: args, title }), + ], + }; +} + +/** + * Adjusted folder: adjustedSopr only, no percentiles (maxAge.*) + * Has Peak Regret metrics like minAge + * @param {CohortWithAdjusted | CohortGroupWithAdjusted} args + * @returns {PartialOptionsGroup} + */ +export function createCohortFolderWithAdjusted(args) { + if ("list" in args) { + const { list } = args; + const title = formatCohortTitle(args.title); + return { + name: args.name || "all", + tree: [ + createGroupedHoldingsSectionWithRelative({ list, title }), + createGroupedValuationSection({ list, title }), + createGroupedPricesSection({ list, title }), + createGroupedCostBasisSection({ list, title }), + createGroupedProfitabilitySectionWithPeakRegret({ list, title }), + createGroupedActivitySectionWithAdjusted({ list, title }), + ], + }; + } + const title = formatCohortTitle(args.name); + return { + name: args.name || "all", + tree: [ + createHoldingsSectionWithRelative({ cohort: args, title }), + createValuationSection({ cohort: args, title }), + createPricesSectionBasic({ cohort: args, title }), + createCostBasisSection({ cohort: args, title }), + createProfitabilitySectionWithPeakRegret({ cohort: args, title }), + createActivitySectionWithAdjusted({ cohort: args, title }), + ], + }; +} + +/** + * Folder for cohorts with nupl + percentiles (no longer used for term.long which has own folder) + * @param {CohortWithNuplPercentiles | CohortGroupWithNuplPercentiles} args + * @returns {PartialOptionsGroup} + */ +export function createCohortFolderWithNupl(args) { + if ("list" in args) { + const { list } = args; + const title = formatCohortTitle(args.title); + return { + name: args.name || "all", + tree: [ + createGroupedHoldingsSectionWithRelative({ list, title }), + createGroupedValuationSection({ list, title }), + createGroupedPricesSection({ list, title }), + createGroupedCostBasisSectionWithPercentiles({ list, title }), + createGroupedProfitabilitySectionWithNupl({ list, title }), + createGroupedActivitySection({ list, title }), + ], + }; + } + const title = formatCohortTitle(args.name); + return { + name: args.name || "all", + tree: [ + createHoldingsSectionWithRelative({ cohort: args, title }), + createValuationSectionFull({ cohort: args, title }), + createPricesSectionFull({ cohort: args, title }), + createCostBasisSectionWithPercentiles({ cohort: args, title }), + createProfitabilitySectionWithNupl({ cohort: args, title }), + createActivitySection({ cohort: args, title }), + ], + }; +} + +/** + * LongTerm folder: term.long (has own market cap + NUPL + peak regret + P/L ratio) + * @param {CohortLongTerm | CohortGroupLongTerm} args + * @returns {PartialOptionsGroup} + */ +export function createCohortFolderLongTerm(args) { + if ("list" in args) { + const { list } = args; + const title = formatCohortTitle(args.title); + return { + name: args.name || "all", + tree: [ + createGroupedHoldingsSectionWithRelative({ list, title }), + createGroupedValuationSectionWithOwnMarketCap({ list, title }), + createGroupedPricesSection({ list, title }), + createGroupedCostBasisSectionWithPercentiles({ list, title }), + createGroupedProfitabilitySectionLongTerm({ list, title }), + createGroupedActivitySection({ list, title }), + ], + }; + } + const title = formatCohortTitle(args.name); + return { + name: args.name || "all", + tree: [ + createHoldingsSectionWithRelative({ cohort: args, title }), + createValuationSectionFull({ cohort: args, title }), + createPricesSectionFull({ cohort: args, title }), + createCostBasisSectionWithPercentiles({ cohort: args, title }), + createProfitabilitySectionLongTerm({ cohort: args, title }), + createActivitySection({ cohort: args, title }), + ], + }; +} + +/** + * Age range folder: ageRange.* (no nupl via RelativePattern2) + * @param {CohortAgeRange | CohortGroupAgeRange} args + * @returns {PartialOptionsGroup} + */ +export function createCohortFolderAgeRange(args) { + if ("list" in args) { + const { list } = args; + const title = formatCohortTitle(args.title); + return { + name: args.name || "all", + tree: [ + createGroupedHoldingsSectionWithOwnSupply({ list, title }), + createGroupedValuationSectionWithOwnMarketCap({ list, title }), + createGroupedPricesSection({ list, title }), + createGroupedCostBasisSectionWithPercentiles({ list, title }), + createGroupedProfitabilitySectionWithInvestedCapitalPct({ list, title }), + createGroupedActivitySection({ list, title }), + ], + }; + } + const title = formatCohortTitle(args.name); + return { + name: args.name || "all", + tree: [ + createHoldingsSectionWithOwnSupply({ cohort: args, title }), + createValuationSectionFull({ cohort: args, title }), + createPricesSectionFull({ cohort: args, title }), + createCostBasisSectionWithPercentiles({ cohort: args, title }), + createProfitabilitySectionWithInvestedCapitalPct({ cohort: args, title }), + createActivitySection({ cohort: args, title }), + ], + }; +} + +/** + * MinAge folder - has peakRegret in unrealized (minAge.*) + * @param {CohortMinAge | CohortGroupMinAge} args + * @returns {PartialOptionsGroup} + */ +export function createCohortFolderMinAge(args) { + if ("list" in args) { + const { list } = args; + const title = formatCohortTitle(args.title); + return { + name: args.name || "all", + tree: [ + createGroupedHoldingsSectionWithRelative({ list, title }), + createGroupedValuationSection({ list, title }), + createGroupedPricesSection({ list, title }), + createGroupedCostBasisSection({ list, title }), + createGroupedProfitabilitySectionWithPeakRegret({ list, title }), + createGroupedActivitySection({ list, title }), + ], + }; + } + const title = formatCohortTitle(args.name); + return { + name: args.name || "all", + tree: [ + createHoldingsSectionWithRelative({ cohort: args, title }), + createValuationSection({ cohort: args, title }), + createPricesSectionBasic({ cohort: args, title }), + createCostBasisSection({ cohort: args, title }), + createProfitabilitySectionWithPeakRegret({ cohort: args, title }), + createActivitySection({ cohort: args, title }), + ], + }; +} + +/** + * Basic folder WITH RelToMarketCap (geAmount.*, ltAmount.*) + * @param {CohortBasicWithMarketCap | CohortGroupBasicWithMarketCap} args + * @returns {PartialOptionsGroup} + */ +export function createCohortFolderBasicWithMarketCap(args) { + if ("list" in args) { + const { list } = args; + const title = formatCohortTitle(args.title); + return { + name: args.name || "all", + tree: [ + createGroupedHoldingsSectionWithRelative({ list, title }), + createGroupedValuationSection({ list, title }), + createGroupedPricesSection({ list, title }), + createGroupedCostBasisSection({ list, title }), + createGroupedProfitabilitySectionWithNupl({ list, title }), + createGroupedActivitySection({ list, title }), + ], + }; + } + const title = formatCohortTitle(args.name); + return { + name: args.name || "all", + tree: [ + createHoldingsSectionWithRelative({ cohort: args, title }), + createValuationSection({ cohort: args, title }), + createPricesSectionBasic({ cohort: args, title }), + createCostBasisSection({ cohort: args, title }), + createProfitabilitySectionWithNupl({ cohort: args, title }), + createActivitySection({ cohort: args, title }), + ], + }; +} + +/** + * Basic folder WITHOUT RelToMarketCap (epoch.*, amountRange.*, year.*) + * @param {CohortBasicWithoutMarketCap | CohortGroupBasicWithoutMarketCap} args + * @returns {PartialOptionsGroup} + */ +export function createCohortFolderBasicWithoutMarketCap(args) { + if ("list" in args) { + const { list } = args; + const title = formatCohortTitle(args.title); + return { + name: args.name || "all", + tree: [ + createGroupedHoldingsSectionWithOwnSupply({ list, title }), + createGroupedValuationSection({ list, title }), + createGroupedPricesSection({ list, title }), + createGroupedCostBasisSection({ list, title }), + createGroupedProfitabilitySectionBasicWithInvestedCapitalPct({ + list, + title, + }), + createGroupedActivitySection({ list, title }), + ], + }; + } + const title = formatCohortTitle(args.name); + return { + name: args.name || "all", + tree: [ + createHoldingsSectionWithOwnSupply({ cohort: args, title }), + createValuationSection({ cohort: args, title }), + createPricesSectionBasic({ cohort: args, title }), + createCostBasisSection({ cohort: args, title }), + createProfitabilitySectionBasicWithInvestedCapitalPct({ + cohort: args, + title, + }), + createActivitySection({ cohort: args, title }), + ], + }; +} + +/** + * Address folder: like basic but with address count (addressable type cohorts) + * Has invested capital percentage metrics + * @param {CohortAddress | CohortGroupAddress} args + * @returns {PartialOptionsGroup} + */ +export function createCohortFolderAddress(args) { + if ("list" in args) { + const { list } = args; + const title = formatCohortTitle(args.title); + return { + name: args.name || "all", + tree: [ + createGroupedHoldingsSectionAddress({ list, title }), + createGroupedValuationSection({ list, title }), + createGroupedPricesSection({ list, title }), + createGroupedCostBasisSection({ list, title }), + createGroupedProfitabilitySectionBasicWithInvestedCapitalPct({ + list, + title, + }), + createGroupedActivitySection({ list, title }), + ], + }; + } + const title = formatCohortTitle(args.name); + return { + name: args.name || "all", + tree: [ + createHoldingsSectionAddress({ cohort: args, title }), + createValuationSection({ cohort: args, title }), + createPricesSectionBasic({ cohort: args, title }), + createCostBasisSection({ cohort: args, title }), + createProfitabilitySectionBasicWithInvestedCapitalPct({ + cohort: args, + title, + }), + createActivitySection({ cohort: args, title }), + ], + }; +} + +/** + * Folder for cohorts WITHOUT relative section (edge case types: empty, p2ms, unknown) + * @param {CohortWithoutRelative | CohortGroupWithoutRelative} args + * @returns {PartialOptionsGroup} + */ +export function createCohortFolderWithoutRelative(args) { + if ("list" in args) { + const { list } = args; + const title = formatCohortTitle(args.title); + return { + name: args.name || "all", + tree: [ + createGroupedHoldingsSection({ list, title }), + createGroupedValuationSection({ list, title }), + createGroupedPricesSection({ list, title }), + createGroupedCostBasisSection({ list, title }), + createGroupedProfitabilitySection({ list, title }), + createGroupedActivitySection({ list, title }), + ], + }; + } + const title = formatCohortTitle(args.name); + return { + name: args.name || "all", + tree: [ + createHoldingsSection({ cohort: args, title }), + createValuationSection({ cohort: args, title }), + createPricesSectionBasic({ cohort: args, title }), + createCostBasisSection({ cohort: args, title }), + createProfitabilitySection({ cohort: args, title }), + createActivitySection({ cohort: args, title }), + ], + }; +} + +/** + * Address amount cohort folder - for address balance cohorts (has NUPL + addrCount) + * @param {AddressCohortObject | AddressCohortGroupObject} args + * @returns {PartialOptionsGroup} + */ +export function createAddressCohortFolder(args) { + if ("list" in args) { + const { list } = args; + const title = formatCohortTitle(args.title); + return { + name: args.name || "all", + tree: [ + createGroupedHoldingsSectionAddressAmount({ list, title }), + createGroupedValuationSection({ list, title }), + createGroupedPricesSection({ list, title }), + createGroupedCostBasisSection({ list, title }), + createGroupedProfitabilitySectionWithNupl({ list, title }), + createGroupedActivitySection({ list, title }), + ], + }; + } + const title = formatCohortTitle(args.name); + return { + name: args.name || "all", + tree: [ + createHoldingsSectionAddressAmount({ cohort: args, title }), + createValuationSection({ cohort: args, title }), + createPricesSectionBasic({ cohort: args, title }), + createCostBasisSection({ cohort: args, title }), + createProfitabilitySectionWithNupl({ cohort: args, title }), + createActivitySection({ cohort: args, title }), + ], + }; +} diff --git a/website/scripts/options/distribution/profitability.js b/website/scripts/options/distribution/profitability.js new file mode 100644 index 000000000..02d8d5ac7 --- /dev/null +++ b/website/scripts/options/distribution/profitability.js @@ -0,0 +1,2788 @@ +/** + * Profitability section builders + * + * Structure: + * Profitability + * Unrealized (Paper Gains/Losses) + * - P&L: Profit, Loss, Net, Total + * - NUPL: Net Unrealized Profit/Loss Ratio (for cohorts with nupl) + * Realized (Locked-In Gains/Losses) + * - Sum: P&L + Net (USD, % of R.Cap) + * - Cumulative: P&L + Net + 30d Change (USD, % of R.Cap, % of M.Cap) + * Volume (Sent in Profit/Loss) + * - Sum + Cumulative + * Invested Capital: In Profit + In Loss (USD, % of R.Cap) + * Peak Regret (Opportunity Cost vs ATH) + * Sentiment (Market Sentiment - Greed, Pain, Net) + */ + +import { Unit } from "../../utils/units.js"; +import { line, baseline, dots, dotsBaseline } from "../series.js"; +import { colors } from "../../utils/colors.js"; +import { priceLine, priceLines } from "../constants.js"; +import { satsBtcUsd, satsBtcUsdFrom } from "../shared.js"; + +// ============================================================================ +// Single Cohort Helpers +// ============================================================================ + +/** + * Create unrealized P&L series (USD only - for cohorts without relative data) + * @param {{ unrealized: UnrealizedPattern }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createUnrealizedPnlSeries(tree) { + return [ + line({ + metric: tree.unrealized.unrealizedProfit, + name: "Profit", + color: colors.profit, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.unrealizedLoss, + name: "Loss", + color: colors.loss, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.totalUnrealizedPnl, + name: "Total", + color: colors.default, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.negUnrealizedLoss, + name: "Negative Loss", + color: colors.loss, + unit: Unit.usd, + defaultActive: false, + }), + priceLine({ + unit: Unit.usd, + defaultActive: false, + }), + ]; +} + +/** + * Create unrealized P&L series with % of Own Market Cap (for ageRange cohorts) + * @param {{ unrealized: UnrealizedPattern, relative: RelativeWithOwnMarketCap }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createUnrealizedPnlSeriesWithOwnMarketCap(tree) { + return [ + // USD + line({ + metric: tree.unrealized.unrealizedProfit, + name: "Profit", + color: colors.profit, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.unrealizedLoss, + name: "Loss", + color: colors.loss, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.totalUnrealizedPnl, + name: "Total", + color: colors.default, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.negUnrealizedLoss, + name: "Negative Loss", + color: colors.loss, + unit: Unit.usd, + defaultActive: false, + }), + priceLine({ + unit: Unit.usd, + defaultActive: false, + }), + // % of Own Market Cap + line({ + metric: tree.relative.unrealizedProfitRelToOwnMarketCap, + name: "Profit", + color: colors.profit, + unit: Unit.pctOwnMcap, + }), + line({ + metric: tree.relative.unrealizedLossRelToOwnMarketCap, + name: "Loss", + color: colors.loss, + unit: Unit.pctOwnMcap, + }), + line({ + metric: tree.relative.negUnrealizedLossRelToOwnMarketCap, + name: "Negative Loss", + color: colors.loss, + unit: Unit.pctOwnMcap, + defaultActive: false, + }), + priceLine({ + unit: Unit.pctOwnMcap, + defaultActive: false, + }), + // % of Own P&L + line({ + metric: tree.relative.unrealizedProfitRelToOwnTotalUnrealizedPnl, + name: "Profit", + color: colors.profit, + unit: Unit.pctOwnPnl, + }), + line({ + metric: tree.relative.unrealizedLossRelToOwnTotalUnrealizedPnl, + name: "Loss", + color: colors.loss, + unit: Unit.pctOwnPnl, + }), + line({ + metric: tree.relative.negUnrealizedLossRelToOwnTotalUnrealizedPnl, + name: "Negative Loss", + color: colors.loss, + unit: Unit.pctOwnPnl, + defaultActive: false, + }), + ...priceLines({ + numbers: [100, 50, 0], + unit: Unit.pctOwnPnl, + }), + ]; +} + +/** + * Create net unrealized P&L series with % of Own Market Cap (for ageRange cohorts) + * @param {{ unrealized: UnrealizedPattern, relative: RelativeWithOwnMarketCap }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createNetUnrealizedPnlSeriesWithOwnMarketCap(tree) { + return [ + baseline({ + metric: tree.unrealized.netUnrealizedPnl, + name: "Net P&L", + unit: Unit.usd, + }), + // % of Own Market Cap + baseline({ + metric: tree.relative.netUnrealizedPnlRelToOwnMarketCap, + name: "Net P&L", + unit: Unit.pctOwnMcap, + }), + // % of Own P&L + baseline({ + metric: tree.relative.netUnrealizedPnlRelToOwnTotalUnrealizedPnl, + name: "Net P&L", + unit: Unit.pctOwnPnl, + }), + ]; +} + +/** + * Create unrealized P&L series with % of Market Cap (USD + % of M.Cap) + * @param {{ unrealized: UnrealizedPattern, relative: RelativeWithNupl }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createUnrealizedPnlSeriesWithMarketCap(tree) { + return [ + // USD + line({ + metric: tree.unrealized.unrealizedProfit, + name: "Profit", + color: colors.profit, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.unrealizedLoss, + name: "Loss", + color: colors.loss, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.totalUnrealizedPnl, + name: "Total", + color: colors.default, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.negUnrealizedLoss, + name: "Negative Loss", + color: colors.loss, + unit: Unit.usd, + defaultActive: false, + }), + priceLine({ + unit: Unit.usd, + defaultActive: false, + }), + // % of Market Cap + line({ + metric: tree.relative.unrealizedProfitRelToMarketCap, + name: "Profit", + color: colors.profit, + unit: Unit.pctMcap, + }), + line({ + metric: tree.relative.unrealizedLossRelToMarketCap, + name: "Loss", + color: colors.loss, + unit: Unit.pctMcap, + }), + line({ + metric: tree.relative.negUnrealizedLossRelToMarketCap, + name: "Negative Loss", + color: colors.loss, + unit: Unit.pctMcap, + defaultActive: false, + }), + priceLine({ + unit: Unit.pctMcap, + defaultActive: false, + }), + ]; +} + +/** + * Create unrealized P&L series for "all" cohort (USD + % of M.Cap + % of Own P&L) + * @param {{ unrealized: UnrealizedPattern, relative: AllRelativePattern }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createUnrealizedPnlSeriesAll(tree) { + return [ + // USD + line({ + metric: tree.unrealized.unrealizedProfit, + name: "Profit", + color: colors.profit, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.unrealizedLoss, + name: "Loss", + color: colors.loss, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.totalUnrealizedPnl, + name: "Total", + color: colors.default, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.negUnrealizedLoss, + name: "Negative Loss", + color: colors.loss, + unit: Unit.usd, + defaultActive: false, + }), + priceLine({ + unit: Unit.usd, + defaultActive: false, + }), + // % of Market Cap + line({ + metric: tree.relative.unrealizedProfitRelToMarketCap, + name: "Profit", + color: colors.profit, + unit: Unit.pctMcap, + }), + line({ + metric: tree.relative.unrealizedLossRelToMarketCap, + name: "Loss", + color: colors.loss, + unit: Unit.pctMcap, + }), + line({ + metric: tree.relative.negUnrealizedLossRelToMarketCap, + name: "Negative Loss", + color: colors.loss, + unit: Unit.pctMcap, + defaultActive: false, + }), + priceLine({ + unit: Unit.pctMcap, + defaultActive: false, + }), + // % of Own P&L + line({ + metric: tree.relative.unrealizedProfitRelToOwnTotalUnrealizedPnl, + name: "Profit", + color: colors.profit, + unit: Unit.pctOwnPnl, + }), + line({ + metric: tree.relative.unrealizedLossRelToOwnTotalUnrealizedPnl, + name: "Loss", + color: colors.loss, + unit: Unit.pctOwnPnl, + }), + line({ + metric: tree.relative.negUnrealizedLossRelToOwnTotalUnrealizedPnl, + name: "Negative Loss", + color: colors.loss, + unit: Unit.pctOwnPnl, + defaultActive: false, + }), + ...priceLines({ + numbers: [100, 50, 0], + unit: Unit.pctOwnPnl, + }), + ]; +} + +/** + * Create net unrealized P&L series for "all" cohort (USD + % of M.Cap + % of Own P&L) + * @param {{ unrealized: UnrealizedPattern, relative: AllRelativePattern }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createNetUnrealizedPnlSeriesAll(tree) { + return [ + baseline({ + metric: tree.unrealized.netUnrealizedPnl, + name: "Net P&L", + unit: Unit.usd, + }), + // % of Market Cap + baseline({ + metric: tree.relative.netUnrealizedPnlRelToMarketCap, + name: "Net P&L", + unit: Unit.pctMcap, + }), + // % of Own P&L + baseline({ + metric: tree.relative.netUnrealizedPnlRelToOwnTotalUnrealizedPnl, + name: "Net P&L", + unit: Unit.pctOwnPnl, + }), + ]; +} + +/** + * Create net unrealized P&L series with % of Market Cap (USD + % of M.Cap) + * @param {{ unrealized: UnrealizedPattern, relative: RelativeWithNupl }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createNetUnrealizedPnlSeriesWithMarketCap(tree) { + return [ + baseline({ + metric: tree.unrealized.netUnrealizedPnl, + name: "Net P&L", + unit: Unit.usd, + }), + // % of Market Cap + baseline({ + metric: tree.relative.netUnrealizedPnlRelToMarketCap, + name: "Net P&L", + unit: Unit.pctMcap, + }), + ]; +} + +/** + * Create net unrealized P&L series + * @param {{ unrealized: UnrealizedPattern }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createNetUnrealizedPnlSeries(tree) { + return [ + baseline({ + metric: tree.unrealized.netUnrealizedPnl, + name: "Net P&L", + unit: Unit.usd, + }), + ]; +} + +/** + * Create NUPL series + * @param {RelativeWithNupl} relative + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createNuplSeries(relative) { + return [ + baseline({ + metric: relative.nupl, + name: "NUPL", + unit: Unit.ratio, + }), + ]; +} + +/** + * Create invested capital series (absolute only - for cohorts without relative data) + * @param {{ unrealized: UnrealizedPattern }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createInvestedCapitalAbsoluteSeries(tree) { + return [ + line({ + metric: tree.unrealized.investedCapitalInProfit, + name: "In Profit", + color: colors.profit, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.investedCapitalInLoss, + name: "In Loss", + color: colors.loss, + unit: Unit.usd, + }), + ]; +} + +/** + * Create invested capital series (USD + % of R.Cap) + * @param {{ unrealized: UnrealizedPattern, relative: RelativeWithInvestedCapitalPct }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createInvestedCapitalSeries(tree) { + return [ + // USD + line({ + metric: tree.unrealized.investedCapitalInProfit, + name: "In Profit", + color: colors.profit, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.investedCapitalInLoss, + name: "In Loss", + color: colors.loss, + unit: Unit.usd, + }), + // % of Own R.Cap + baseline({ + metric: tree.relative.investedCapitalInProfitPct, + name: "In Profit", + color: colors.profit, + unit: Unit.pctOwnRcap, + }), + baseline({ + metric: tree.relative.investedCapitalInLossPct, + name: "In Loss", + color: colors.loss, + unit: Unit.pctOwnRcap, + }), + ...priceLines({ + numbers: [100, 50], + unit: Unit.pctOwnRcap, + }), + ]; +} + +/** + * Create peak regret series + * @param {{ unrealized: UnrealizedFullPattern }} tree + * @param {Color} color + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createPeakRegretSeries(tree, color) { + return [ + line({ + metric: tree.unrealized.peakRegret, + name: "Peak Regret", + color, + unit: Unit.usd, + }), + ]; +} + +/** + * Create peak regret series with RelToMarketCap + * @param {{ unrealized: UnrealizedFullPattern, relative: RelativeWithPeakRegret }} tree + * @param {Color} color + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createPeakRegretSeriesWithMarketCap(tree, color) { + return [ + line({ + metric: tree.unrealized.peakRegret, + name: "Peak Regret", + color, + unit: Unit.usd, + }), + baseline({ + metric: tree.relative.unrealizedPeakRegretRelToMarketCap, + name: "Rel. to Market Cap", + color, + unit: Unit.pctMcap, + }), + ]; +} + +/** + * Create sentiment series for single cohort + * @param {{ unrealized: UnrealizedPattern }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createSentimentSeries(tree) { + return [ + baseline({ + metric: tree.unrealized.netSentiment, + name: "Net Sentiment", + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.greedIndex, + name: "Greed Index", + color: colors.profit, + unit: Unit.usd, + defaultActive: false, + }), + line({ + metric: tree.unrealized.painIndex, + name: "Pain Index", + color: colors.loss, + unit: Unit.usd, + defaultActive: false, + }), + ]; +} + +// ============================================================================ +// Realized P&L Helpers +// ============================================================================ + +/** + * Create realized P&L sum series (Profit, Loss only - no Net) + * @param {{ realized: AnyRealizedPattern }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createRealizedPnlSumSeries(tree) { + return [ + line({ + metric: tree.realized.realizedProfit7dEma, + name: "Profit 7d EMA", + color: colors.profit, + unit: Unit.usd, + }), + line({ + metric: tree.realized.realizedLoss7dEma, + name: "Loss 7d EMA", + color: colors.loss, + unit: Unit.usd, + }), + dots({ + metric: tree.realized.realizedProfit.sum, + name: "Profit", + color: colors.profit, + unit: Unit.usd, + defaultActive: false, + }), + dots({ + metric: tree.realized.negRealizedLoss.sum, + name: "Negative Loss", + color: colors.loss, + unit: Unit.usd, + defaultActive: false, + }), + dots({ + metric: tree.realized.realizedLoss.sum, + name: "Loss", + color: colors.loss, + unit: Unit.usd, + defaultActive: false, + }), + dots({ + metric: tree.realized.realizedValue, + name: "Value", + color: colors.default, + unit: Unit.usd, + defaultActive: false, + }), + // % of R.Cap + baseline({ + metric: tree.realized.realizedProfitRelToRealizedCap.sum, + name: "Profit", + color: colors.profit, + unit: Unit.pctRcap, + }), + baseline({ + metric: tree.realized.realizedLossRelToRealizedCap.sum, + name: "Loss", + color: colors.loss, + unit: Unit.pctRcap, + }), + ]; +} + +/** + * Create realized Net P&L sum series (baseline) + * @param {{ realized: AnyRealizedPattern }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createRealizedNetPnlSumSeries(tree) { + return [ + baseline({ + metric: tree.realized.netRealizedPnl7dEma, + name: "Net 7d EMA", + unit: Unit.usd, + }), + dotsBaseline({ + metric: tree.realized.netRealizedPnl.sum, + name: "Net", + unit: Unit.usd, + defaultActive: false, + }), + // % of R.Cap + baseline({ + metric: tree.realized.netRealizedPnlRelToRealizedCap.sum, + name: "Net", + unit: Unit.pctRcap, + }), + ]; +} + +/** + * Create realized P&L cumulative series (Profit, Loss only - no Net) + * @param {{ realized: AnyRealizedPattern }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createRealizedPnlCumulativeSeries(tree) { + return [ + line({ + metric: tree.realized.realizedProfit.cumulative, + name: "Profit", + color: colors.profit, + unit: Unit.usd, + }), + line({ + metric: tree.realized.realizedLoss.cumulative, + name: "Loss", + color: colors.loss, + unit: Unit.usd, + }), + line({ + metric: tree.realized.negRealizedLoss.cumulative, + name: "Negative Loss", + color: colors.loss, + unit: Unit.usd, + defaultActive: false, + }), + // % of R.Cap + baseline({ + metric: tree.realized.realizedProfitRelToRealizedCap.cumulative, + name: "Profit", + color: colors.profit, + unit: Unit.pctRcap, + }), + baseline({ + metric: tree.realized.realizedLossRelToRealizedCap.cumulative, + name: "Loss", + color: colors.loss, + unit: Unit.pctRcap, + }), + ]; +} + +/** + * Create realized Net P&L cumulative series (baseline) + * @param {{ realized: AnyRealizedPattern }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createRealizedNetPnlCumulativeSeries(tree) { + return [ + baseline({ + metric: tree.realized.netRealizedPnl.cumulative, + name: "Net", + unit: Unit.usd, + }), + // % of R.Cap + baseline({ + metric: tree.realized.netRealizedPnlRelToRealizedCap.cumulative, + name: "Net", + unit: Unit.pctRcap, + }), + ]; +} + +/** + * Create realized 30d change series + * @param {{ realized: AnyRealizedPattern }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createRealized30dChangeSeries(tree) { + return [ + baseline({ + metric: tree.realized.netRealizedPnlCumulative30dDelta, + name: "30d Change", + unit: Unit.usd, + }), + // % of M.Cap + baseline({ + metric: tree.realized.netRealizedPnlCumulative30dDeltaRelToMarketCap, + name: "30d Change", + unit: Unit.pctMcap, + }), + // % of R.Cap + baseline({ + metric: tree.realized.netRealizedPnlCumulative30dDeltaRelToRealizedCap, + name: "30d Change", + unit: Unit.pctRcap, + }), + ]; +} + +/** + * Create sent in profit/loss tree for single cohort + * @param {{ realized: AnyRealizedPattern }} tree + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function createSentInPnlTree(tree, title) { + return [ + { + name: "Sum", + title: title("Sent In Profit & Loss"), + bottom: [ + ...satsBtcUsd({ + pattern: tree.realized.sentInProfit14dEma, + name: "In Profit 14d EMA", + color: colors.profit, + defaultActive: false, + }), + ...satsBtcUsd({ + pattern: tree.realized.sentInLoss14dEma, + name: "In Loss 14d EMA", + color: colors.loss, + defaultActive: false, + }), + ...satsBtcUsdFrom({ + source: tree.realized.sentInProfit, + key: "sum", + name: "In Profit", + color: colors.profit, + }), + ...satsBtcUsdFrom({ + source: tree.realized.sentInLoss, + key: "sum", + name: "In Loss", + color: colors.loss, + }), + ], + }, + { + name: "Cumulative", + title: title("Cumulative Sent In Profit & Loss"), + bottom: [ + ...satsBtcUsdFrom({ + source: tree.realized.sentInProfit, + key: "cumulative", + name: "In Profit", + color: colors.profit, + }), + ...satsBtcUsdFrom({ + source: tree.realized.sentInLoss, + key: "cumulative", + name: "In Loss", + color: colors.loss, + }), + ], + }, + ]; +} + +/** + * Create realized subfolder for single cohort + * @param {{ realized: AnyRealizedPattern }} tree + * @param {(metric: string) => string} title + * @returns {PartialOptionsGroup} + */ +function createRealizedSubfolder(tree, title) { + return { + name: "Realized", + tree: [ + { + name: "P&L", + title: title("Realized P&L"), + bottom: createRealizedPnlSumSeries(tree), + }, + { + name: "Net", + title: title("Net Realized P&L"), + bottom: createRealizedNetPnlSumSeries(tree), + }, + { + name: "30d Change", + title: title("Realized P&L 30d Change"), + bottom: createRealized30dChangeSeries(tree), + }, + { + name: "Total", + title: title("Total Realized P&L"), + bottom: [ + line({ + metric: tree.realized.totalRealizedPnl, + name: "Total", + unit: Unit.usd, + color: colors.bitcoin, + }), + ], + }, + { + name: "Peak Regret", + title: title("Realized Peak Regret"), + bottom: [ + line({ + metric: tree.realized.peakRegret.sum, + name: "Peak Regret", + unit: Unit.usd, + }), + ], + }, + { + name: "Cumulative", + tree: [ + { + name: "P&L", + title: title("Cumulative Realized P&L"), + bottom: createRealizedPnlCumulativeSeries(tree), + }, + { + name: "Net", + title: title("Cumulative Net Realized P&L"), + bottom: createRealizedNetPnlCumulativeSeries(tree), + }, + { + name: "Peak Regret", + title: title("Cumulative Realized Peak Regret"), + bottom: [ + line({ + metric: tree.realized.peakRegret.cumulative, + name: "Peak Regret", + unit: Unit.usd, + }), + // % of R.Cap + line({ + metric: tree.realized.peakRegretRelToRealizedCap, + name: "Peak Regret", + unit: Unit.pctRcap, + }), + ], + }, + ], + }, + ], + }; +} + +/** + * Create realized subfolder for cohorts with RealizedWithExtras (has P/L Ratio) + * @param {{ realized: RealizedWithExtras }} tree + * @param {(metric: string) => string} title + * @returns {PartialOptionsGroup} + */ +function createRealizedSubfolderWithExtras(tree, title) { + return { + name: "Realized", + tree: [ + { + name: "P&L", + title: title("Realized P&L"), + bottom: createRealizedPnlSumSeries(tree), + }, + { + name: "Net", + title: title("Net Realized P&L"), + bottom: createRealizedNetPnlSumSeries(tree), + }, + { + name: "30d Change", + title: title("Realized P&L 30d Change"), + bottom: createRealized30dChangeSeries(tree), + }, + { + name: "Total", + title: title("Total Realized P&L"), + bottom: [ + line({ + metric: tree.realized.totalRealizedPnl, + name: "Total", + unit: Unit.usd, + color: colors.bitcoin, + }), + ], + }, + { + name: "P/L Ratio", + title: title("Realized Profit/Loss Ratio"), + bottom: [ + baseline({ + metric: tree.realized.realizedProfitToLossRatio, + name: "P/L Ratio", + unit: Unit.ratio, + }), + ], + }, + { + name: "Peak Regret", + title: title("Realized Peak Regret"), + bottom: [ + line({ + metric: tree.realized.peakRegret.sum, + name: "Peak Regret", + unit: Unit.usd, + }), + ], + }, + { + name: "Cumulative", + tree: [ + { + name: "P&L", + title: title("Cumulative Realized P&L"), + bottom: createRealizedPnlCumulativeSeries(tree), + }, + { + name: "Net", + title: title("Cumulative Net Realized P&L"), + bottom: createRealizedNetPnlCumulativeSeries(tree), + }, + { + name: "Peak Regret", + title: title("Cumulative Realized Peak Regret"), + bottom: [ + line({ + metric: tree.realized.peakRegret.cumulative, + name: "Peak Regret", + unit: Unit.usd, + }), + // % of R.Cap + line({ + metric: tree.realized.peakRegretRelToRealizedCap, + name: "Peak Regret", + unit: Unit.pctRcap, + }), + ], + }, + ], + }, + ], + }; +} + +/** + * Create volume subfolder for single cohort + * @param {{ realized: AnyRealizedPattern }} tree + * @param {(metric: string) => string} title + * @returns {PartialOptionsGroup} + */ +function createVolumeSubfolder(tree, title) { + return { + name: "Volume", + tree: createSentInPnlTree(tree, title), + }; +} + +// ============================================================================ +// Single Cohort Section Builders +// ============================================================================ + +/** + * Create basic profitability section (all cohorts have these) + * @param {{ cohort: UtxoCohortObject | CohortWithoutRelative, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createProfitabilitySection({ cohort, title }) { + const { tree } = cohort; + return { + name: "Profitability", + tree: [ + { + name: "Unrealized", + tree: [ + { + name: "P&L", + title: title("Unrealized P&L"), + bottom: createUnrealizedPnlSeries(tree), + }, + { + name: "Net P&L", + title: title("Net Unrealized P&L"), + bottom: createNetUnrealizedPnlSeries(tree), + }, + ], + }, + createRealizedSubfolder(tree, title), + createVolumeSubfolder(tree, title), + { + name: "Invested Capital", + tree: [ + { + name: "Absolute", + title: title("Invested Capital In Profit & Loss"), + bottom: createInvestedCapitalAbsoluteSeries(tree), + }, + ], + }, + { + name: "Sentiment", + title: title("Market Sentiment"), + bottom: createSentimentSeries(tree), + }, + ], + }; +} + +/** + * Create profitability section with invested capital pct only (for basic cohorts) + * Has invested capital % but no unrealized P&L relative metrics + * @param {{ cohort: CohortBasicWithoutMarketCap, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createProfitabilitySectionBasicWithInvestedCapitalPct({ + cohort, + title, +}) { + const { tree } = cohort; + return { + name: "Profitability", + tree: [ + { + name: "Unrealized", + tree: [ + { + name: "P&L", + title: title("Unrealized P&L"), + bottom: createUnrealizedPnlSeries(tree), + }, + { + name: "Net P&L", + title: title("Net Unrealized P&L"), + bottom: createNetUnrealizedPnlSeries(tree), + }, + ], + }, + createRealizedSubfolder(tree, title), + createVolumeSubfolder(tree, title), + { + name: "Invested Capital", + title: title("Invested Capital In Profit & Loss"), + bottom: createInvestedCapitalSeries(tree), + }, + { + name: "Sentiment", + title: title("Market Sentiment"), + bottom: createSentimentSeries(tree), + }, + ], + }; +} + +/** + * Create profitability section with invested capital pct (for ageRange cohorts) + * Has invested capital % and unrealized P&L % of Own Market Cap + * @param {{ cohort: CohortAgeRange, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createProfitabilitySectionWithInvestedCapitalPct({ + cohort, + title, +}) { + const { tree, color } = cohort; + return { + name: "Profitability", + tree: [ + { + name: "Unrealized", + tree: [ + { + name: "P&L", + title: title("Unrealized P&L"), + bottom: createUnrealizedPnlSeriesWithOwnMarketCap(tree), + }, + { + name: "Net P&L", + title: title("Net Unrealized P&L"), + bottom: createNetUnrealizedPnlSeriesWithOwnMarketCap(tree), + }, + { + name: "Peak Regret", + title: title("Unrealized Peak Regret"), + bottom: createPeakRegretSeries(tree, color), + }, + ], + }, + createRealizedSubfolderWithExtras(tree, title), + createVolumeSubfolder(tree, title), + { + name: "Invested Capital", + title: title("Invested Capital In Profit & Loss"), + bottom: createInvestedCapitalSeries(tree), + }, + { + name: "Sentiment", + title: title("Market Sentiment"), + bottom: createSentimentSeries(tree), + }, + ], + }; +} + +/** + * Create profitability section with NUPL (for cohorts with RelativeWithNupl) + * CohortBasicWithMarketCap + * @param {{ cohort: CohortBasicWithMarketCap, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createProfitabilitySectionWithNupl({ cohort, title }) { + const { tree } = cohort; + return { + name: "Profitability", + tree: [ + { + name: "Unrealized", + tree: [ + { + name: "P&L", + title: title("Unrealized P&L"), + bottom: createUnrealizedPnlSeriesWithMarketCap(tree), + }, + { + name: "Net P&L", + title: title("Net Unrealized P&L"), + bottom: createNetUnrealizedPnlSeriesWithMarketCap(tree), + }, + { + name: "NUPL", + title: title("NUPL"), + bottom: createNuplSeries(tree.relative), + }, + ], + }, + createRealizedSubfolder(tree, title), + createVolumeSubfolder(tree, title), + { + name: "Invested Capital", + title: title("Invested Capital In Profit & Loss"), + bottom: createInvestedCapitalSeries(tree), + }, + { + name: "Sentiment", + title: title("Market Sentiment"), + bottom: createSentimentSeries(tree), + }, + ], + }; +} + +/** + * Create unrealized P&L series for LongTerm cohort (USD + % M.Cap + % Own M.Cap + % Own P&L) + * @param {{ unrealized: UnrealizedFullPattern, relative: RelativeWithOwnMarketCap & RelativeWithNupl }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createUnrealizedPnlSeriesLongTerm(tree) { + return [ + // USD + line({ + metric: tree.unrealized.unrealizedProfit, + name: "Profit", + color: colors.profit, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.unrealizedLoss, + name: "Loss", + color: colors.loss, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.totalUnrealizedPnl, + name: "Total", + color: colors.default, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.negUnrealizedLoss, + name: "Negative Loss", + color: colors.loss, + unit: Unit.usd, + defaultActive: false, + }), + priceLine({ + unit: Unit.usd, + defaultActive: false, + }), + // % of Market Cap (only loss available for LongTerm) + line({ + metric: tree.relative.unrealizedLossRelToMarketCap, + name: "Loss", + color: colors.loss, + unit: Unit.pctMcap, + }), + // % of Own Market Cap + line({ + metric: tree.relative.unrealizedProfitRelToOwnMarketCap, + name: "Profit", + color: colors.profit, + unit: Unit.pctOwnMcap, + }), + line({ + metric: tree.relative.unrealizedLossRelToOwnMarketCap, + name: "Loss", + color: colors.loss, + unit: Unit.pctOwnMcap, + }), + line({ + metric: tree.relative.negUnrealizedLossRelToOwnMarketCap, + name: "Negative Loss", + color: colors.loss, + unit: Unit.pctOwnMcap, + defaultActive: false, + }), + priceLine({ + unit: Unit.pctOwnMcap, + defaultActive: false, + }), + // % of Own P&L + line({ + metric: tree.relative.unrealizedProfitRelToOwnTotalUnrealizedPnl, + name: "Profit", + color: colors.profit, + unit: Unit.pctOwnPnl, + }), + line({ + metric: tree.relative.unrealizedLossRelToOwnTotalUnrealizedPnl, + name: "Loss", + color: colors.loss, + unit: Unit.pctOwnPnl, + }), + line({ + metric: tree.relative.negUnrealizedLossRelToOwnTotalUnrealizedPnl, + name: "Negative Loss", + color: colors.loss, + unit: Unit.pctOwnPnl, + defaultActive: false, + }), + ...priceLines({ + numbers: [100, 50, 0], + unit: Unit.pctOwnPnl, + }), + ]; +} + +/** + * Create net unrealized P&L series for LongTerm cohort (USD + % Own M.Cap + % Own P&L) + * @param {{ unrealized: UnrealizedPattern, relative: RelativeWithOwnMarketCap }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createNetUnrealizedPnlSeriesLongTerm(tree) { + return [ + baseline({ + metric: tree.unrealized.netUnrealizedPnl, + name: "Net P&L", + unit: Unit.usd, + }), + // % of Own Market Cap + baseline({ + metric: tree.relative.netUnrealizedPnlRelToOwnMarketCap, + name: "Net P&L", + unit: Unit.pctOwnMcap, + }), + // % of Own P&L + baseline({ + metric: tree.relative.netUnrealizedPnlRelToOwnTotalUnrealizedPnl, + name: "Net P&L", + unit: Unit.pctOwnPnl, + }), + ]; +} + +/** + * Create profitability section for LongTerm cohort (has own market cap + NUPL + peak regret + P/L ratio) + * @param {{ cohort: CohortLongTerm, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createProfitabilitySectionLongTerm({ cohort, title }) { + const { tree, color } = cohort; + return { + name: "Profitability", + tree: [ + { + name: "Unrealized", + tree: [ + { + name: "P&L", + title: title("Unrealized P&L"), + bottom: createUnrealizedPnlSeriesLongTerm(tree), + }, + { + name: "Net P&L", + title: title("Net Unrealized P&L"), + bottom: createNetUnrealizedPnlSeriesLongTerm(tree), + }, + { + name: "NUPL", + title: title("NUPL"), + bottom: createNuplSeries(tree.relative), + }, + { + name: "Peak Regret", + title: title("Unrealized Peak Regret"), + bottom: createPeakRegretSeriesWithMarketCap(tree, color), + }, + ], + }, + createRealizedSubfolderWithExtras(tree, title), + createVolumeSubfolder(tree, title), + { + name: "Invested Capital", + title: title("Invested Capital In Profit & Loss"), + bottom: createInvestedCapitalSeries(tree), + }, + { + name: "Sentiment", + title: title("Market Sentiment"), + bottom: createSentimentSeries(tree), + }, + ], + }; +} + +/** + * Create unrealized P&L series for Full cohorts (USD + % M.Cap + % Own M.Cap + % Own P&L) + * @param {{ unrealized: UnrealizedFullPattern, relative: FullRelativePattern }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createUnrealizedPnlSeriesFull(tree) { + return [ + // USD + line({ + metric: tree.unrealized.unrealizedProfit, + name: "Profit", + color: colors.profit, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.unrealizedLoss, + name: "Loss", + color: colors.loss, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.totalUnrealizedPnl, + name: "Total", + color: colors.default, + unit: Unit.usd, + }), + line({ + metric: tree.unrealized.negUnrealizedLoss, + name: "Negative Loss", + color: colors.loss, + unit: Unit.usd, + defaultActive: false, + }), + priceLine({ + unit: Unit.usd, + defaultActive: false, + }), + // % of Market Cap + line({ + metric: tree.relative.unrealizedProfitRelToMarketCap, + name: "Profit", + color: colors.profit, + unit: Unit.pctMcap, + }), + line({ + metric: tree.relative.unrealizedLossRelToMarketCap, + name: "Loss", + color: colors.loss, + unit: Unit.pctMcap, + }), + line({ + metric: tree.relative.negUnrealizedLossRelToMarketCap, + name: "Negative Loss", + color: colors.loss, + unit: Unit.pctMcap, + defaultActive: false, + }), + priceLine({ + unit: Unit.pctMcap, + defaultActive: false, + }), + // % of Own Market Cap + line({ + metric: tree.relative.unrealizedProfitRelToOwnMarketCap, + name: "Profit", + color: colors.profit, + unit: Unit.pctOwnMcap, + }), + line({ + metric: tree.relative.unrealizedLossRelToOwnMarketCap, + name: "Loss", + color: colors.loss, + unit: Unit.pctOwnMcap, + }), + line({ + metric: tree.relative.negUnrealizedLossRelToOwnMarketCap, + name: "Negative Loss", + color: colors.loss, + unit: Unit.pctOwnMcap, + defaultActive: false, + }), + priceLine({ + unit: Unit.pctOwnMcap, + defaultActive: false, + }), + // % of Own P&L + line({ + metric: tree.relative.unrealizedProfitRelToOwnTotalUnrealizedPnl, + name: "Profit", + color: colors.profit, + unit: Unit.pctOwnPnl, + }), + line({ + metric: tree.relative.unrealizedLossRelToOwnTotalUnrealizedPnl, + name: "Loss", + color: colors.loss, + unit: Unit.pctOwnPnl, + }), + line({ + metric: tree.relative.negUnrealizedLossRelToOwnTotalUnrealizedPnl, + name: "Negative Loss", + color: colors.loss, + unit: Unit.pctOwnPnl, + defaultActive: false, + }), + ...priceLines({ + numbers: [100, 50, 0], + unit: Unit.pctOwnPnl, + }), + ]; +} + +/** + * Create net unrealized P&L series for Full cohorts (USD + % M.Cap + % Own M.Cap + % Own P&L) + * @param {{ unrealized: UnrealizedPattern, relative: FullRelativePattern }} tree + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function createNetUnrealizedPnlSeriesFull(tree) { + return [ + baseline({ + metric: tree.unrealized.netUnrealizedPnl, + name: "Net P&L", + unit: Unit.usd, + }), + // % of Market Cap + baseline({ + metric: tree.relative.netUnrealizedPnlRelToMarketCap, + name: "Net P&L", + unit: Unit.pctMcap, + }), + // % of Own Market Cap + baseline({ + metric: tree.relative.netUnrealizedPnlRelToOwnMarketCap, + name: "Net P&L", + unit: Unit.pctOwnMcap, + }), + // % of Own P&L + baseline({ + metric: tree.relative.netUnrealizedPnlRelToOwnTotalUnrealizedPnl, + name: "Net P&L", + unit: Unit.pctOwnPnl, + }), + ]; +} + +/** + * Create profitability section for Full cohorts (USD + % M.Cap + % Own M.Cap + % Own P&L + Peak Regret) + * @param {{ cohort: CohortFull, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createProfitabilitySectionFull({ cohort, title }) { + const { tree, color } = cohort; + return { + name: "Profitability", + tree: [ + { + name: "Unrealized", + tree: [ + { + name: "P&L", + title: title("Unrealized P&L"), + bottom: createUnrealizedPnlSeriesFull(tree), + }, + { + name: "Net P&L", + title: title("Net Unrealized P&L"), + bottom: createNetUnrealizedPnlSeriesFull(tree), + }, + { + name: "NUPL", + title: title("NUPL"), + bottom: createNuplSeries(tree.relative), + }, + { + name: "Peak Regret", + title: title("Unrealized Peak Regret"), + bottom: createPeakRegretSeriesWithMarketCap(tree, color), + }, + ], + }, + createRealizedSubfolderWithExtras(tree, title), + createVolumeSubfolder(tree, title), + { + name: "Invested Capital", + title: title("Invested Capital In Profit & Loss"), + bottom: createInvestedCapitalSeries(tree), + }, + { + name: "Sentiment", + title: title("Market Sentiment"), + bottom: createSentimentSeries(tree), + }, + ], + }; +} + +/** + * Create profitability section with NUPL + Peak Regret (CohortAll has special All pattern) + * @param {{ cohort: CohortAll, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createProfitabilitySectionAll({ cohort, title }) { + const { tree, color } = cohort; + return { + name: "Profitability", + tree: [ + { + name: "Unrealized", + tree: [ + { + name: "P&L", + title: title("Unrealized P&L"), + bottom: createUnrealizedPnlSeriesAll(tree), + }, + { + name: "Net P&L", + title: title("Net Unrealized P&L"), + bottom: createNetUnrealizedPnlSeriesAll(tree), + }, + { + name: "NUPL", + title: title("NUPL"), + bottom: createNuplSeries(tree.relative), + }, + { + name: "Peak Regret", + title: title("Unrealized Peak Regret"), + bottom: createPeakRegretSeriesWithMarketCap(tree, color), + }, + ], + }, + createRealizedSubfolderWithExtras(tree, title), + createVolumeSubfolder(tree, title), + { + name: "Invested Capital", + title: title("Invested Capital In Profit & Loss"), + bottom: createInvestedCapitalSeries(tree), + }, + { + name: "Sentiment", + title: title("Market Sentiment"), + bottom: createSentimentSeries(tree), + }, + ], + }; +} + +/** + * Create profitability section with Peak Regret + NUPL (CohortMinAge has GlobalPeakRelativePattern) + * @param {{ cohort: CohortMinAge, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createProfitabilitySectionWithPeakRegret({ cohort, title }) { + const { tree, color } = cohort; + return { + name: "Profitability", + tree: [ + { + name: "Unrealized", + tree: [ + { + name: "P&L", + title: title("Unrealized P&L"), + bottom: createUnrealizedPnlSeriesWithMarketCap(tree), + }, + { + name: "Net P&L", + title: title("Net Unrealized P&L"), + bottom: createNetUnrealizedPnlSeriesWithMarketCap(tree), + }, + { + name: "NUPL", + title: title("NUPL"), + bottom: createNuplSeries(tree.relative), + }, + { + name: "Peak Regret", + title: title("Unrealized Peak Regret"), + bottom: createPeakRegretSeriesWithMarketCap(tree, color), + }, + ], + }, + createRealizedSubfolder(tree, title), + createVolumeSubfolder(tree, title), + { + name: "Invested Capital", + title: title("Invested Capital In Profit & Loss"), + bottom: createInvestedCapitalSeries(tree), + }, + { + name: "Sentiment", + title: title("Market Sentiment"), + bottom: createSentimentSeries(tree), + }, + ], + }; +} + +// ============================================================================ +// Grouped Cohort Helpers +// ============================================================================ + +/** + * Create grouped P&L charts (Profit, Loss, Net P&L) - USD only + * @template {{ color: Color, name: string, tree: { unrealized: UnrealizedPattern } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function groupedPnlCharts(list, title) { + return [ + { + name: "Profit", + title: title("Unrealized Profit"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.unrealized.unrealizedProfit, + name, + color, + unit: Unit.usd, + }), + ), + }, + { + name: "Loss", + title: title("Unrealized Loss"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.unrealized.negUnrealizedLoss, + name, + color, + unit: Unit.usd, + }), + ), + }, + { + name: "Net P&L", + title: title("Net Unrealized P&L"), + bottom: list.map(({ color, name, tree }) => + baseline({ + metric: tree.unrealized.netUnrealizedPnl, + name, + color, + unit: Unit.usd, + }), + ), + }, + ]; +} + +/** + * Create grouped P&L charts with % of Market Cap (Profit, Loss, Net P&L) + * @template {{ color: Color, name: string, tree: { unrealized: UnrealizedPattern, relative: RelativeWithNupl } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function groupedPnlChartsWithMarketCap(list, title) { + return [ + { + name: "Profit", + title: title("Unrealized Profit"), + bottom: [ + // USD + ...list.map(({ color, name, tree }) => + line({ + metric: tree.unrealized.unrealizedProfit, + name, + color, + unit: Unit.usd, + }), + ), + // % of Market Cap + ...list.map(({ color, name, tree }) => + baseline({ + metric: tree.relative.unrealizedProfitRelToMarketCap, + name, + color, + unit: Unit.pctMcap, + }), + ), + ], + }, + { + name: "Loss", + title: title("Unrealized Loss"), + bottom: [ + // USD + ...list.map(({ color, name, tree }) => + line({ + metric: tree.unrealized.negUnrealizedLoss, + name, + color, + unit: Unit.usd, + }), + ), + // % of Market Cap + ...list.map(({ color, name, tree }) => + baseline({ + metric: tree.relative.negUnrealizedLossRelToMarketCap, + name, + color, + unit: Unit.pctMcap, + }), + ), + ], + }, + { + name: "Net P&L", + title: title("Net Unrealized P&L"), + bottom: [ + // USD + ...list.map(({ color, name, tree }) => + baseline({ + metric: tree.unrealized.netUnrealizedPnl, + name, + color, + unit: Unit.usd, + }), + ), + // % of Market Cap + ...list.map(({ color, name, tree }) => + baseline({ + metric: tree.relative.netUnrealizedPnlRelToMarketCap, + name, + color, + unit: Unit.pctMcap, + }), + ), + ], + }, + ]; +} + +/** + * Create grouped P&L charts with % of Own Market Cap (for ageRange cohorts) + * @template {{ color: Color, name: string, tree: { unrealized: UnrealizedPattern, relative: RelativeWithOwnMarketCap } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function groupedPnlChartsWithOwnMarketCap(list, title) { + return [ + { + name: "Profit", + title: title("Unrealized Profit"), + bottom: [ + // USD + ...list.map(({ color, name, tree }) => + line({ + metric: tree.unrealized.unrealizedProfit, + name, + color, + unit: Unit.usd, + }), + ), + // % of Own Market Cap + ...list.map(({ color, name, tree }) => + line({ + metric: tree.relative.unrealizedProfitRelToOwnMarketCap, + name, + color, + unit: Unit.pctOwnMcap, + }), + ), + // % of Own P&L + ...list.map(({ color, name, tree }) => + line({ + metric: tree.relative.unrealizedProfitRelToOwnTotalUnrealizedPnl, + name, + color, + unit: Unit.pctOwnPnl, + }), + ), + ], + }, + { + name: "Loss", + title: title("Unrealized Loss"), + bottom: [ + // USD + ...list.map(({ color, name, tree }) => + line({ + metric: tree.unrealized.negUnrealizedLoss, + name, + color, + unit: Unit.usd, + }), + ), + // % of Own Market Cap + ...list.map(({ color, name, tree }) => + line({ + metric: tree.relative.negUnrealizedLossRelToOwnMarketCap, + name, + color, + unit: Unit.pctOwnMcap, + }), + ), + // % of Own P&L + ...list.map(({ color, name, tree }) => + line({ + metric: tree.relative.negUnrealizedLossRelToOwnTotalUnrealizedPnl, + name, + color, + unit: Unit.pctOwnPnl, + }), + ), + ], + }, + { + name: "Net P&L", + title: title("Net Unrealized P&L"), + bottom: [ + // USD + ...list.map(({ color, name, tree }) => + baseline({ + metric: tree.unrealized.netUnrealizedPnl, + name, + color, + unit: Unit.usd, + }), + ), + // % of Own Market Cap + ...list.map(({ color, name, tree }) => + baseline({ + metric: tree.relative.netUnrealizedPnlRelToOwnMarketCap, + name, + color, + unit: Unit.pctOwnMcap, + }), + ), + // % of Own P&L + ...list.map(({ color, name, tree }) => + baseline({ + metric: tree.relative.netUnrealizedPnlRelToOwnTotalUnrealizedPnl, + name, + color, + unit: Unit.pctOwnPnl, + }), + ), + ], + }, + ]; +} + +/** + * Create grouped invested capital charts (absolute only - for cohorts without relative data) + * @template {{ color: Color, name: string, tree: { unrealized: UnrealizedPattern } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function groupedInvestedCapitalAbsoluteCharts(list, title) { + return [ + { + name: "In Profit", + title: title("Invested Capital In Profit"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.unrealized.investedCapitalInProfit, + name, + color, + unit: Unit.usd, + }), + ), + }, + { + name: "In Loss", + title: title("Invested Capital In Loss"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.unrealized.investedCapitalInLoss, + name, + color, + unit: Unit.usd, + }), + ), + }, + ]; +} + +/** + * Create grouped invested capital charts (USD + % of R.Cap) + * @template {{ color: Color, name: string, tree: { unrealized: UnrealizedPattern, relative: RelativeWithInvestedCapitalPct } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function groupedInvestedCapitalCharts(list, title) { + return [ + { + name: "In Profit", + title: title("Invested Capital In Profit"), + bottom: [ + // USD + ...list.map(({ color, name, tree }) => + line({ + metric: tree.unrealized.investedCapitalInProfit, + name, + color, + unit: Unit.usd, + }), + ), + // % of Own R.Cap + ...list.map(({ color, name, tree }) => + baseline({ + metric: tree.relative.investedCapitalInProfitPct, + name, + color, + unit: Unit.pctOwnRcap, + }), + ), + ...priceLines({ + numbers: [100, 50], + unit: Unit.pctOwnRcap, + }), + ], + }, + { + name: "In Loss", + title: title("Invested Capital In Loss"), + bottom: [ + // USD + ...list.map(({ color, name, tree }) => + line({ + metric: tree.unrealized.investedCapitalInLoss, + name, + color, + unit: Unit.usd, + }), + ), + // % of Own R.Cap + ...list.map(({ color, name, tree }) => + baseline({ + metric: tree.relative.investedCapitalInLossPct, + name, + color, + unit: Unit.pctOwnRcap, + }), + ), + ...priceLines({ + numbers: [100, 50], + unit: Unit.pctOwnRcap, + }), + ], + }, + ]; +} + +/** + * Create grouped realized P&L sum charts + * @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function groupedRealizedPnlSumCharts(list, title) { + return [ + { + name: "Profit", + title: title("Realized Profit"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.realized.realizedProfit.sum, + name, + color, + unit: Unit.usd, + }), + ), + }, + { + name: "Loss", + title: title("Realized Loss"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.realized.negRealizedLoss.sum, + name, + color, + unit: Unit.usd, + }), + ), + }, + { + name: "Total", + title: title("Total Realized P&L"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.realized.totalRealizedPnl, + name, + color, + unit: Unit.usd, + }), + ), + }, + { + name: "Value", + title: title("Realized Value"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.realized.realizedValue, + name, + color, + unit: Unit.usd, + }), + ), + }, + ]; +} + +/** + * Create grouped realized P&L sum charts with extras (has P/L Ratio) + * @template {{ color: Color, name: string, tree: { realized: RealizedWithExtras } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function groupedRealizedPnlSumChartsWithExtras(list, title) { + return [ + ...groupedRealizedPnlSumCharts(list, title), + { + name: "P/L Ratio", + title: title("Realized Profit/Loss Ratio"), + bottom: list.map(({ color, name, tree }) => + baseline({ + metric: tree.realized.realizedProfitToLossRatio, + name, + color, + unit: Unit.ratio, + }), + ), + }, + ]; +} + +/** + * Create grouped realized Net P&L sum chart + * @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialChartOption} + */ +function groupedRealizedNetPnlSumChart(list, title) { + return { + name: "Net", + title: title("Net Realized P&L"), + bottom: list.map(({ color, name, tree }) => + baseline({ + metric: tree.realized.netRealizedPnl.sum, + name, + color, + unit: Unit.usd, + }), + ), + }; +} + +/** + * Create grouped realized P&L cumulative charts (Profit, Loss only) + * @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function groupedRealizedPnlCumulativeCharts(list, title) { + return [ + { + name: "Profit", + title: title("Cumulative Realized Profit"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.realized.realizedProfit.cumulative, + name, + color, + unit: Unit.usd, + }), + ), + }, + { + name: "Loss", + title: title("Cumulative Realized Loss"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.realized.negRealizedLoss.cumulative, + name, + color, + unit: Unit.usd, + }), + ), + }, + ]; +} + +/** + * Create grouped realized Net P&L cumulative chart + * @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialChartOption} + */ +function groupedRealizedNetPnlCumulativeChart(list, title) { + return { + name: "Net", + title: title("Cumulative Net Realized P&L"), + bottom: list.map(({ color, name, tree }) => + baseline({ + metric: tree.realized.netRealizedPnl.cumulative, + name, + color, + unit: Unit.usd, + }), + ), + }; +} + +/** + * Create grouped sent in P/L sum tree + * @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function groupedSentInPnlSumTree(list, title) { + return [ + { + name: "In Profit", + title: title("Sent In Profit"), + bottom: [ + ...list.flatMap(({ color, name, tree }) => + satsBtcUsd({ + pattern: tree.realized.sentInProfit14dEma, + name: `${name} 14d EMA`, + color, + defaultActive: false, + }), + ), + ...list.flatMap(({ color, name, tree }) => + satsBtcUsdFrom({ + source: tree.realized.sentInProfit, + key: "sum", + name, + color, + }), + ), + ], + }, + { + name: "In Loss", + title: title("Sent In Loss"), + bottom: [ + ...list.flatMap(({ color, name, tree }) => + satsBtcUsd({ + pattern: tree.realized.sentInLoss14dEma, + name: `${name} 14d EMA`, + color, + defaultActive: false, + }), + ), + ...list.flatMap(({ color, name, tree }) => + satsBtcUsdFrom({ + source: tree.realized.sentInLoss, + key: "sum", + name, + color, + }), + ), + ], + }, + ]; +} + +/** + * Create grouped sent in P/L cumulative tree + * @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function groupedSentInPnlCumulativeTree(list, title) { + return [ + { + name: "In Profit", + title: title("Cumulative Sent In Profit"), + bottom: list.flatMap(({ color, name, tree }) => + satsBtcUsdFrom({ + source: tree.realized.sentInProfit, + key: "cumulative", + name, + color, + }), + ), + }, + { + name: "In Loss", + title: title("Cumulative Sent In Loss"), + bottom: list.flatMap(({ color, name, tree }) => + satsBtcUsdFrom({ + source: tree.realized.sentInLoss, + key: "cumulative", + name, + color, + }), + ), + }, + ]; +} + +/** + * Create grouped realized subfolder + * @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsGroup} + */ +function groupedRealizedSubfolder(list, title) { + return { + name: "Realized", + tree: [ + { + name: "P&L", + tree: groupedRealizedPnlSumCharts(list, title), + }, + groupedRealizedNetPnlSumChart(list, title), + { + name: "30d Change", + title: title("Realized P&L 30d Change"), + bottom: list.map(({ color, name, tree }) => + baseline({ + metric: tree.realized.netRealizedPnlCumulative30dDelta, + name, + color, + unit: Unit.usd, + }), + ), + }, + { + name: "Cumulative", + tree: [ + { + name: "P&L", + tree: groupedRealizedPnlCumulativeCharts(list, title), + }, + groupedRealizedNetPnlCumulativeChart(list, title), + ], + }, + ], + }; +} + +/** + * Create grouped realized subfolder with extras (has P/L Ratio) + * @template {{ color: Color, name: string, tree: { realized: RealizedWithExtras } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsGroup} + */ +function groupedRealizedSubfolderWithExtras(list, title) { + return { + name: "Realized", + tree: [ + { + name: "P&L", + tree: groupedRealizedPnlSumChartsWithExtras(list, title), + }, + groupedRealizedNetPnlSumChart(list, title), + { + name: "30d Change", + title: title("Realized P&L 30d Change"), + bottom: list.map(({ color, name, tree }) => + baseline({ + metric: tree.realized.netRealizedPnlCumulative30dDelta, + name, + color, + unit: Unit.usd, + }), + ), + }, + { + name: "Cumulative", + tree: [ + { + name: "P&L", + tree: groupedRealizedPnlCumulativeCharts(list, title), + }, + groupedRealizedNetPnlCumulativeChart(list, title), + ], + }, + ], + }; +} + +/** + * Create grouped volume subfolder + * @template {{ color: Color, name: string, tree: { realized: AnyRealizedPattern } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsGroup} + */ +function groupedVolumeSubfolder(list, title) { + return { + name: "Volume", + tree: [ + { + name: "Sum", + tree: groupedSentInPnlSumTree(list, title), + }, + { + name: "Cumulative", + tree: groupedSentInPnlCumulativeTree(list, title), + }, + ], + }; +} + +/** + * Create grouped sentiment folder + * @template {{ color: Color, name: string, tree: { unrealized: UnrealizedPattern } }} T + * @param {readonly T[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsGroup} + */ +function groupedSentimentFolder(list, title) { + return { + name: "Sentiment", + tree: [ + { + name: "Net", + title: title("Net Sentiment"), + bottom: list.map(({ color, name, tree }) => + baseline({ + metric: tree.unrealized.netSentiment, + name, + color, + unit: Unit.usd, + }), + ), + }, + { + name: "Greed", + title: title("Greed Index"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.unrealized.greedIndex, + name, + color, + unit: Unit.usd, + }), + ), + }, + { + name: "Pain", + title: title("Pain Index"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.unrealized.painIndex, + name, + color, + unit: Unit.usd, + }), + ), + }, + ], + }; +} + +// ============================================================================ +// Grouped Cohort Section Builders +// ============================================================================ + +/** + * Create grouped profitability section (basic) + * @template {readonly (UtxoCohortObject | CohortWithoutRelative)[]} T + * @param {{ list: T, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createGroupedProfitabilitySection({ list, title }) { + return { + name: "Profitability", + tree: [ + { + name: "Unrealized", + tree: groupedPnlCharts(list, title), + }, + groupedRealizedSubfolder(list, title), + groupedVolumeSubfolder(list, title), + { + name: "Invested Capital", + tree: groupedInvestedCapitalAbsoluteCharts(list, title), + }, + groupedSentimentFolder(list, title), + ], + }; +} + +/** + * Create grouped profitability section with invested capital pct only (for basic cohorts) + * @param {{ list: readonly CohortBasicWithoutMarketCap[], title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createGroupedProfitabilitySectionBasicWithInvestedCapitalPct({ + list, + title, +}) { + return { + name: "Profitability", + tree: [ + { + name: "Unrealized", + tree: groupedPnlCharts(list, title), + }, + groupedRealizedSubfolder(list, title), + groupedVolumeSubfolder(list, title), + { + name: "Invested Capital", + tree: groupedInvestedCapitalCharts(list, title), + }, + groupedSentimentFolder(list, title), + ], + }; +} + +/** + * Create grouped profitability section with invested capital pct (for ageRange cohorts) + * Has unrealized P&L % of Own Market Cap + * @param {{ list: readonly CohortAgeRange[], title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createGroupedProfitabilitySectionWithInvestedCapitalPct({ + list, + title, +}) { + return { + name: "Profitability", + tree: [ + { + name: "Unrealized", + tree: [ + ...groupedPnlChartsWithOwnMarketCap(list, title), + { + name: "Peak Regret", + title: title("Unrealized Peak Regret"), + bottom: list.map(({ color, name, tree }) => + line({ + metric: tree.unrealized.peakRegret, + name, + color, + unit: Unit.usd, + }), + ), + }, + ], + }, + groupedRealizedSubfolderWithExtras(list, title), + groupedVolumeSubfolder(list, title), + { + name: "Invested Capital", + tree: groupedInvestedCapitalCharts(list, title), + }, + groupedSentimentFolder(list, title), + ], + }; +} + +/** + * Create grouped profitability section with NUPL + * @param {{ list: readonly (CohortFull | CohortBasicWithMarketCap)[], title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createGroupedProfitabilitySectionWithNupl({ list, title }) { + return { + name: "Profitability", + tree: [ + { + name: "Unrealized", + tree: [ + ...groupedPnlChartsWithMarketCap(list, title), + { + name: "NUPL", + title: title("NUPL"), + bottom: list.map(({ color, name, tree }) => + baseline({ + metric: tree.relative.nupl, + name, + color, + unit: Unit.ratio, + }), + ), + }, + ], + }, + groupedRealizedSubfolder(list, title), + groupedVolumeSubfolder(list, title), + { + name: "Invested Capital", + tree: groupedInvestedCapitalCharts(list, title), + }, + groupedSentimentFolder(list, title), + ], + }; +} + +/** + * Create grouped P&L charts with % of Own Market Cap for LongTerm cohorts + * @param {readonly CohortLongTerm[]} list + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function groupedPnlChartsLongTerm(list, title) { + return [ + { + name: "Profit", + title: title("Unrealized Profit"), + bottom: [ + // USD + ...list.map(({ color, name, tree }) => + line({ + metric: tree.unrealized.unrealizedProfit, + name, + color, + unit: Unit.usd, + }), + ), + // % of Own Market Cap + ...list.map(({ color, name, tree }) => + line({ + metric: tree.relative.unrealizedProfitRelToOwnMarketCap, + name, + color, + unit: Unit.pctOwnMcap, + }), + ), + // % of Own P&L + ...list.map(({ color, name, tree }) => + line({ + metric: tree.relative.unrealizedProfitRelToOwnTotalUnrealizedPnl, + name, + color, + unit: Unit.pctOwnPnl, + }), + ), + ], + }, + { + name: "Loss", + title: title("Unrealized Loss"), + bottom: [ + // USD + ...list.map(({ color, name, tree }) => + line({ + metric: tree.unrealized.negUnrealizedLoss, + name, + color, + unit: Unit.usd, + }), + ), + // % of Market Cap + ...list.map(({ color, name, tree }) => + line({ + metric: tree.relative.unrealizedLossRelToMarketCap, + name, + color, + unit: Unit.pctMcap, + }), + ), + // % of Own Market Cap + ...list.map(({ color, name, tree }) => + line({ + metric: tree.relative.negUnrealizedLossRelToOwnMarketCap, + name, + color, + unit: Unit.pctOwnMcap, + }), + ), + // % of Own P&L + ...list.map(({ color, name, tree }) => + line({ + metric: tree.relative.negUnrealizedLossRelToOwnTotalUnrealizedPnl, + name, + color, + unit: Unit.pctOwnPnl, + }), + ), + ], + }, + { + name: "Net P&L", + title: title("Net Unrealized P&L"), + bottom: [ + // USD + ...list.map(({ color, name, tree }) => + baseline({ + metric: tree.unrealized.netUnrealizedPnl, + name, + color, + unit: Unit.usd, + }), + ), + // % of Own Market Cap + ...list.map(({ color, name, tree }) => + baseline({ + metric: tree.relative.netUnrealizedPnlRelToOwnMarketCap, + name, + color, + unit: Unit.pctOwnMcap, + }), + ), + // % of Own P&L + ...list.map(({ color, name, tree }) => + baseline({ + metric: tree.relative.netUnrealizedPnlRelToOwnTotalUnrealizedPnl, + name, + color, + unit: Unit.pctOwnPnl, + }), + ), + ], + }, + ]; +} + +/** + * Create grouped profitability section for LongTerm cohorts (has own market cap + NUPL + peak regret + P/L ratio) + * @param {{ list: readonly CohortLongTerm[], title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createGroupedProfitabilitySectionLongTerm({ list, title }) { + return { + name: "Profitability", + tree: [ + { + name: "Unrealized", + tree: [ + ...groupedPnlChartsLongTerm(list, title), + { + name: "NUPL", + title: title("NUPL"), + bottom: list.map(({ color, name, tree }) => + baseline({ + metric: tree.relative.nupl, + name, + color, + unit: Unit.ratio, + }), + ), + }, + { + name: "Peak Regret", + title: title("Unrealized Peak Regret"), + bottom: [ + // USD + ...list.map(({ color, name, tree }) => + line({ + metric: tree.unrealized.peakRegret, + name, + color, + unit: Unit.usd, + }), + ), + // % of Market Cap + ...list.map(({ color, name, tree }) => + baseline({ + metric: tree.relative.unrealizedPeakRegretRelToMarketCap, + name, + color, + unit: Unit.pctMcap, + }), + ), + ], + }, + ], + }, + groupedRealizedSubfolderWithExtras(list, title), + groupedVolumeSubfolder(list, title), + { + name: "Invested Capital", + tree: groupedInvestedCapitalCharts(list, title), + }, + groupedSentimentFolder(list, title), + ], + }; +} + +/** + * Create grouped profitability section with Peak Regret + NUPL (for minAge cohorts) + * @param {{ list: readonly CohortMinAge[], title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createGroupedProfitabilitySectionWithPeakRegret({ + list, + title, +}) { + return { + name: "Profitability", + tree: [ + { + name: "Unrealized", + tree: [ + ...groupedPnlChartsWithMarketCap(list, title), + { + name: "NUPL", + title: title("NUPL"), + bottom: list.map(({ color, name, tree }) => + baseline({ + metric: tree.relative.nupl, + name, + color, + unit: Unit.ratio, + }), + ), + }, + { + name: "Peak Regret", + title: title("Unrealized Peak Regret"), + bottom: [ + // USD + ...list.map(({ color, name, tree }) => + line({ + metric: tree.unrealized.peakRegret, + name, + color, + unit: Unit.usd, + }), + ), + // % of Market Cap + ...list.map(({ color, name, tree }) => + baseline({ + metric: tree.relative.unrealizedPeakRegretRelToMarketCap, + name, + color, + unit: Unit.pctMcap, + }), + ), + ], + }, + ], + }, + groupedRealizedSubfolder(list, title), + groupedVolumeSubfolder(list, title), + { + name: "Invested Capital", + tree: groupedInvestedCapitalCharts(list, title), + }, + groupedSentimentFolder(list, title), + ], + }; +} diff --git a/website/scripts/options/distribution/shared.js b/website/scripts/options/distribution/shared.js index ffa84eb5f..9bad0aedf 100644 --- a/website/scripts/options/distribution/shared.js +++ b/website/scripts/options/distribution/shared.js @@ -341,13 +341,6 @@ export function createGroupedSupplySection( return { name: "Supply", tree: [ - { - name: "Total", - title: title("Supply"), - bottom: createGroupedSupplyTotalSeries(list, { - relativeMetrics: supplyRelativeMetrics, - }), - }, { name: "30d Change", title: title("Supply 30d Change"), @@ -369,6 +362,13 @@ export function createGroupedSupplySection( relativeMetrics: lossRelativeMetrics, }), }, + { + name: "Total", + title: title("Supply"), + bottom: createGroupedSupplyTotalSeries(list, { + relativeMetrics: supplyRelativeMetrics, + }), + }, ], }; } @@ -988,10 +988,10 @@ export function createSingleSentSeries(cohort) { */ export function createSingleSellSideRiskSeries(tree) { return [ - dots({ - metric: tree.realized.sellSideRiskRatio, - name: "Raw", - color: colors.bitcoin, + line({ + metric: tree.realized.sellSideRiskRatio30dEma, + name: "30d EMA", + color: colors.ma._1m, unit: Unit.ratio, }), line({ @@ -1000,10 +1000,10 @@ export function createSingleSellSideRiskSeries(tree) { color: colors.ma._1w, unit: Unit.ratio, }), - line({ - metric: tree.realized.sellSideRiskRatio30dEma, - name: "30d EMA", - color: colors.ma._1m, + dots({ + metric: tree.realized.sellSideRiskRatio, + name: "Raw", + color: colors.bitcoin, unit: Unit.ratio, }), ]; @@ -1130,7 +1130,7 @@ export function createSingleSoprSeries(tree) { baseline({ metric: tree.realized.sopr7dEma, name: "7d EMA", - color: colors.bi.sopr7d, + color: colors.bi.p2, unit: Unit.ratio, defaultActive: false, base: 1, @@ -1138,7 +1138,7 @@ export function createSingleSoprSeries(tree) { baseline({ metric: tree.realized.sopr30dEma, name: "30d EMA", - color: colors.bi.sopr30d, + color: colors.bi.p2, unit: Unit.ratio, defaultActive: false, base: 1, @@ -1340,12 +1340,14 @@ export function createSingleSentimentSeries(tree) { name: "Greed Index", color: colors.profit, unit: Unit.usd, + defaultActive: false, }), line({ metric: tree.unrealized.painIndex, name: "Pain Index", color: colors.loss, unit: Unit.usd, + defaultActive: false, }), ]; } diff --git a/website/scripts/options/distribution/utxo.js b/website/scripts/options/distribution/utxo.js index fe68bfd2b..0d39489ca 100644 --- a/website/scripts/options/distribution/utxo.js +++ b/website/scripts/options/distribution/utxo.js @@ -24,13 +24,7 @@ */ import { - createSingleSellSideRiskSeries, - createGroupedSellSideRiskSeries, - createSingleValueCreatedDestroyedSeries, - createSingleValueFlowBreakdownSeries, - createSingleCapitulationProfitFlowSeries, createSingleSoprSeries, - createSingleCoinsDestroyedSeries, createSingleRealizedAthRegretSeries, createGroupedRealizedAthRegretSeries, createSingleSentimentSeries, @@ -64,6 +58,20 @@ import { createValuationSectionFull, createGroupedValuationSection, } from "./valuation.js"; +import { + createActivitySection, + createActivitySectionWithAdjusted, + createGroupedActivitySection, + createGroupedActivitySectionWithAdjusted, +} from "./activity.js"; +import { + createProfitabilitySection, + createProfitabilitySectionWithNupl, + createProfitabilitySectionAll, + createProfitabilitySectionWithPeakRegret, + createGroupedProfitabilitySection, + createGroupedProfitabilitySectionWithNupl, +} from "./profitability.js"; import { Unit } from "../../utils/units.js"; import { line, baseline } from "../series.js"; import { priceLine } from "../constants.js"; @@ -84,12 +92,11 @@ export function createCohortFolderAll(args) { name: args.name || "all", tree: [ createHoldingsSectionAll({ cohort: args, title }), - createPricesSectionFull({ cohort: args, title }), createValuationSectionFull({ cohort: args, title }), - createSingleRealizedSectionFull(args, title), - createSingleUnrealizedSectionAll(args, title), + createPricesSectionFull({ cohort: args, title }), createCostBasisSectionWithPercentiles({ cohort: args, title }), - createSingleActivitySectionWithAdjusted(args, title), + createProfitabilitySectionAll({ cohort: args, title }), + createActivitySectionWithAdjusted({ cohort: args, title }), ], }; } @@ -107,14 +114,11 @@ export function createCohortFolderFull(args) { name: args.name || "all", tree: [ createGroupedHoldingsSectionWithRelative({ list, title }), - createGroupedPricesSection({ list, title }), createGroupedValuationSection({ list, title }), - createGroupedRealizedSectionWithAdjusted(list, title, { - ratioMetrics: createGroupedRealizedPnlRatioMetrics, - }), - createGroupedUnrealizedSectionFull(list, title), + createGroupedPricesSection({ list, title }), createGroupedCostBasisSectionWithPercentiles({ list, title }), - createGroupedActivitySectionWithAdjusted(list, title), + createGroupedProfitabilitySectionWithNupl({ list, title }), + createGroupedActivitySectionWithAdjusted({ list, title }), ], }; } @@ -123,12 +127,11 @@ export function createCohortFolderFull(args) { name: args.name || "all", tree: [ createHoldingsSectionWithRelative({ cohort: args, title }), - createPricesSectionFull({ cohort: args, title }), createValuationSectionFull({ cohort: args, title }), - createSingleRealizedSectionFull(args, title), - createSingleUnrealizedSectionFull(args, title), + createPricesSectionFull({ cohort: args, title }), createCostBasisSectionWithPercentiles({ cohort: args, title }), - createSingleActivitySectionWithAdjusted(args, title), + createProfitabilitySectionWithNupl({ cohort: args, title }), + createActivitySectionWithAdjusted({ cohort: args, title }), ], }; } @@ -146,12 +149,11 @@ export function createCohortFolderWithAdjusted(args) { name: args.name || "all", tree: [ createGroupedHoldingsSectionWithRelative({ list, title }), - createGroupedPricesSection({ list, title }), createGroupedValuationSection({ list, title }), - createGroupedRealizedSectionWithAdjusted(list, title), - createGroupedUnrealizedSectionWithMarketCap(list, title), + createGroupedPricesSection({ list, title }), createGroupedCostBasisSection({ list, title }), - createGroupedActivitySectionWithAdjusted(list, title), + createGroupedProfitabilitySectionWithNupl({ list, title }), + createGroupedActivitySectionWithAdjusted({ list, title }), ], }; } @@ -160,12 +162,11 @@ export function createCohortFolderWithAdjusted(args) { name: args.name || "all", tree: [ createHoldingsSectionWithRelative({ cohort: args, title }), - createPricesSectionBasic({ cohort: args, title }), createValuationSection({ cohort: args, title }), - createSingleRealizedSectionWithAdjusted(args, title), - createSingleUnrealizedSectionWithMarketCap(args, title), + createPricesSectionBasic({ cohort: args, title }), createCostBasisSection({ cohort: args, title }), - createSingleActivitySectionWithAdjusted(args, title), + createProfitabilitySectionWithNupl({ cohort: args, title }), + createActivitySectionWithAdjusted({ cohort: args, title }), ], }; } @@ -183,13 +184,10 @@ export function createCohortFolderWithNupl(args) { name: args.name || "all", tree: [ createGroupedHoldingsSectionWithRelative({ list, title }), - createGroupedPricesSection({ list, title }), createGroupedValuationSection({ list, title }), - createGroupedRealizedSectionBasic(list, title, { - ratioMetrics: createGroupedRealizedPnlRatioMetrics, - }), - createGroupedUnrealizedSectionWithNupl({ list, title }), + createGroupedPricesSection({ list, title }), createGroupedCostBasisSectionWithPercentiles({ list, title }), + createGroupedProfitabilitySectionWithNupl({ list, title }), createGroupedActivitySection({ list, title }), ], }; @@ -199,11 +197,10 @@ export function createCohortFolderWithNupl(args) { name: args.name || "all", tree: [ createHoldingsSectionWithRelative({ cohort: args, title }), - createPricesSectionFull({ cohort: args, title }), createValuationSectionFull({ cohort: args, title }), - createSingleRealizedSectionWithPercentiles(args, title), - createSingleUnrealizedSectionWithNupl({ cohort: args, title }), + createPricesSectionFull({ cohort: args, title }), createCostBasisSectionWithPercentiles({ cohort: args, title }), + createProfitabilitySectionWithNupl({ cohort: args, title }), createActivitySection({ cohort: args, title }), ], }; @@ -222,13 +219,10 @@ export function createCohortFolderAgeRange(args) { name: args.name || "all", tree: [ createGroupedHoldingsSection({ list, title }), - createGroupedPricesSection({ list, title }), createGroupedValuationSection({ list, title }), - createGroupedRealizedSectionBasic(list, title, { - ratioMetrics: createGroupedRealizedPnlRatioMetrics, - }), - createGroupedUnrealizedSectionAgeRange(list, title), + createGroupedPricesSection({ list, title }), createGroupedCostBasisSectionWithPercentiles({ list, title }), + createGroupedProfitabilitySection({ list, title }), createGroupedActivitySection({ list, title }), ], }; @@ -238,11 +232,10 @@ export function createCohortFolderAgeRange(args) { name: args.name || "all", tree: [ createHoldingsSection({ cohort: args, title }), - createPricesSectionFull({ cohort: args, title }), createValuationSectionFull({ cohort: args, title }), - createSingleRealizedSectionWithPercentiles(args, title), - createSingleUnrealizedSectionAgeRange(args, title), + createPricesSectionFull({ cohort: args, title }), createCostBasisSectionWithPercentiles({ cohort: args, title }), + createProfitabilitySection({ cohort: args, title }), createActivitySection({ cohort: args, title }), ], }; @@ -261,11 +254,10 @@ export function createCohortFolderMinAge(args) { name: args.name || "all", tree: [ createGroupedHoldingsSectionWithRelative({ list, title }), - createGroupedPricesSection({ list, title }), createGroupedValuationSection({ list, title }), - createGroupedRealizedSectionBasic(list, title), - createGroupedUnrealizedSectionMinAge(list, title), + createGroupedPricesSection({ list, title }), createGroupedCostBasisSection({ list, title }), + createGroupedProfitabilitySection({ list, title }), createGroupedActivitySection({ list, title }), ], }; @@ -275,11 +267,10 @@ export function createCohortFolderMinAge(args) { name: args.name || "all", tree: [ createHoldingsSectionWithRelative({ cohort: args, title }), - createPricesSectionBasic({ cohort: args, title }), createValuationSection({ cohort: args, title }), - createSingleRealizedSectionBasic(args, title), - createSingleUnrealizedSectionMinAge(args, title), + createPricesSectionBasic({ cohort: args, title }), createCostBasisSection({ cohort: args, title }), + createProfitabilitySectionWithPeakRegret({ cohort: args, title }), createActivitySection({ cohort: args, title }), ], }; @@ -298,11 +289,10 @@ export function createCohortFolderBasicWithMarketCap(args) { name: args.name || "all", tree: [ createGroupedHoldingsSectionWithRelative({ list, title }), - createGroupedPricesSection({ list, title }), createGroupedValuationSection({ list, title }), - createGroupedRealizedSectionBasic(list, title), - createGroupedUnrealizedSectionWithMarketCapOnly(list, title), + createGroupedPricesSection({ list, title }), createGroupedCostBasisSection({ list, title }), + createGroupedProfitabilitySectionWithNupl({ list, title }), createGroupedActivitySection({ list, title }), ], }; @@ -312,11 +302,10 @@ export function createCohortFolderBasicWithMarketCap(args) { name: args.name || "all", tree: [ createHoldingsSectionWithRelative({ cohort: args, title }), - createPricesSectionBasic({ cohort: args, title }), createValuationSection({ cohort: args, title }), - createSingleRealizedSectionBasic(args, title), - createSingleUnrealizedSectionWithMarketCapOnly(args, title), + createPricesSectionBasic({ cohort: args, title }), createCostBasisSection({ cohort: args, title }), + createProfitabilitySectionWithNupl({ cohort: args, title }), createActivitySection({ cohort: args, title }), ], }; @@ -335,11 +324,10 @@ export function createCohortFolderBasicWithoutMarketCap(args) { name: args.name || "all", tree: [ createGroupedHoldingsSection({ list, title }), - createGroupedPricesSection({ list, title }), createGroupedValuationSection({ list, title }), - createGroupedRealizedSectionBasic(list, title), - createGroupedUnrealizedSectionBase(list, title), + createGroupedPricesSection({ list, title }), createGroupedCostBasisSection({ list, title }), + createGroupedProfitabilitySection({ list, title }), createGroupedActivitySection({ list, title }), ], }; @@ -349,11 +337,10 @@ export function createCohortFolderBasicWithoutMarketCap(args) { name: args.name || "all", tree: [ createHoldingsSection({ cohort: args, title }), - createPricesSectionBasic({ cohort: args, title }), createValuationSection({ cohort: args, title }), - createSingleRealizedSectionBasic(args, title), - createSingleUnrealizedSectionBase(args, title), + createPricesSectionBasic({ cohort: args, title }), createCostBasisSection({ cohort: args, title }), + createProfitabilitySection({ cohort: args, title }), createActivitySection({ cohort: args, title }), ], }; @@ -373,11 +360,10 @@ export function createCohortFolderAddress(args) { name: args.name || "all", tree: [ createGroupedHoldingsSectionAddress({ list, title }), - createGroupedPricesSection({ list, title }), createGroupedValuationSection({ list, title }), - createGroupedRealizedSectionBasic(list, title), - createGroupedUnrealizedSectionBase(list, title), + createGroupedPricesSection({ list, title }), createGroupedCostBasisSection({ list, title }), + createGroupedProfitabilitySection({ list, title }), createGroupedActivitySection({ list, title }), ], }; @@ -387,11 +373,10 @@ export function createCohortFolderAddress(args) { name: args.name || "all", tree: [ createHoldingsSectionAddress({ cohort: args, title }), - createPricesSectionBasic({ cohort: args, title }), createValuationSection({ cohort: args, title }), - createSingleRealizedSectionBasic(args, title), - createSingleUnrealizedSectionBase(args, title), + createPricesSectionBasic({ cohort: args, title }), createCostBasisSection({ cohort: args, title }), + createProfitabilitySection({ cohort: args, title }), createActivitySection({ cohort: args, title }), ], }; @@ -410,11 +395,10 @@ export function createCohortFolderWithoutRelative(args) { name: args.name || "all", tree: [ createGroupedHoldingsSection({ list, title }), - createGroupedPricesSection({ list, title }), createGroupedValuationSection({ list, title }), - createGroupedRealizedSectionBasic(list, title), - createGroupedUnrealizedSectionWithoutRelative(list, title), + createGroupedPricesSection({ list, title }), createGroupedCostBasisSection({ list, title }), + createGroupedProfitabilitySection({ list, title }), createGroupedActivitySection({ list, title }), ], }; @@ -424,11 +408,10 @@ export function createCohortFolderWithoutRelative(args) { name: args.name || "all", tree: [ createHoldingsSection({ cohort: args, title }), - createPricesSectionBasic({ cohort: args, title }), createValuationSection({ cohort: args, title }), - createSingleRealizedSectionBasic(args, title), - createSingleUnrealizedSectionWithoutRelative(args, title), + createPricesSectionBasic({ cohort: args, title }), createCostBasisSection({ cohort: args, title }), + createProfitabilitySection({ cohort: args, title }), createActivitySection({ cohort: args, title }), ], }; @@ -734,6 +717,7 @@ function createSingleRealizedPnlSection(cohort, title, { extra = [] } = {}) { name: "Sum", title: title("Realized P&L"), bottom: [ + // USD line({ metric: tree.realized.realizedProfit.sum, name: "Profit", @@ -773,12 +757,26 @@ function createSingleRealizedPnlSection(cohort, title, { extra = [] } = {}) { unit: Unit.usd, defaultActive: false, }), + // % of R.Cap + baseline({ + metric: tree.realized.realizedProfitRelToRealizedCap.sum, + name: "Profit", + color: colors.profit, + unit: Unit.pctRcap, + }), + baseline({ + metric: tree.realized.realizedLossRelToRealizedCap.sum, + name: "Loss", + color: colors.loss, + unit: Unit.pctRcap, + }), ], }, { name: "Cumulative", title: title("Realized P&L (Total)"), bottom: [ + // USD line({ metric: tree.realized.realizedProfit.cumulative, name: "Profit", @@ -798,42 +796,23 @@ function createSingleRealizedPnlSection(cohort, title, { extra = [] } = {}) { unit: Unit.usd, defaultActive: false, }), + // % of R.Cap + baseline({ + metric: tree.realized.realizedProfitRelToRealizedCap.cumulative, + name: "Profit", + color: colors.profit, + unit: Unit.pctRcap, + }), + baseline({ + metric: tree.realized.realizedLossRelToRealizedCap.cumulative, + name: "Loss", + color: colors.loss, + unit: Unit.pctRcap, + }), ], }, ], }, - { - name: "P&L Relative", - title: title("Realized P&L"), - bottom: [ - baseline({ - metric: tree.realized.realizedProfitRelToRealizedCap.sum, - name: "Profit", - color: colors.profit, - unit: Unit.pctRcap, - }), - baseline({ - metric: tree.realized.realizedProfitRelToRealizedCap.cumulative, - name: "Profit Cumulative", - color: colors.profit, - unit: Unit.pctRcap, - defaultActive: false, - }), - baseline({ - metric: tree.realized.realizedLossRelToRealizedCap.sum, - name: "Loss", - color: colors.loss, - unit: Unit.pctRcap, - }), - baseline({ - metric: tree.realized.realizedLossRelToRealizedCap.cumulative, - name: "Loss Cumulative", - color: colors.loss, - unit: Unit.pctRcap, - defaultActive: false, - }), - ], - }, { name: "Net P&L", tree: [ @@ -841,6 +820,7 @@ function createSingleRealizedPnlSection(cohort, title, { extra = [] } = {}) { name: "Sum", title: title("Net Realized P&L"), bottom: [ + // USD baseline({ metric: tree.realized.netRealizedPnl.sum, name: "Net", @@ -851,23 +831,19 @@ function createSingleRealizedPnlSection(cohort, title, { extra = [] } = {}) { name: "Net 7d EMA", unit: Unit.usd, }), + // % of R.Cap baseline({ metric: tree.realized.netRealizedPnlRelToRealizedCap.sum, name: "Net", unit: Unit.pctRcap, }), - baseline({ - metric: - tree.realized.netRealizedPnlCumulative30dDeltaRelToMarketCap, - name: "30d Change", - unit: Unit.pctMcap, - }), ], }, { name: "Cumulative", title: title("Net Realized P&L (Total)"), bottom: [ + // USD baseline({ metric: tree.realized.netRealizedPnl.cumulative, name: "Net", @@ -879,6 +855,7 @@ function createSingleRealizedPnlSection(cohort, title, { extra = [] } = {}) { unit: Unit.usd, defaultActive: false, }), + // % of R.Cap baseline({ metric: tree.realized.netRealizedPnlRelToRealizedCap.cumulative, name: "Net", @@ -891,6 +868,13 @@ function createSingleRealizedPnlSection(cohort, title, { extra = [] } = {}) { unit: Unit.pctRcap, defaultActive: false, }), + // % of M.Cap + baseline({ + metric: + tree.realized.netRealizedPnlCumulative30dDeltaRelToMarketCap, + name: "30d Change", + unit: Unit.pctMcap, + }), ], }, ], @@ -1330,14 +1314,14 @@ function createSingleAdjustedSoprChart(cohort, title) { baseline({ metric: tree.realized.adjustedSopr, name: "Adjusted", - color: colors.bi.adjustedSopr, + color: colors.bi.p1, unit: Unit.ratio, base: 1, }), baseline({ metric: tree.realized.adjustedSopr7dEma, name: "Adj. 7d EMA", - color: colors.bi.adjustedSopr7d, + color: colors.bi.p2, unit: Unit.ratio, defaultActive: false, base: 1, @@ -1345,7 +1329,7 @@ function createSingleAdjustedSoprChart(cohort, title) { baseline({ metric: tree.realized.adjustedSopr30dEma, name: "Adj. 30d EMA", - color: colors.bi.adjustedSopr30d, + color: colors.bi.p3, unit: Unit.ratio, defaultActive: false, base: 1, @@ -1639,13 +1623,13 @@ function createInvestedCapitalRelMetrics(rel) { metric: rel.investedCapitalInProfitPct, name: "In Profit", color: colors.profit, - unit: Unit.pctRcap, + unit: Unit.pctOwnRcap, }), baseline({ metric: rel.investedCapitalInLossPct, name: "In Loss", color: colors.loss, - unit: Unit.pctRcap, + unit: Unit.pctOwnRcap, }), ]; } @@ -1656,12 +1640,6 @@ function createInvestedCapitalRelMetrics(rel) { */ function createUnrealizedPnlBaseMetrics(tree) { return [ - line({ - metric: tree.unrealized.totalUnrealizedPnl, - name: "Total", - color: colors.default, - unit: Unit.usd, - }), line({ metric: tree.unrealized.unrealizedProfit, name: "Profit", @@ -1682,6 +1660,12 @@ function createUnrealizedPnlBaseMetrics(tree) { unit: Unit.usd, defaultActive: false, }), + line({ + metric: tree.unrealized.totalUnrealizedPnl, + name: "Total", + color: colors.default, + unit: Unit.usd, + }), ]; } @@ -1934,7 +1918,7 @@ function createUnrealizedSection({ charts = [], }) { return { - name: "Unrealized", + name: "Profitability", tree: [ { name: "P&L", @@ -2008,7 +1992,7 @@ function createGroupedInvestedCapitalRelativeCharts(list, title) { metric: tree.relative.investedCapitalInProfitPct, name, color, - unit: Unit.pctRcap, + unit: Unit.pctOwnRcap, }), ), }, @@ -2020,7 +2004,7 @@ function createGroupedInvestedCapitalRelativeCharts(list, title) { metric: tree.relative.investedCapitalInLossPct, name, color, - unit: Unit.pctRcap, + unit: Unit.pctOwnRcap, }), ), }, @@ -2044,7 +2028,7 @@ function createGroupedUnrealizedSection({ charts = [], }) { return { - name: "Unrealized", + name: "Profitability", tree: [ ...createGroupedUnrealizedBaseCharts(list, title), { @@ -2102,7 +2086,7 @@ function createGroupedUnrealizedSection({ */ function createGroupedUnrealizedSectionWithoutRelative(list, title) { return { - name: "Unrealized", + name: "Profitability", tree: [ ...createGroupedUnrealizedBaseCharts(list, title), { @@ -2563,439 +2547,3 @@ function createGroupedUnrealizedSectionAgeRange(list, title) { charts: [createGroupedPeakRegretChartBasic(list, title)], }); } - -// ============================================================================ -// Cost Basis Section Builders (generic, type-safe composition) -// ============================================================================ -// ============================================================================ -// Activity Section Builders (generic, type-safe composition) -// ============================================================================ - -/** - * Generic single activity section builder - callers pass optional extra value metrics - * @param {Object} args - * @param {UtxoCohortObject | CohortWithoutRelative} args.cohort - * @param {(metric: string) => string} args.title - * @param {AnyFetchedSeriesBlueprint[]} [args.valueMetrics] - Extra value metrics (e.g., adjusted) - * @returns {PartialOptionsGroup} - */ -function createActivitySection({ cohort, title, valueMetrics = [] }) { - const { tree, color } = cohort; - - return { - name: "Activity", - tree: [ - { - name: "Sent", - tree: [ - { - name: "Sum", - title: title("Sent"), - bottom: [ - line({ - metric: tree.activity.sent.sats.sum, - name: "sum", - color, - unit: Unit.sats, - }), - line({ - metric: tree.activity.sent.bitcoin.sum, - name: "sum", - color, - unit: Unit.btc, - }), - line({ - metric: tree.activity.sent.dollars.sum, - name: "sum", - color, - unit: Unit.usd, - }), - line({ - metric: tree.activity.sent14dEma.sats, - name: "14d EMA", - unit: Unit.sats, - }), - line({ - metric: tree.activity.sent14dEma.bitcoin, - name: "14d EMA", - unit: Unit.btc, - }), - line({ - metric: tree.activity.sent14dEma.dollars, - name: "14d EMA", - unit: Unit.usd, - }), - ], - }, - { - name: "Cumulative", - title: title("Sent (Total)"), - bottom: [ - line({ - metric: tree.activity.sent.sats.cumulative, - name: "all-time", - color, - unit: Unit.sats, - }), - line({ - metric: tree.activity.sent.bitcoin.cumulative, - name: "all-time", - color, - unit: Unit.btc, - }), - line({ - metric: tree.activity.sent.dollars.cumulative, - name: "all-time", - color, - unit: Unit.usd, - }), - ], - }, - ], - }, - { - name: "Sell Side Risk", - title: title("Sell Side Risk Ratio"), - bottom: createSingleSellSideRiskSeries(tree), - }, - { - name: "Value", - tree: [ - { - name: "Created & Destroyed", - title: title("Value Created & Destroyed"), - bottom: [ - ...createSingleValueCreatedDestroyedSeries(tree), - ...valueMetrics, - ], - }, - { - name: "Breakdown", - title: title("Value Flow Breakdown"), - bottom: createSingleValueFlowBreakdownSeries(tree), - }, - { - name: "Flow", - title: title("Capitulation & Profit Flow"), - bottom: createSingleCapitulationProfitFlowSeries(tree), - }, - ], - }, - { - name: "Coins Destroyed", - title: title("Coins Destroyed"), - bottom: createSingleCoinsDestroyedSeries(cohort), - }, - ], - }; -} - -/** - * Create grouped value flow charts (profit/loss created/destroyed, profit/capitulation flow) - * @template {readonly (UtxoCohortObject | CohortWithoutRelative)[]} T - * @param {T} list - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function createGroupedValueFlowCharts(list, title) { - return [ - { - name: "Profit Created", - title: title("Profit Value Created"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.profitValueCreated, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "Profit Destroyed", - title: title("Profit Value Destroyed"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.profitValueDestroyed, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "Loss Created", - title: title("Loss Value Created"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.lossValueCreated, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "Loss Destroyed", - title: title("Loss Value Destroyed"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.lossValueDestroyed, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "Profit Flow", - title: title("Profit Flow"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.profitFlow, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "Capitulation Flow", - title: title("Capitulation Flow"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.capitulationFlow, - name, - color, - unit: Unit.usd, - }), - ]), - }, - ]; -} - -/** - * Generic grouped activity section builder - callers pass optional value tree - * @template {readonly (UtxoCohortObject | CohortWithoutRelative)[]} T - * @param {Object} args - * @param {T} args.list - * @param {(metric: string) => string} args.title - * @param {PartialOptionsTree} [args.valueTree] - Optional value tree (defaults to basic created/destroyed) - * @returns {PartialOptionsGroup} - */ -function createGroupedActivitySection({ list, title, valueTree }) { - return { - name: "Activity", - tree: [ - { - name: "Sent", - tree: [ - { - name: "Sum", - title: title("Sent"), - bottom: list.flatMap(({ color, name, tree }) => - satsBtcUsd({ - pattern: { - sats: tree.activity.sent.sats.sum, - bitcoin: tree.activity.sent.bitcoin.sum, - dollars: tree.activity.sent.dollars.sum, - }, - name, - color, - }), - ), - }, - { - name: "14d EMA", - title: title("Sent 14d EMA"), - bottom: list.flatMap(({ color, name, tree }) => - satsBtcUsd({ pattern: tree.activity.sent14dEma, name, color }), - ), - }, - ], - }, - { - name: "Sell Side Risk", - title: title("Sell Side Risk Ratio"), - bottom: createGroupedSellSideRiskSeries(list), - }, - { - name: "Value", - tree: valueTree ?? [ - { - name: "Created", - title: title("Value Created"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.valueCreated, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "Destroyed", - title: title("Value Destroyed"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.valueDestroyed, - name, - color, - unit: Unit.usd, - }), - ]), - }, - ...createGroupedValueFlowCharts(list, title), - ], - }, - { - name: "Coins Destroyed", - tree: [ - { - name: "Sum", - title: title("Coins Destroyed"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.activity.coinblocksDestroyed.sum, - name, - color, - unit: Unit.coinblocks, - }), - line({ - metric: tree.activity.coindaysDestroyed.sum, - name, - color, - unit: Unit.coindays, - }), - ]), - }, - { - name: "Cumulative", - title: title("Cumulative Coins Destroyed"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.activity.coinblocksDestroyed.cumulative, - name, - color, - unit: Unit.coinblocks, - }), - line({ - metric: tree.activity.coindaysDestroyed.cumulative, - name, - color, - unit: Unit.coindays, - }), - ]), - }, - ], - }, - ], - }; -} - -// ============================================================================ -// Activity Section Variants (by cohort capability) -// ============================================================================ - -/** - * Create activity section with adjusted values (for cohorts with RealizedPattern3/4) - * @param {CohortAll | CohortFull | CohortWithAdjusted} cohort - * @param {(metric: string) => string} title - * @returns {PartialOptionsGroup} - */ -function createSingleActivitySectionWithAdjusted(cohort, title) { - const { tree } = cohort; - return createActivitySection({ - cohort, - title, - valueMetrics: [ - line({ - metric: tree.realized.adjustedValueCreated, - name: "Adjusted Created", - color: colors.adjustedCreated, - unit: Unit.usd, - }), - line({ - metric: tree.realized.adjustedValueDestroyed, - name: "Adjusted Destroyed", - color: colors.adjustedDestroyed, - unit: Unit.usd, - }), - ], - }); -} - -/** - * Create activity section for grouped cohorts with adjusted values (for cohorts with RealizedPattern3/4) - * @param {readonly (CohortFull | CohortWithAdjusted)[]} list - * @param {(metric: string) => string} title - * @returns {PartialOptionsGroup} - */ -function createGroupedActivitySectionWithAdjusted(list, title) { - return createGroupedActivitySection({ - list, - title, - valueTree: [ - { - name: "Created", - tree: [ - { - name: "Normal", - title: title("Value Created"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.valueCreated, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "Adjusted", - title: title("Adjusted Value Created"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.adjustedValueCreated, - name, - color, - unit: Unit.usd, - }), - ]), - }, - ], - }, - { - name: "Destroyed", - tree: [ - { - name: "Normal", - title: title("Value Destroyed"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.valueDestroyed, - name, - color, - unit: Unit.usd, - }), - ]), - }, - { - name: "Adjusted", - title: title("Adjusted Value Destroyed"), - bottom: list.flatMap(({ color, name, tree }) => [ - line({ - metric: tree.realized.adjustedValueDestroyed, - name, - color, - unit: Unit.usd, - }), - ]), - }, - ], - }, - ...createGroupedValueFlowCharts(list, title), - ], - }); -} diff --git a/website/scripts/options/distribution/valuation.js b/website/scripts/options/distribution/valuation.js index eb8d9c3b1..2817fe322 100644 --- a/website/scripts/options/distribution/valuation.js +++ b/website/scripts/options/distribution/valuation.js @@ -58,7 +58,15 @@ export function createValuationSectionFull({ cohort, title }) { { name: "Realized Cap", title: title("Realized Cap"), - bottom: createSingleRealizedCapSeries(cohort), + bottom: [ + ...createSingleRealizedCapSeries(cohort), + baseline({ + metric: tree.realized.realizedCapRelToOwnMarketCap, + name: "Rel. to Own M.Cap", + color, + unit: Unit.pctOwnMcap, + }), + ], }, { name: "30d Change", @@ -163,3 +171,63 @@ export function createGroupedValuationSection({ list, title }) { ], }; } + +/** + * @template {{ name: string, color: Color, tree: { realized: { realizedCap: AnyMetricPattern, realizedCap30dDelta: AnyMetricPattern, realizedCapRelToOwnMarketCap: AnyMetricPattern, realizedPriceExtra: { ratio: AnyMetricPattern } } } }} T + * @param {{ list: readonly T[], title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createGroupedValuationSectionWithOwnMarketCap({ list, title }) { + return { + name: "Valuation", + tree: [ + { + name: "Realized Cap", + title: title("Realized Cap"), + bottom: [ + ...list.map(({ name, color, tree }) => + line({ + metric: tree.realized.realizedCap, + name, + color, + unit: Unit.usd, + }), + ), + ...list.map(({ name, color, tree }) => + baseline({ + metric: tree.realized.realizedCapRelToOwnMarketCap, + name, + color, + unit: Unit.pctOwnMcap, + }), + ), + ], + }, + { + name: "30d Change", + title: title("Realized Cap 30d Change"), + bottom: list.map(({ name, color, tree }) => + baseline({ + metric: tree.realized.realizedCap30dDelta, + name, + color, + unit: Unit.usd, + }), + ), + }, + { + name: "MVRV", + title: title("MVRV"), + bottom: list.map(({ name, color, tree }) => + baseline({ + metric: tree.realized.realizedPriceExtra.ratio, + name, + color, + unit: Unit.ratio, + base: 1, + }), + ), + }, + ], + }; +} diff --git a/website/scripts/options/investing.js b/website/scripts/options/investing.js index a2823328e..85b7f9136 100644 --- a/website/scripts/options/investing.js +++ b/website/scripts/options/investing.js @@ -327,7 +327,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) { baseline({ metric: dca.periodLumpSumMaxReturn[key], name: "Lump Sum", - color: colors.bi.lumpSum, + color: colors.bi.p2, unit: Unit.percentage, }), ], @@ -345,7 +345,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) { baseline({ metric: dca.periodLumpSumMinReturn[key], name: "Lump Sum", - color: colors.bi.lumpSum, + color: colors.bi.p2, unit: Unit.percentage, }), ], @@ -369,7 +369,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) { baseline({ metric: dca.periodLumpSumReturns[key], name: "Lump Sum", - color: colors.bi.lumpSum, + color: colors.bi.p2, unit: Unit.percentage, }), ], @@ -395,7 +395,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) { baseline({ metric: dca.periodLumpSumReturns[key], name: "Lump Sum", - color: colors.bi.lumpSum, + color: colors.bi.p2, unit: Unit.percentage, }), baseline({ @@ -406,7 +406,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) { baseline({ metric: returns.cagr[key], name: "Lump Sum", - color: colors.bi.lumpSum, + color: colors.bi.p2, unit: Unit.cagr, }), ], diff --git a/website/scripts/options/mining.js b/website/scripts/options/mining.js index c2737c7a3..685609652 100644 --- a/website/scripts/options/mining.js +++ b/website/scripts/options/mining.js @@ -415,7 +415,7 @@ export function createMiningSection() { }, { name: "Distribution", - title: "Coinbase Rewards Distribution", + title: "Coinbase Rewards per Block Distribution", bottom: distributionBtcSatsUsd(blocks.rewards.coinbase), }, { @@ -476,7 +476,7 @@ export function createMiningSection() { tree: [ { name: "Sum", - title: "Transaction Fee Revenue", + title: "Transaction Fee Revenue per Block", bottom: satsBtcUsdFrom({ source: transactions.fees.fee, key: "sum", @@ -485,7 +485,7 @@ export function createMiningSection() { }, { name: "Distribution", - title: "Transaction Fee Revenue Distribution", + title: "Transaction Fee Revenue per Block Distribution", bottom: distributionBtcSatsUsd(transactions.fees.fee), }, { diff --git a/website/scripts/options/network.js b/website/scripts/options/network.js index ceb5d924b..09ded173b 100644 --- a/website/scripts/options/network.js +++ b/website/scripts/options/network.js @@ -10,10 +10,10 @@ import { baseline, fromSupplyPattern, fromBaseStatsPattern, - chartsFromFull, + chartsFromFullPerBlock, chartsFromValueFull, fromStatsPattern, - chartsFromSum, + chartsFromSumPerBlock, } from "./series.js"; import { satsBtcUsd, satsBtcUsdFrom } from "./shared.js"; @@ -93,20 +93,20 @@ export function createNetworkSection() { { key: "sending", name: "Sending", - title: "Sending Address Count", - compareTitle: "Sending Address Count by Type", + title: "Unique Sending Addresses per Block", + compareTitle: "Unique Sending Addresses per Block by Type", }, { key: "receiving", name: "Receiving", - title: "Receiving Address Count", - compareTitle: "Receiving Address Count by Type", + title: "Unique Receiving Addresses per Block", + compareTitle: "Unique Receiving Addresses per Block by Type", }, { key: "both", name: "Sending & Receiving", - title: "Addresses Sending & Receiving", - compareTitle: "Addresses Sending & Receiving by Type", + title: "Unique Addresses Sending & Receiving per Block", + compareTitle: "Unique Addresses Sending & Receiving per Block by Type", }, ]); @@ -114,15 +114,15 @@ export function createNetworkSection() { const balanceTypes = /** @type {const} */ ([ { key: "balanceIncreased", - name: "Increased", - title: "Addresses with Increased Balance", - compareTitle: "Addresses with Increased Balance by Type", + name: "Accumulating", + title: "Accumulating Addresses per Block", + compareTitle: "Accumulating Addresses per Block by Type", }, { key: "balanceDecreased", - name: "Decreased", - title: "Addresses with Decreased Balance", - compareTitle: "Addresses with Decreased Balance by Type", + name: "Distributing", + title: "Distributing Addresses per Block", + compareTitle: "Distributing Addresses per Block by Type", }, ]); @@ -165,16 +165,16 @@ export function createNetworkSection() { unit: Unit.count, }), line({ - metric: distribution.totalAddrCount[key], - name: "Total", - color: colors.default, + metric: distribution.emptyAddrCount[key].count, + name: "Empty", + color: colors.gray, unit: Unit.count, defaultActive: false, }), line({ - metric: distribution.emptyAddrCount[key].count, - name: "Empty", - color: colors.gray, + metric: distribution.totalAddrCount[key], + name: "Total", + color: colors.default, unit: Unit.count, defaultActive: false, }), @@ -189,14 +189,21 @@ export function createNetworkSection() { bottom: [ baseline({ metric: distribution.addrCount[key]._30dChange, - name: "30d Change", + name: "Funded", unit: Unit.count, }), + baseline({ + metric: distribution.emptyAddrCount[key]._30dChange, + name: "Empty", + color: colors.gray, + unit: Unit.count, + defaultActive: false, + }), ], }, { name: "New", - tree: chartsFromFull({ + tree: chartsFromFullPerBlock({ pattern: distribution.newAddrCount[key], title: `${titlePrefix}New Address Count`, unit: Unit.count, @@ -204,7 +211,7 @@ export function createNetworkSection() { }, { name: "Reactivated", - title: `${titlePrefix}Reactivated Address Count`, + title: `${titlePrefix}Reactivated Addresses per Block`, bottom: fromBaseStatsPattern({ pattern: distribution.addressActivity[key].reactivated, unit: Unit.count, @@ -212,7 +219,7 @@ export function createNetworkSection() { }, { name: "Growth Rate", - title: `${titlePrefix}Address Growth Rate`, + title: `${titlePrefix}Address Growth Rate per Block`, bottom: fromBaseStatsPattern({ pattern: distribution.growthRate[key], unit: Unit.ratio, @@ -288,7 +295,7 @@ export function createNetworkSection() { }, { name: "Reactivated", - title: `${groupName} Reactivated Address Count`, + title: `${groupName} Reactivated Addresses per Block`, bottom: types.flatMap((t) => [ line({ metric: distribution.addressActivity[t.key].reactivated.base, @@ -306,7 +313,7 @@ export function createNetworkSection() { }, { name: "Growth Rate", - title: `${groupName} Address Growth Rate`, + title: `${groupName} Address Growth Rate per Block`, bottom: types.flatMap((t) => [ dots({ metric: distribution.growthRate[t.key].base, @@ -489,7 +496,7 @@ export function createNetworkSection() { tree: [ { name: "Count", - tree: chartsFromFull({ + tree: chartsFromFullPerBlock({ pattern: transactions.count.txCount, title: "Transaction Count", unit: Unit.count, @@ -497,7 +504,7 @@ export function createNetworkSection() { }, { name: "Fee Rate", - title: "Fee Rate", + title: "Transaction Fee Rate", bottom: fromStatsPattern({ pattern: transactions.fees.feeRate, unit: Unit.feeRate, @@ -897,7 +904,7 @@ export function createNetworkSection() { }, { name: "Inputs", - tree: chartsFromSum({ + tree: chartsFromSumPerBlock({ pattern: inputs.count, title: "Input Count", unit: Unit.count, @@ -905,7 +912,7 @@ export function createNetworkSection() { }, { name: "Outputs", - tree: chartsFromSum({ + tree: chartsFromSumPerBlock({ pattern: outputs.count.totalCount, title: "Output Count", unit: Unit.count, @@ -985,7 +992,7 @@ export function createNetworkSection() { }, { name: "Reactivated", - title: "Reactivated Address Count by Type", + title: "Reactivated Addresses per Block by Type", bottom: addressTypes.flatMap((t) => [ line({ metric: @@ -1007,7 +1014,7 @@ export function createNetworkSection() { }, { name: "Growth Rate", - title: "Address Growth Rate by Type", + title: "Address Growth Rate per Block by Type", bottom: addressTypes.flatMap((t) => [ dots({ metric: distribution.growthRate[t.key].base, @@ -1159,7 +1166,7 @@ export function createNetworkSection() { createScriptCompare("Legacy", legacyScripts), ...legacyScripts.map((t) => ({ name: t.name, - tree: chartsFromFull({ + tree: chartsFromFullPerBlock({ pattern: scripts.count[t.key], title: `${t.name} Output Count`, unit: Unit.count, @@ -1173,7 +1180,7 @@ export function createNetworkSection() { createScriptCompare("Script Hash", scriptHashScripts), ...scriptHashScripts.map((t) => ({ name: t.name, - tree: chartsFromFull({ + tree: chartsFromFullPerBlock({ pattern: scripts.count[t.key], title: `${t.name} Output Count`, unit: Unit.count, @@ -1187,7 +1194,7 @@ export function createNetworkSection() { createScriptCompare("SegWit", segwitScripts), ...segwitScripts.map((t) => ({ name: t.name, - tree: chartsFromFull({ + tree: chartsFromFullPerBlock({ pattern: scripts.count[t.key], title: `${t.name} Output Count`, unit: Unit.count, @@ -1201,7 +1208,7 @@ export function createNetworkSection() { createScriptCompare("Taproot", taprootAddresses), ...taprootAddresses.map((t) => ({ name: t.name, - tree: chartsFromFull({ + tree: chartsFromFullPerBlock({ pattern: scripts.count[t.key], title: `${t.name} Output Count`, unit: Unit.count, @@ -1215,7 +1222,7 @@ export function createNetworkSection() { createScriptCompare("Other", otherScripts), ...otherScripts.map((t) => ({ name: t.name, - tree: chartsFromFull({ + tree: chartsFromFullPerBlock({ pattern: scripts.count[t.key], title: `${t.name} Output Count`, unit: Unit.count, diff --git a/website/scripts/options/partial.js b/website/scripts/options/partial.js index a59962bcc..b485a2a74 100644 --- a/website/scripts/options/partial.js +++ b/website/scripts/options/partial.js @@ -6,6 +6,7 @@ import { createCohortFolderFull, createCohortFolderWithAdjusted, createCohortFolderWithNupl, + createCohortFolderLongTerm, createCohortFolderAgeRange, createCohortFolderMinAge, createCohortFolderBasicWithMarketCap, @@ -90,8 +91,8 @@ export function createPartialOptions() { // STH - Short term holder cohort (Full capability) createCohortFolderFull(termShort), - // LTH - Long term holder cohort (nupl) - createCohortFolderWithNupl(termLong), + // LTH - Long term holder cohort (own market cap + nupl + peak regret + P/L ratio) + createCohortFolderLongTerm(termLong), // Ages cohorts { diff --git a/website/scripts/options/series.js b/website/scripts/options/series.js index 17624454d..50542267b 100644 --- a/website/scripts/options/series.js +++ b/website/scripts/options/series.js @@ -211,7 +211,6 @@ export function candlestick({ metric, name, key, - defaultActive, unit, options, @@ -221,7 +220,6 @@ export function candlestick({ metric, title: name, key, - unit, defaultActive, options, @@ -238,6 +236,7 @@ export function candlestick({ * @param {Color | [Color, Color]} [args.color] * @param {boolean} [args.defaultActive] * @param {number | undefined} [args.base] + * @param {number} [args.style] - Line style (0: Solid, 1: Dotted, 2: Dashed, 3: LargeDashed, 4: SparseDotted) * @param {BaselineSeriesPartialOptions} [args.options] * @returns {FetchedBaselineSeriesBlueprint} */ @@ -249,6 +248,7 @@ export function baseline({ defaultActive, unit, base, + style, options, }) { const isTuple = Array.isArray(color); @@ -261,6 +261,58 @@ export function baseline({ colors: isTuple ? color : undefined, unit, defaultActive, + options: { + baseValue: { + price: base, + }, + lineStyle: style, + ...options, + }, + }; +} + +/** + * @param {Omit[0], 'style'>} args + */ +export function dottedBaseline(args) { + const _args = /** @type {Parameters[0]} */ (args); + _args.style = 1; + return baseline(_args); +} + +/** + * Baseline series rendered as dots (points only, no line) + * @param {Object} args + * @param {AnyMetricPattern} args.metric + * @param {string} args.name + * @param {Unit} args.unit + * @param {string} [args.key] + * @param {Color | [Color, Color]} [args.color] + * @param {boolean} [args.defaultActive] + * @param {number | undefined} [args.base] + * @param {BaselineSeriesPartialOptions} [args.options] + * @returns {FetchedDotsBaselineSeriesBlueprint} + */ +export function dotsBaseline({ + metric, + name, + key, + color, + defaultActive, + unit, + base, + options, +}) { + const isTuple = Array.isArray(color); + return { + type: /** @type {const} */ ("DotsBaseline"), + metric, + title: name, + key, + color: isTuple ? undefined : color, + colors: isTuple ? color : undefined, + unit, + defaultActive, options: { baseValue: { price: base, @@ -509,9 +561,18 @@ function btcSatsUsdSeries({ metrics, name, color, defaultActive }) { * @param {FullStatsPattern} args.pattern * @param {string} args.title * @param {Unit} args.unit + * @param {string} [args.distributionSuffix] * @returns {PartialOptionsTree} */ -export function chartsFromFull({ pattern, title, unit }) { +export function chartsFromFull({ + pattern, + title, + unit, + distributionSuffix = "", +}) { + const distTitle = distributionSuffix + ? `${title} ${distributionSuffix} Distribution` + : `${title} Distribution`; return [ { name: "Sum", @@ -523,7 +584,7 @@ export function chartsFromFull({ pattern, title, unit }) { }, { name: "Distribution", - title: `${title} Distribution`, + title: distTitle, bottom: distributionSeries(pattern, unit), }, { @@ -534,16 +595,36 @@ export function chartsFromFull({ pattern, title, unit }) { ]; } +/** + * Split pattern into 3 charts with "per Block" in distribution title + * @param {Object} args + * @param {FullStatsPattern} args.pattern + * @param {string} args.title + * @param {Unit} args.unit + * @returns {PartialOptionsTree} + */ +export const chartsFromFullPerBlock = (args) => + chartsFromFull({ ...args, distributionSuffix: "per Block" }); + /** * Split pattern with sum + distribution + cumulative into 3 charts (no base) * @param {Object} args * @param {AnyStatsPattern} args.pattern * @param {string} args.title * @param {Unit} args.unit + * @param {string} [args.distributionSuffix] * @returns {PartialOptionsTree} */ -export function chartsFromSum({ pattern, title, unit }) { +export function chartsFromSum({ + pattern, + title, + unit, + distributionSuffix = "", +}) { const { stat } = colors; + const distTitle = distributionSuffix + ? `${title} ${distributionSuffix} Distribution` + : `${title} Distribution`; return [ { name: "Sum", @@ -552,7 +633,7 @@ export function chartsFromSum({ pattern, title, unit }) { }, { name: "Distribution", - title: `${title} Distribution`, + title: distTitle, bottom: distributionSeries(pattern, unit), }, { @@ -563,6 +644,17 @@ export function chartsFromSum({ pattern, title, unit }) { ]; } +/** + * Split pattern into 3 charts with "per Block" in distribution title (no base) + * @param {Object} args + * @param {AnyStatsPattern} args.pattern + * @param {string} args.title + * @param {Unit} args.unit + * @returns {PartialOptionsTree} + */ +export const chartsFromSumPerBlock = (args) => + chartsFromSum({ ...args, distributionSuffix: "per Block" }); + /** * Split pattern with sum + cumulative into 2 charts * @param {Object} args diff --git a/website/scripts/options/shared.js b/website/scripts/options/shared.js index 834b8460a..999b3baf1 100644 --- a/website/scripts/options/shared.js +++ b/website/scripts/options/shared.js @@ -20,9 +20,10 @@ export const formatCohortTitle = (cohortTitle) => (metric) => * @param {string} args.name * @param {Color} [args.color] * @param {boolean} [args.defaultActive] + * @param {number} [args.style] * @returns {FetchedLineSeriesBlueprint[]} */ -export function satsBtcUsd({ pattern, name, color, defaultActive }) { +export function satsBtcUsd({ pattern, name, color, defaultActive, style }) { return [ line({ metric: pattern.bitcoin, @@ -30,14 +31,23 @@ export function satsBtcUsd({ pattern, name, color, defaultActive }) { color, unit: Unit.btc, defaultActive, + style, + }), + line({ + metric: pattern.sats, + name, + color, + unit: Unit.sats, + defaultActive, + style, }), - line({ metric: pattern.sats, name, color, unit: Unit.sats, defaultActive }), line({ metric: pattern.dollars, name, color, unit: Unit.usd, defaultActive, + style, }), ]; } @@ -60,7 +70,13 @@ export function satsBtcUsdBaseline({ pattern, name, color, defaultActive }) { unit: Unit.btc, defaultActive, }), - baseline({ metric: pattern.sats, name, color, unit: Unit.sats, defaultActive }), + baseline({ + metric: pattern.sats, + name, + color, + unit: Unit.sats, + defaultActive, + }), baseline({ metric: pattern.dollars, name, @@ -146,7 +162,12 @@ export function revenueBtcSatsUsd({ coinbase, subsidy, fee, key }) { name: "Subsidy", color: colors.mining.subsidy, }), - ...satsBtcUsdFrom({ source: fee, key, name: "Fees", color: colors.mining.fee }), + ...satsBtcUsdFrom({ + source: fee, + key, + name: "Fees", + color: colors.mining.fee, + }), ]; } @@ -477,8 +498,7 @@ export function createPriceRatioCharts({ ], }, createRatioChart({ - title: (name) => - titleFn(titlePrefix ? `${titlePrefix} ${name}` : name), + title: (name) => titleFn(titlePrefix ? `${titlePrefix} ${name}` : name), pricePattern, ratio, color, diff --git a/website/scripts/options/types.js b/website/scripts/options/types.js index 023c77e55..cb38baf17 100644 --- a/website/scripts/options/types.js +++ b/website/scripts/options/types.js @@ -35,7 +35,14 @@ * @property {LineSeriesPartialOptions} [options] * @typedef {BaseSeriesBlueprint & DotsSeriesBlueprintSpecific} DotsSeriesBlueprint * - * @typedef {BaselineSeriesBlueprint | CandlestickSeriesBlueprint | LineSeriesBlueprint | HistogramSeriesBlueprint | DotsSeriesBlueprint} AnySeriesBlueprint + * @typedef {Object} DotsBaselineSeriesBlueprintSpecific + * @property {"DotsBaseline"} type + * @property {Color} [color] + * @property {[Color, Color]} [colors] + * @property {BaselineSeriesPartialOptions} [options] + * @typedef {BaseSeriesBlueprint & DotsBaselineSeriesBlueprintSpecific} DotsBaselineSeriesBlueprint + * + * @typedef {BaselineSeriesBlueprint | CandlestickSeriesBlueprint | LineSeriesBlueprint | HistogramSeriesBlueprint | DotsSeriesBlueprint | DotsBaselineSeriesBlueprint} AnySeriesBlueprint * * @typedef {AnySeriesBlueprint["type"]} SeriesType * @@ -46,6 +53,7 @@ * @typedef {LineSeriesBlueprint & FetchedAnySeriesOptions} FetchedLineSeriesBlueprint * @typedef {HistogramSeriesBlueprint & FetchedAnySeriesOptions} FetchedHistogramSeriesBlueprint * @typedef {DotsSeriesBlueprint & FetchedAnySeriesOptions} FetchedDotsSeriesBlueprint + * @typedef {DotsBaselineSeriesBlueprint & FetchedAnySeriesOptions} FetchedDotsBaselineSeriesBlueprint * @typedef {AnySeriesBlueprint & FetchedAnySeriesOptions} AnyFetchedSeriesBlueprint * * Any pattern with dollars and sats sub-metrics (auto-expands to USD + sats) @@ -317,6 +325,7 @@ * @property {string} title * @property {Color} color * @property {AddressCohortPattern} tree + * @property {Brk._30dCountPattern} addrCount * * @typedef {UtxoCohortObject | AddressCohortObject | CohortWithoutRelative} CohortObject * diff --git a/website/scripts/options/unused.js b/website/scripts/options/unused.js index 203523814..e4c9eb801 100644 --- a/website/scripts/options/unused.js +++ b/website/scripts/options/unused.js @@ -28,22 +28,27 @@ function walk(node, map, path) { for (const [key, value] of Object.entries(node)) { const kn = key.toLowerCase(); if ( - // kn === "mvrv" || + key.endsWith("Raw") || + key.endsWith("Cents") || + key.endsWith("State") || + key.endsWith("Start") || + kn === "mvrv" || // kn === "time" || // kn === "height" || - // kn === "constants" || + kn === "constants" || kn === "blockhash" || kn === "date" || // kn === "oracle" || - // kn === "split" || + kn === "split" || // kn === "ohlc" || - // kn === "outpoint" || + kn === "outpoint" || kn === "positions" || // kn === "outputtype" || kn === "heighttopool" || - // kn === "txid" || + kn === "txid" || kn.startsWith("timestamp") || - // kn.startsWith("satdays") || + kn.startsWith("satdays") || + kn.startsWith("satblocks") || // kn.endsWith("state") || // kn.endsWith("cents") || kn.endsWith("index") || diff --git a/website/scripts/panes/chart.js b/website/scripts/panes/chart.js index b797c1fef..3721d8bc3 100644 --- a/website/scripts/panes/chart.js +++ b/website/scripts/panes/chart.js @@ -61,7 +61,7 @@ export function init() { type: "Candlestick", title: "Price", metric: brk.metrics.price.sats.ohlc, - colors: colors.bi.profitLoss, + colors: /** @type {const} */ ([colors.bi.p1[1], colors.bi.p1[0]]), }; result.set(Unit.sats, [satsPrice, ...(optionTop.get(Unit.sats) ?? [])]); @@ -97,15 +97,16 @@ export function init() { _setOption = (opt) => { headingElement.innerHTML = opt.title; - // Update index choices based on option - setChoices(computeChoices(opt)); - + // Set blueprints first so storageId is correct before any index change blueprints = chart.setBlueprints({ name: opt.title, top: buildTopBlueprints(opt.top), bottom: opt.bottom, onDataLoaded: updatePriceWithLatest, }); + + // Update index choices (may trigger rebuild if index changes) + setChoices(computeChoices(opt)); }; // Live price update listener diff --git a/website/scripts/types.js b/website/scripts/types.js index cb4a6f30a..4fd1e6da6 100644 --- a/website/scripts/types.js +++ b/website/scripts/types.js @@ -14,7 +14,7 @@ * * @import { WebSockets } from "./utils/ws.js" * - * @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, TableOption, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree, UtxoCohortObject, AddressCohortObject, CohortObject, CohortGroupObject, FetchedLineSeriesBlueprint, FetchedBaselineSeriesBlueprint, FetchedHistogramSeriesBlueprint, PatternAll, PatternFull, PatternWithAdjusted, PatternWithPercentiles, PatternBasic, PatternBasicWithMarketCap, PatternBasicWithoutMarketCap, PatternWithoutRelative, CohortAll, CohortFull, CohortWithAdjusted, CohortWithPercentiles, CohortBasic, CohortBasicWithMarketCap, CohortBasicWithoutMarketCap, CohortWithoutRelative, CohortAddress, CohortLongTerm, CohortAgeRange, CohortMinAge, CohortGroupFull, CohortGroupWithAdjusted, CohortGroupWithPercentiles, CohortGroupLongTerm, CohortGroupAgeRange, CohortGroupBasic, CohortGroupBasicWithMarketCap, CohortGroupBasicWithoutMarketCap, CohortGroupWithoutRelative, CohortGroupMinAge, CohortGroupAddress, UtxoCohortGroupObject, AddressCohortGroupObject, FetchedDotsSeriesBlueprint, FetchedCandlestickSeriesBlueprint, FetchedPriceSeriesBlueprint, AnyPricePattern, AnyValuePattern } from "./options/partial.js" + * @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, TableOption, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree, UtxoCohortObject, AddressCohortObject, CohortObject, CohortGroupObject, FetchedLineSeriesBlueprint, FetchedBaselineSeriesBlueprint, FetchedHistogramSeriesBlueprint, FetchedDotsBaselineSeriesBlueprint, PatternAll, PatternFull, PatternWithAdjusted, PatternWithPercentiles, PatternBasic, PatternBasicWithMarketCap, PatternBasicWithoutMarketCap, PatternWithoutRelative, CohortAll, CohortFull, CohortWithAdjusted, CohortWithPercentiles, CohortBasic, CohortBasicWithMarketCap, CohortBasicWithoutMarketCap, CohortWithoutRelative, CohortAddress, CohortLongTerm, CohortAgeRange, CohortMinAge, CohortGroupFull, CohortGroupWithAdjusted, CohortGroupWithPercentiles, CohortGroupLongTerm, CohortGroupAgeRange, CohortGroupBasic, CohortGroupBasicWithMarketCap, CohortGroupBasicWithoutMarketCap, CohortGroupWithoutRelative, CohortGroupMinAge, CohortGroupAddress, UtxoCohortGroupObject, AddressCohortGroupObject, FetchedDotsSeriesBlueprint, FetchedCandlestickSeriesBlueprint, FetchedPriceSeriesBlueprint, AnyPricePattern, AnyValuePattern } from "./options/partial.js" * * * @import { UnitObject as Unit } from "./utils/units.js" @@ -155,12 +155,12 @@ * @typedef {UtxoCohortPattern | AddressCohortPattern} CohortPattern * * Relative pattern capability types - * @typedef {GlobalRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithMarketCap + * @typedef {GlobalRelativePattern | GlobalPeakRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithMarketCap * @typedef {OwnRelativePattern | FullRelativePattern} RelativeWithOwnMarketCap * @typedef {OwnRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithOwnPnl - * @typedef {GlobalRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithNupl + * @typedef {GlobalRelativePattern | GlobalPeakRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithNupl * @typedef {GlobalPeakRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithPeakRegret - * @typedef {BasicRelativePattern | GlobalRelativePattern | OwnRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithInvestedCapitalPct + * @typedef {BasicRelativePattern | GlobalRelativePattern | GlobalPeakRelativePattern | OwnRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithInvestedCapitalPct * * Realized pattern capability types * RealizedWithExtras: patterns with realizedCapRelToOwnMarketCap + realizedProfitToLossRatio diff --git a/website/scripts/utils/colors.js b/website/scripts/utils/colors.js index 3949eac4f..dbc3f50c5 100644 --- a/website/scripts/utils/colors.js +++ b/website/scripts/utils/colors.js @@ -99,22 +99,14 @@ export const colors = { bitcoin: palette.orange, usd: palette.green, - // Bi-color pairs for baselines + // Bi-color pairs for baselines (spaced by 2 in palette) bi: { /** @type {[Color, Color]} */ - profitLoss: [palette.green, palette.red], + p1: [palette.green, palette.red], /** @type {[Color, Color]} */ - sopr7d: [palette.lime, palette.rose], + p2: [palette.teal, palette.amber], /** @type {[Color, Color]} */ - sopr30d: [palette.avocado, palette.pink], - /** @type {[Color, Color]} */ - adjustedSopr: [palette.yellow, palette.fuchsia], - /** @type {[Color, Color]} */ - adjustedSopr7d: [palette.amber, palette.purple], - /** @type {[Color, Color]} */ - adjustedSopr30d: [palette.orange, palette.violet], - /** @type {[Color, Color]} */ - lumpSum: [palette.cyan, palette.orange], + p3: [palette.sky, palette.avocado], }, @@ -148,9 +140,9 @@ export const colors = { // Mining mining: { - coinbase: palette.orange, - subsidy: palette.lime, - fee: palette.cyan, + coinbase: palette.red, + subsidy: palette.orange, + fee: palette.yellow, }, // Network @@ -158,9 +150,9 @@ export const colors = { // Entity (transactions, inputs, outputs) entity: { - tx: palette.orange, - input: palette.red, - output: palette.cyan, + tx: palette.red, + input: palette.orange, + output: palette.yellow, }, // Technical indicators @@ -215,9 +207,9 @@ export const colors = { // Transaction versions txVersion: { - v1: palette.orange, - v2: palette.cyan, - v3: palette.lime, + v1: palette.red, + v2: palette.orange, + v3: palette.yellow, }, pct: { @@ -239,17 +231,17 @@ export const colors = { _25: palette.fuchsia, _20: palette.pink, _15: palette.rose, - _10: palette.pink, - _05: palette.fuchsia, - _0: palette.purple, + _10: palette.red, + _05: palette.orange, + _0: palette.amber, }, time: { - _24h: palette.pink, - _1w: palette.red, - _1m: palette.yellow, - _1y: palette.lime, - all: palette.teal, + _24h: palette.red, + _1w: palette.yellow, + _1m: palette.green, + _1y: palette.blue, + all: palette.purple, }, term: { @@ -280,44 +272,44 @@ export const colors = { }, ageRange: { - upTo1h: palette.rose, - _1hTo1d: palette.pink, - _1dTo1w: palette.red, - _1wTo1m: palette.orange, - _1mTo2m: palette.yellow, - _2mTo3m: palette.yellow, - _3mTo4m: palette.lime, - _4mTo5m: palette.lime, - _5mTo6m: palette.lime, - _6mTo1y: palette.green, - _1yTo2y: palette.cyan, + upTo1h: palette.red, + _1hTo1d: palette.orange, + _1dTo1w: palette.amber, + _1wTo1m: palette.yellow, + _1mTo2m: palette.avocado, + _2mTo3m: palette.lime, + _3mTo4m: palette.green, + _4mTo5m: palette.emerald, + _5mTo6m: palette.teal, + _6mTo1y: palette.cyan, + _1yTo2y: palette.sky, _2yTo3y: palette.blue, _3yTo4y: palette.indigo, _4yTo5y: palette.violet, _5yTo6y: palette.purple, - _6yTo7y: palette.purple, - _7yTo8y: palette.fuchsia, - _8yTo10y: palette.fuchsia, - _10yTo12y: palette.pink, - _12yTo15y: palette.red, - from15y: palette.orange, + _6yTo7y: palette.fuchsia, + _7yTo8y: palette.pink, + _8yTo10y: palette.rose, + _10yTo12y: palette.red, + _12yTo15y: palette.orange, + from15y: palette.amber, }, amount: { - _1sat: palette.orange, + _1sat: palette.red, _10sats: palette.orange, _100sats: palette.yellow, _1kSats: palette.lime, _10kSats: palette.green, - _100kSats: palette.cyan, - _1mSats: palette.blue, - _10mSats: palette.indigo, - _1btc: palette.purple, + _100kSats: palette.teal, + _1mSats: palette.cyan, + _10mSats: palette.blue, + _1btc: palette.indigo, _10btc: palette.violet, - _100btc: palette.fuchsia, - _1kBtc: palette.pink, - _10kBtc: palette.red, - _100kBtc: palette.orange, + _100btc: palette.purple, + _1kBtc: palette.fuchsia, + _10kBtc: palette.pink, + _100kBtc: palette.rose, }, amountRange: { @@ -326,22 +318,22 @@ export const colors = { _10satsTo100sats: palette.yellow, _100satsTo1kSats: palette.lime, _1kSatsTo10kSats: palette.green, - _10kSatsTo100kSats: palette.cyan, - _100kSatsTo1mSats: palette.blue, - _1mSatsTo10mSats: palette.indigo, - _10mSatsTo1btc: palette.purple, + _10kSatsTo100kSats: palette.teal, + _100kSatsTo1mSats: palette.cyan, + _1mSatsTo10mSats: palette.blue, + _10mSatsTo1btc: palette.indigo, _1btcTo10btc: palette.violet, - _10btcTo100btc: palette.fuchsia, - _100btcTo1kBtc: palette.pink, - _1kBtcTo10kBtc: palette.red, - _10kBtcTo100kBtc: palette.orange, - _100kBtcOrMore: palette.yellow, + _10btcTo100btc: palette.purple, + _100btcTo1kBtc: palette.fuchsia, + _1kBtcTo10kBtc: palette.pink, + _10kBtcTo100kBtc: palette.rose, + _100kBtcOrMore: palette.red, }, epoch: { _0: palette.red, - _1: palette.yellow, - _2: palette.orange, + _1: palette.orange, + _2: palette.yellow, _3: palette.lime, _4: palette.green, }, @@ -388,6 +380,7 @@ export const colors = { _8d: palette.orange, _12d: palette.amber, _13d: palette.yellow, + _14d: palette.avocado, _21d: palette.avocado, _26d: palette.lime, _1m: palette.green, @@ -429,10 +422,10 @@ export const colors = { p2wpkh: palette.teal, p2wsh: palette.blue, p2tr: palette.indigo, - p2a: palette.purple, - opreturn: palette.pink, - unknown: palette.violet, - empty: palette.fuchsia, + p2a: palette.violet, + opreturn: palette.purple, + unknown: palette.fuchsia, + empty: palette.pink, }, arr: Object.values(palette), diff --git a/website/scripts/utils/units.js b/website/scripts/utils/units.js index f8ecced85..94f437c17 100644 --- a/website/scripts/utils/units.js +++ b/website/scripts/utils/units.js @@ -21,6 +21,7 @@ export const Unit = /** @type {const} */ ({ pctOwn: { id: "pct-own", name: "% of Own Supply" }, pctMcap: { id: "pct-mcap", name: "% of Market Cap" }, pctRcap: { id: "pct-rcap", name: "% of Realized Cap" }, + pctOwnRcap: { id: "pct-own-rcap", name: "% of Own Realized Cap" }, pctOwnMcap: { id: "pct-own-mcap", name: "% of Own Market Cap" }, pctOwnPnl: { id: "pct-own-pnl", name: "% of Own P&L" },