diff --git a/crates/brk_computer/src/indexes/day1.rs b/crates/brk_computer/src/indexes/day1.rs index b25fb018c..7a7b79e3c 100644 --- a/crates/brk_computer/src/indexes/day1.rs +++ b/crates/brk_computer/src/indexes/day1.rs @@ -15,7 +15,7 @@ pub struct Vecs { impl Vecs { pub(crate) fn forced_import(db: &Database, version: Version) -> Result { Ok(Self { - identity: EagerVec::forced_import(db, "day1", version)?, + identity: EagerVec::forced_import(db, "day1_index", version)?, date: EagerVec::forced_import(db, "date", version + Version::ONE)?, first_height: EagerVec::forced_import(db, "first_height", version)?, height_count: EagerVec::forced_import(db, "height_count", version)?, diff --git a/crates/brk_computer/src/indexes/day3.rs b/crates/brk_computer/src/indexes/day3.rs index 90c6490ee..9f601ce5d 100644 --- a/crates/brk_computer/src/indexes/day3.rs +++ b/crates/brk_computer/src/indexes/day3.rs @@ -13,7 +13,7 @@ pub struct Vecs { impl Vecs { pub(crate) fn forced_import(db: &Database, version: Version) -> Result { Ok(Self { - identity: EagerVec::forced_import(db, "day3", version)?, + identity: EagerVec::forced_import(db, "day3_index", version)?, first_height: EagerVec::forced_import(db, "day3_first_height", version)?, }) } diff --git a/crates/brk_computer/src/indexes/hour1.rs b/crates/brk_computer/src/indexes/hour1.rs index 4d57d383f..8f1c6c0c2 100644 --- a/crates/brk_computer/src/indexes/hour1.rs +++ b/crates/brk_computer/src/indexes/hour1.rs @@ -13,7 +13,7 @@ pub struct Vecs { impl Vecs { pub(crate) fn forced_import(db: &Database, version: Version) -> Result { Ok(Self { - identity: EagerVec::forced_import(db, "hour1", version)?, + identity: EagerVec::forced_import(db, "hour1_index", version)?, first_height: EagerVec::forced_import(db, "hour1_first_height", version)?, }) } diff --git a/crates/brk_computer/src/indexes/hour12.rs b/crates/brk_computer/src/indexes/hour12.rs index 12dd4d640..2c5cb4721 100644 --- a/crates/brk_computer/src/indexes/hour12.rs +++ b/crates/brk_computer/src/indexes/hour12.rs @@ -13,7 +13,7 @@ pub struct Vecs { impl Vecs { pub(crate) fn forced_import(db: &Database, version: Version) -> Result { Ok(Self { - identity: EagerVec::forced_import(db, "hour12", version)?, + identity: EagerVec::forced_import(db, "hour12_index", version)?, first_height: EagerVec::forced_import(db, "hour12_first_height", version)?, }) } diff --git a/crates/brk_computer/src/indexes/hour4.rs b/crates/brk_computer/src/indexes/hour4.rs index a59acee41..b278daeda 100644 --- a/crates/brk_computer/src/indexes/hour4.rs +++ b/crates/brk_computer/src/indexes/hour4.rs @@ -13,7 +13,7 @@ pub struct Vecs { impl Vecs { pub(crate) fn forced_import(db: &Database, version: Version) -> Result { Ok(Self { - identity: EagerVec::forced_import(db, "hour4", version)?, + identity: EagerVec::forced_import(db, "hour4_index", version)?, first_height: EagerVec::forced_import(db, "hour4_first_height", version)?, }) } diff --git a/crates/brk_computer/src/indexes/minute10.rs b/crates/brk_computer/src/indexes/minute10.rs index bdbcfead1..f7e86d5ac 100644 --- a/crates/brk_computer/src/indexes/minute10.rs +++ b/crates/brk_computer/src/indexes/minute10.rs @@ -13,7 +13,7 @@ pub struct Vecs { impl Vecs { pub(crate) fn forced_import(db: &Database, version: Version) -> Result { Ok(Self { - identity: EagerVec::forced_import(db, "minute10", version)?, + identity: EagerVec::forced_import(db, "minute10_index", version)?, first_height: EagerVec::forced_import(db, "minute10_first_height", version)?, }) } diff --git a/crates/brk_computer/src/indexes/minute30.rs b/crates/brk_computer/src/indexes/minute30.rs index 4d6344eef..6fd38fc2e 100644 --- a/crates/brk_computer/src/indexes/minute30.rs +++ b/crates/brk_computer/src/indexes/minute30.rs @@ -13,7 +13,7 @@ pub struct Vecs { impl Vecs { pub(crate) fn forced_import(db: &Database, version: Version) -> Result { Ok(Self { - identity: EagerVec::forced_import(db, "minute30", version)?, + identity: EagerVec::forced_import(db, "minute30_index", version)?, first_height: EagerVec::forced_import(db, "minute30_first_height", version)?, }) } diff --git a/crates/brk_computer/src/indexes/month1.rs b/crates/brk_computer/src/indexes/month1.rs index 8fbde6b4b..1a79967e4 100644 --- a/crates/brk_computer/src/indexes/month1.rs +++ b/crates/brk_computer/src/indexes/month1.rs @@ -14,7 +14,7 @@ pub struct Vecs { impl Vecs { pub(crate) fn forced_import(db: &Database, version: Version) -> Result { Ok(Self { - identity: EagerVec::forced_import(db, "month1", version)?, + identity: EagerVec::forced_import(db, "month1_index", version)?, date: EagerVec::forced_import(db, "date", version)?, first_height: EagerVec::forced_import(db, "month1_first_height", version)?, }) diff --git a/crates/brk_computer/src/indexes/month3.rs b/crates/brk_computer/src/indexes/month3.rs index bb674ecbe..00f128833 100644 --- a/crates/brk_computer/src/indexes/month3.rs +++ b/crates/brk_computer/src/indexes/month3.rs @@ -14,7 +14,7 @@ pub struct Vecs { impl Vecs { pub(crate) fn forced_import(db: &Database, version: Version) -> Result { Ok(Self { - identity: EagerVec::forced_import(db, "month3", version)?, + identity: EagerVec::forced_import(db, "month3_index", version)?, date: EagerVec::forced_import(db, "date", version)?, first_height: EagerVec::forced_import(db, "month3_first_height", version)?, }) diff --git a/crates/brk_computer/src/indexes/month6.rs b/crates/brk_computer/src/indexes/month6.rs index 64f2cea96..3feb33196 100644 --- a/crates/brk_computer/src/indexes/month6.rs +++ b/crates/brk_computer/src/indexes/month6.rs @@ -14,7 +14,7 @@ pub struct Vecs { impl Vecs { pub(crate) fn forced_import(db: &Database, version: Version) -> Result { Ok(Self { - identity: EagerVec::forced_import(db, "month6", version)?, + identity: EagerVec::forced_import(db, "month6_index", version)?, date: EagerVec::forced_import(db, "date", version)?, first_height: EagerVec::forced_import(db, "month6_first_height", version)?, }) diff --git a/crates/brk_computer/src/indexes/week1.rs b/crates/brk_computer/src/indexes/week1.rs index 4cd7958aa..00a059df3 100644 --- a/crates/brk_computer/src/indexes/week1.rs +++ b/crates/brk_computer/src/indexes/week1.rs @@ -14,7 +14,7 @@ pub struct Vecs { impl Vecs { pub(crate) fn forced_import(db: &Database, version: Version) -> Result { Ok(Self { - identity: EagerVec::forced_import(db, "week1", version)?, + identity: EagerVec::forced_import(db, "week1_index", version)?, date: EagerVec::forced_import(db, "date", version)?, first_height: EagerVec::forced_import(db, "week1_first_height", version)?, }) diff --git a/crates/brk_computer/src/indexes/year1.rs b/crates/brk_computer/src/indexes/year1.rs index c9a734ae5..eaacddca0 100644 --- a/crates/brk_computer/src/indexes/year1.rs +++ b/crates/brk_computer/src/indexes/year1.rs @@ -14,7 +14,7 @@ pub struct Vecs { impl Vecs { pub(crate) fn forced_import(db: &Database, version: Version) -> Result { Ok(Self { - identity: EagerVec::forced_import(db, "year1", version)?, + identity: EagerVec::forced_import(db, "year1_index", version)?, date: EagerVec::forced_import(db, "date", version)?, first_height: EagerVec::forced_import(db, "year1_first_height", version)?, }) diff --git a/crates/brk_computer/src/indexes/year10.rs b/crates/brk_computer/src/indexes/year10.rs index 6629cdc56..36d368e46 100644 --- a/crates/brk_computer/src/indexes/year10.rs +++ b/crates/brk_computer/src/indexes/year10.rs @@ -14,7 +14,7 @@ pub struct Vecs { impl Vecs { pub(crate) fn forced_import(db: &Database, version: Version) -> Result { Ok(Self { - identity: EagerVec::forced_import(db, "year10", version)?, + identity: EagerVec::forced_import(db, "year10_index", version)?, date: EagerVec::forced_import(db, "date", version)?, first_height: EagerVec::forced_import(db, "year10_first_height", version)?, }) diff --git a/crates/brk_types/src/index.rs b/crates/brk_types/src/index.rs index bde68c71e..4523124ab 100644 --- a/crates/brk_types/src/index.rs +++ b/crates/brk_types/src/index.rs @@ -5,8 +5,6 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use vecdb::PrintableIndex; -use crate::PairOutputIndex; - use super::{ Date, Day1, Day3, EmptyAddressIndex, EmptyOutputIndex, Epoch, FundedAddressIndex, Halving, Height, Hour1, Hour4, Hour12, Minute10, Minute30, Month1, Month3, Month6, OpReturnIndex, @@ -39,28 +37,44 @@ pub enum Index { Halving, Epoch, Height, + #[serde(rename = "tx_index")] TxIndex, + #[serde(rename = "txin_index")] TxInIndex, + #[serde(rename = "txout_index")] TxOutIndex, + #[serde(rename = "empty_output_index")] EmptyOutputIndex, + #[serde(rename = "op_return_index")] OpReturnIndex, + #[serde(rename = "p2a_address_index")] P2AAddressIndex, + #[serde(rename = "p2ms_output_index")] P2MSOutputIndex, + #[serde(rename = "p2pk33_address_index")] P2PK33AddressIndex, + #[serde(rename = "p2pk65_address_index")] P2PK65AddressIndex, + #[serde(rename = "p2pkh_address_index")] P2PKHAddressIndex, + #[serde(rename = "p2sh_address_index")] P2SHAddressIndex, + #[serde(rename = "p2tr_address_index")] P2TRAddressIndex, + #[serde(rename = "p2wpkh_address_index")] P2WPKHAddressIndex, + #[serde(rename = "p2wsh_address_index")] P2WSHAddressIndex, + #[serde(rename = "unknown_output_index")] UnknownOutputIndex, + #[serde(rename = "funded_address_index")] FundedAddressIndex, + #[serde(rename = "empty_address_index")] EmptyAddressIndex, - PairOutputIndex, } impl Index { - pub const fn all() -> [Self; 34] { + pub const fn all() -> [Self; 33] { [ Self::Minute10, Self::Minute30, @@ -95,7 +109,6 @@ impl Index { Self::UnknownOutputIndex, Self::FundedAddressIndex, Self::EmptyAddressIndex, - Self::PairOutputIndex, ] } @@ -134,7 +147,6 @@ impl Index { Self::UnknownOutputIndex => UnknownOutputIndex::to_possible_strings(), Self::FundedAddressIndex => FundedAddressIndex::to_possible_strings(), Self::EmptyAddressIndex => EmptyAddressIndex::to_possible_strings(), - Self::PairOutputIndex => PairOutputIndex::to_possible_strings(), } } @@ -180,7 +192,6 @@ impl Index { Self::UnknownOutputIndex => ::to_string(), Self::FundedAddressIndex => ::to_string(), Self::EmptyAddressIndex => ::to_string(), - Self::PairOutputIndex => ::to_string(), } } diff --git a/crates/brk_types/src/lib.rs b/crates/brk_types/src/lib.rs index 2caee43fb..d0c93f80b 100644 --- a/crates/brk_types/src/lib.rs +++ b/crates/brk_types/src/lib.rs @@ -131,7 +131,6 @@ mod p2wsh_address_index; mod p2wsh_bytes; mod pagination; mod pagination_index; -mod pair_output_index; mod percentile; mod pool; mod pool_detail; @@ -327,7 +326,6 @@ pub use p2wsh_address_index::*; pub use p2wsh_bytes::*; pub use pagination::*; pub use pagination_index::*; -pub use pair_output_index::*; pub use percentile::*; pub use pool::*; pub use pool_detail::*; diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index aa2717c79..6185c9cfe 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -426,7 +426,7 @@ * Aggregation dimension for querying metrics. Includes time-based (date, week, month, year), * block-based (height, tx_index), and address/output type indexes. * - * @typedef {("minute10"|"minute30"|"hour1"|"hour4"|"hour12"|"day1"|"day3"|"week1"|"month1"|"month3"|"month6"|"year1"|"year10"|"halving"|"epoch"|"height"|"txindex"|"txinindex"|"txoutindex"|"emptyoutputindex"|"opreturnindex"|"p2aaddressindex"|"p2msoutputindex"|"p2pk33addressindex"|"p2pk65addressindex"|"p2pkhaddressindex"|"p2shaddressindex"|"p2traddressindex"|"p2wpkhaddressindex"|"p2wshaddressindex"|"unknownoutputindex"|"fundedaddressindex"|"emptyaddressindex"|"pairoutputindex")} Index + * @typedef {("minute10"|"minute30"|"hour1"|"hour4"|"hour12"|"day1"|"day3"|"week1"|"month1"|"month3"|"month6"|"year1"|"year10"|"halving"|"epoch"|"height"|"tx_index"|"txin_index"|"txout_index"|"empty_output_index"|"op_return_index"|"p2a_address_index"|"p2ms_output_index"|"p2pk33_address_index"|"p2pk65_address_index"|"p2pkh_address_index"|"p2sh_address_index"|"p2tr_address_index"|"p2wpkh_address_index"|"p2wsh_address_index"|"unknown_output_index"|"funded_address_index"|"empty_address_index")} Index */ /** * Information about an available index and its query aliases @@ -6001,8 +6001,7 @@ class BrkClient extends BrkClientBase { "p2wsh_address_index", "unknown_output_index", "funded_address_index", - "empty_address_index", - "pair_output_index" + "empty_address_index" ]); POOL_ID_TO_POOL_NAME = /** @type {const} */ ({ diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index 635a6c050..2ab30bcf0 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -199,7 +199,7 @@ Year1 = int Year10 = int # Aggregation dimension for querying metrics. Includes time-based (date, week, month, year), # block-based (height, tx_index), and address/output type indexes. -Index = Literal["minute10", "minute30", "hour1", "hour4", "hour12", "day1", "day3", "week1", "month1", "month3", "month6", "year1", "year10", "halving", "epoch", "height", "txindex", "txinindex", "txoutindex", "emptyoutputindex", "opreturnindex", "p2aaddressindex", "p2msoutputindex", "p2pk33addressindex", "p2pk65addressindex", "p2pkhaddressindex", "p2shaddressindex", "p2traddressindex", "p2wpkhaddressindex", "p2wshaddressindex", "unknownoutputindex", "fundedaddressindex", "emptyaddressindex", "pairoutputindex"] +Index = Literal["minute10", "minute30", "hour1", "hour4", "hour12", "day1", "day3", "week1", "month1", "month3", "month6", "year1", "year10", "halving", "epoch", "height", "tx_index", "txin_index", "txout_index", "empty_output_index", "op_return_index", "p2a_address_index", "p2ms_output_index", "p2pk33_address_index", "p2pk65_address_index", "p2pkh_address_index", "p2sh_address_index", "p2tr_address_index", "p2wpkh_address_index", "p2wsh_address_index", "unknown_output_index", "funded_address_index", "empty_address_index"] # Hierarchical tree node for organizing metrics into categories TreeNode = Union[dict[str, "TreeNode"], "MetricLeafWithSchema"] class AddressChainStats(TypedDict): @@ -5322,8 +5322,7 @@ class BrkClient(BrkClientBase): "p2wsh_address_index", "unknown_output_index", "funded_address_index", - "empty_address_index", - "pair_output_index" + "empty_address_index" ] POOL_ID_TO_POOL_NAME = { diff --git a/website/scripts/chart/index.js b/website/scripts/chart/index.js index e4cf16a9c..79f76ee8c 100644 --- a/website/scripts/chart/index.js +++ b/website/scripts/chart/index.js @@ -71,6 +71,8 @@ import { Unit } from "../utils/units.js"; const lineWidth = /** @type {any} */ (1.5); +const MAX_SIZE = 100_000; + /** * @param {Object} args * @param {HTMLElement} args.parent @@ -138,7 +140,7 @@ export function createChart({ parent, brk, fitContent }) { if (cached) { this.data = cached; } - endpoint.slice(-10000).fetch((/** @type {any} */ result) => { + endpoint.slice(-MAX_SIZE).fetch((/** @type {any} */ result) => { if (currentGen !== generation) return; cache.set(endpoint.path, result); this.data = result; @@ -741,7 +743,7 @@ export function createChart({ parent, brk, fitContent }) { valuesVersion = cachedValues.version; tryProcess(); } - await valuesEndpoint.slice(-10000).fetch((result) => { + await valuesEndpoint.slice(-MAX_SIZE).fetch((result) => { cache.set(valuesEndpoint.path, result); valuesData = result.data; valuesStamp = result.stamp; diff --git a/website/scripts/options/cointime.js b/website/scripts/options/cointime.js index 68b8d74ff..7bb765aa3 100644 --- a/website/scripts/options/cointime.js +++ b/website/scripts/options/cointime.js @@ -1,8 +1,8 @@ import { colors } from "../utils/colors.js"; import { brk } from "../client.js"; import { Unit } from "../utils/units.js"; -import { dots, line, price, rollingWindowsTree } from "./series.js"; -import { satsBtcUsd, createPriceRatioCharts } from "./shared.js"; +import { dots, line, baseline, price, rollingWindowsTree } from "./series.js"; +import { satsBtcUsd } from "./shared.js"; /** * Create Cointime section @@ -24,7 +24,7 @@ export function createCointimeSection() { // Reference lines for cap comparisons const capReferenceLines = /** @type {const} */ ([ - { metric: supply.marketCap, name: "Market", color: colors.default }, + { metric: supply.marketCap.usd, name: "Market", color: colors.default }, { metric: all.realized.cap.usd, name: "Realized", @@ -153,24 +153,53 @@ export function createCointimeSection() { ), ], }, - ...prices.map(({ pattern, name, color }) => ({ - name, - tree: createPriceRatioCharts({ - context: `${name} Price`, - legend: name, - pricePattern: pattern, - ratio: pattern, - color, - priceReferences: [ - price({ - metric: all.realized.price, - name: "Realized", - color: colors.realized, - defaultActive: false, - }), + ...prices.map(({ pattern, name, color }) => { + const p = pattern.percentiles; + const pctUsd = /** @type {const} */ ([ + { name: "pct95", prop: p.pct95.price, color: colors.ratioPct._95 }, + { name: "pct5", prop: p.pct5.price, color: colors.ratioPct._5 }, + { name: "pct99", prop: p.pct99.price, color: colors.ratioPct._99 }, + { name: "pct1", prop: p.pct1.price, color: colors.ratioPct._1 }, + ]); + const pctRatio = /** @type {const} */ ([ + { name: "pct95", prop: p.pct95.ratio, color: colors.ratioPct._95 }, + { name: "pct5", prop: p.pct5.ratio, color: colors.ratioPct._5 }, + { name: "pct99", prop: p.pct99.ratio, color: colors.ratioPct._99 }, + { name: "pct1", prop: p.pct1.ratio, color: colors.ratioPct._1 }, + ]); + return { + name, + tree: [ + { + name: "Price", + title: `${name} Price`, + top: [ + price({ metric: pattern, name, color }), + price({ + metric: all.realized.price, + name: "Realized", + color: colors.realized, + defaultActive: false, + }), + ...pctUsd.map(({ name: pName, prop, color: pColor }) => + price({ metric: prop, name: pName, color: pColor, defaultActive: false, options: { lineStyle: 1 } }), + ), + ], + }, + { + name: "Ratio", + title: `${name} Price Ratio`, + top: [price({ metric: pattern, name, color })], + bottom: [ + baseline({ metric: pattern.ratio, name: "Ratio", unit: Unit.ratio, base: 1 }), + ...pctRatio.map(({ name: pName, prop, color: pColor }) => + line({ metric: prop, name: pName, color: pColor, defaultActive: false, unit: Unit.ratio, options: { lineStyle: 1 } }), + ), + ], + }, ], - }), - })), + }; + }), ], }, diff --git a/website/scripts/options/constants.js b/website/scripts/options/constants.js index cc76224ed..9a23c70b1 100644 --- a/website/scripts/options/constants.js +++ b/website/scripts/options/constants.js @@ -6,7 +6,7 @@ import { line } from "./series.js"; /** * Get constant pattern by number dynamically from tree - * Examples: 0 → constant0, 38.2 → constant382, -1 → constantMinus1 + * Examples: 0 → _0, 38.2 → _382, -1 → minus1 * @param {BrkClient["metrics"]["constants"]} constants * @param {number} num * @returns {AnyMetricPattern} @@ -14,8 +14,8 @@ import { line } from "./series.js"; export function getConstant(constants, num) { const key = num >= 0 - ? `constant${String(num).replace(".", "")}` - : `constantMinus${Math.abs(num)}`; + ? `_${String(num).replace(".", "")}` + : `minus${Math.abs(num)}`; const constant = /** @type {AnyMetricPattern | undefined} */ ( /** @type {Record} */ (constants)[key] ); diff --git a/website/scripts/options/distribution/activity.js b/website/scripts/options/distribution/activity.js index 2d0f6c5e5..45314e3b0 100644 --- a/website/scripts/options/distribution/activity.js +++ b/website/scripts/options/distribution/activity.js @@ -1,330 +1,202 @@ /** * 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 + * Capabilities by cohort type: + * - All/STH: activity (full), SOPR (rolling + adjusted), sell side risk, value (flows + breakdown), coins + * - LTH: activity (full), SOPR (rolling), sell side risk, value (flows + breakdown), coins + * - AgeRange/MaxAge: activity (basic), SOPR (24h only), value (no flows/breakdown), coins + * - Others (UtxoAmount, Empty, Address): no activity, value only */ import { Unit } from "../../utils/units.js"; import { line, baseline, dotsBaseline, dots } from "../series.js"; import { - satsBtcUsd, mapCohortsWithAll, flatMapCohortsWithAll, } from "../shared.js"; import { colors } from "../../utils/colors.js"; // ============================================================================ -// Shared Helpers +// Shared Volume Helpers // ============================================================================ /** - * Create grouped SOPR chart entries (Raw, 7d EMA, 30d EMA) - * @template {{ color: Color, name: string }} T - * @param {readonly T[]} list - * @param {T} all - * @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, - all, - getSopr, - getSopr7d, - getSopr30d, - title, - titlePrefix, -) { - return [ - { - name: "Raw", - title: title(`${titlePrefix}SOPR`), - bottom: mapCohortsWithAll(list, all, (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: mapCohortsWithAll(list, all, (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: mapCohortsWithAll(list, all, (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 {T} all + * @param {{ sent: Brk.BaseCumulativeInSumPattern, coindaysDestroyed: Brk.BaseCumulativeSumPattern }} activity + * @param {Color} color * @param {(metric: string) => string} title * @returns {PartialOptionsTree} */ -function valueBreakdownTree(list, all, title) { +function volumeAndCoinsTree(activity, color, title) { return [ { - name: "Profit", + name: "Volume", tree: [ { - name: "Created", - title: title("Profit Value Created"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + name: "Sum", + title: title("Sent Volume"), + bottom: [ line({ - metric: tree.realized.profitValueCreated, - name, - color, - unit: Unit.usd, + metric: activity.sent.sum._24h, + name: "24h", + color: colors.indicator.main, + unit: Unit.sats, + defaultActive: false, }), - ), + line({ + metric: activity.sent.base, + name: "Sum", + color, + unit: Unit.sats, + }), + ], }, { - name: "Destroyed", - title: title("Profit Value Destroyed"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + name: "Cumulative", + title: title("Sent Volume (Total)"), + bottom: [ line({ - metric: tree.realized.profitValueDestroyed, - name, + metric: activity.sent.cumulative, + name: "All-time", color, - unit: Unit.usd, + unit: Unit.sats, }), - ), + ], }, ], }, { - name: "Loss", + name: "Coins Destroyed", tree: [ { - name: "Created", - title: title("Loss Value Created"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + name: "Sum", + title: title("Coindays Destroyed"), + bottom: [ line({ - metric: tree.realized.lossValueCreated, - name, + metric: activity.coindaysDestroyed.sum._24h, + name: "24h", color, - unit: Unit.usd, + unit: Unit.coindays, }), - ), + ], }, { - name: "Destroyed", - title: title("Loss Value Destroyed"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + name: "Cumulative", + title: title("Cumulative Coindays Destroyed"), + bottom: [ line({ - metric: tree.realized.lossValueDestroyed, - name, + metric: activity.coindaysDestroyed.cumulative, + name: "All-time", color, - unit: Unit.usd, + unit: Unit.coindays, }), - ), + ], }, ], }, ]; } -/** - * 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 {T} all - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function coinsDestroyedTree(list, all, title) { - return [ - { - name: "Sum", - title: title("Coins Destroyed"), - bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [ - line({ - metric: tree.activity.coinblocksDestroyed.sum._24h, - name, - color, - unit: Unit.coinblocks, - }), - line({ - metric: tree.activity.coindaysDestroyed.sum._24h, - name, - color, - unit: Unit.coindays, - }), - ]), - }, - { - name: "Cumulative", - title: title("Cumulative Coins Destroyed"), - bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [ - line({ - metric: tree.activity.coinblocksDestroyed.cumulative, - name, - color, - unit: Unit.coinblocks, - }), - line({ - metric: tree.activity.coindaysDestroyed.cumulative, - name, - color, - unit: Unit.coindays, - }), - ]), - }, - ]; -} - // ============================================================================ -// Rolling Helpers +// Shared SOPR Helpers // ============================================================================ /** - * Rolling SOPR tree for single cohort - * @param {Object} m - * @param {AnyMetricPattern} m.s24h - * @param {AnyMetricPattern} m.s7d - * @param {AnyMetricPattern} m.s30d - * @param {AnyMetricPattern} m.s1y - * @param {AnyMetricPattern} m.ema24h7d - * @param {AnyMetricPattern} m.ema24h30d + * @param {Brk._1m1w1y24hPattern} ratio * @param {(metric: string) => string} title - * @param {string} prefix + * @param {string} [prefix] * @returns {PartialOptionsTree} */ -function singleRollingSoprTree(m, title, prefix = "") { +function singleRollingSoprTree(ratio, title, prefix = "") { return [ { name: "Compare", title: title(`Rolling ${prefix}SOPR`), bottom: [ - baseline({ metric: m.s24h, name: "24h", color: colors.time._24h, unit: Unit.ratio, base: 1 }), - baseline({ metric: m.s7d, name: "7d", color: colors.time._1w, unit: Unit.ratio, base: 1 }), - baseline({ metric: m.s30d, name: "30d", color: colors.time._1m, unit: Unit.ratio, base: 1 }), - baseline({ metric: m.s1y, name: "1y", color: colors.time._1y, unit: Unit.ratio, base: 1 }), + baseline({ metric: ratio._24h, name: "24h", color: colors.time._24h, unit: Unit.ratio, base: 1 }), + baseline({ metric: ratio._1w, name: "7d", color: colors.time._1w, unit: Unit.ratio, base: 1 }), + baseline({ metric: ratio._1m, name: "30d", color: colors.time._1m, unit: Unit.ratio, base: 1 }), + baseline({ metric: ratio._1y, name: "1y", color: colors.time._1y, unit: Unit.ratio, base: 1 }), ], }, { name: "24h", title: title(`${prefix}SOPR (24h)`), - bottom: [ - baseline({ metric: m.ema24h30d, name: "30d EMA", color: colors.bi.p3, unit: Unit.ratio, base: 1 }), - baseline({ metric: m.ema24h7d, name: "7d EMA", color: colors.bi.p2, unit: Unit.ratio, base: 1 }), - dotsBaseline({ metric: m.s24h, name: "24h", color: colors.bi.p1, unit: Unit.ratio, base: 1 }), - ], + bottom: [dotsBaseline({ metric: ratio._24h, name: "24h", unit: Unit.ratio, base: 1 })], }, { name: "7d", title: title(`${prefix}SOPR (7d)`), - bottom: [baseline({ metric: m.s7d, name: "SOPR", unit: Unit.ratio, base: 1 })], + bottom: [baseline({ metric: ratio._1w, name: "SOPR", unit: Unit.ratio, base: 1 })], }, { name: "30d", title: title(`${prefix}SOPR (30d)`), - bottom: [baseline({ metric: m.s30d, name: "SOPR", unit: Unit.ratio, base: 1 })], + bottom: [baseline({ metric: ratio._1m, name: "SOPR", unit: Unit.ratio, base: 1 })], }, { name: "1y", title: title(`${prefix}SOPR (1y)`), - bottom: [baseline({ metric: m.s1y, name: "SOPR", unit: Unit.ratio, base: 1 })], + bottom: [baseline({ metric: ratio._1y, name: "SOPR", unit: Unit.ratio, base: 1 })], }, ]; } +// ============================================================================ +// Shared Sell Side Risk Helpers +// ============================================================================ + /** - * Rolling sell side risk tree for single cohort - * @param {AnyRealizedPattern} r + * @param {Brk._1m1w1y24hPattern6} sellSideRisk * @param {(metric: string) => string} title * @returns {PartialOptionsTree} */ -function singleRollingSellSideRiskTree(r, title) { +function singleSellSideRiskTree(sellSideRisk, title) { return [ { name: "Compare", title: title("Rolling Sell Side Risk"), bottom: [ - line({ metric: r.sellSideRiskRatio24h, name: "24h", color: colors.time._24h, unit: Unit.ratio }), - line({ metric: r.sellSideRiskRatio7d, name: "7d", color: colors.time._1w, unit: Unit.ratio }), - line({ metric: r.sellSideRiskRatio30d, name: "30d", color: colors.time._1m, unit: Unit.ratio }), - line({ metric: r.sellSideRiskRatio1y, name: "1y", color: colors.time._1y, unit: Unit.ratio }), + line({ metric: sellSideRisk._24h.ratio, name: "24h", color: colors.time._24h, unit: Unit.ratio }), + line({ metric: sellSideRisk._1w.ratio, name: "7d", color: colors.time._1w, unit: Unit.ratio }), + line({ metric: sellSideRisk._1m.ratio, name: "30d", color: colors.time._1m, unit: Unit.ratio }), + line({ metric: sellSideRisk._1y.ratio, name: "1y", color: colors.time._1y, unit: Unit.ratio }), ], }, { name: "24h", title: title("Sell Side Risk (24h)"), - bottom: [ - line({ metric: r.sellSideRiskRatio24h30dEma, name: "30d EMA", color: colors.time._1m, unit: Unit.ratio }), - line({ metric: r.sellSideRiskRatio24h7dEma, name: "7d EMA", color: colors.time._1w, unit: Unit.ratio }), - dots({ metric: r.sellSideRiskRatio24h, name: "Raw", color: colors.bitcoin, unit: Unit.ratio }), - ], + bottom: [dots({ metric: sellSideRisk._24h.ratio, name: "Raw", color: colors.bitcoin, unit: Unit.ratio })], }, { name: "7d", title: title("Sell Side Risk (7d)"), - bottom: [line({ metric: r.sellSideRiskRatio7d, name: "Risk", unit: Unit.ratio })], + bottom: [line({ metric: sellSideRisk._1w.ratio, name: "Risk", unit: Unit.ratio })], }, { name: "30d", title: title("Sell Side Risk (30d)"), - bottom: [line({ metric: r.sellSideRiskRatio30d, name: "Risk", unit: Unit.ratio })], + bottom: [line({ metric: sellSideRisk._1m.ratio, name: "Risk", unit: Unit.ratio })], }, { name: "1y", title: title("Sell Side Risk (1y)"), - bottom: [line({ metric: r.sellSideRiskRatio1y, name: "Risk", unit: Unit.ratio })], + bottom: [line({ metric: sellSideRisk._1y.ratio, name: "Risk", unit: Unit.ratio })], }, ]; } +// ============================================================================ +// Shared Value Helpers +// ============================================================================ + /** - * Rolling value created/destroyed tree for single cohort - * @param {Object} m - * @param {AnyMetricPattern} m.created24h - * @param {AnyMetricPattern} m.created7d - * @param {AnyMetricPattern} m.created30d - * @param {AnyMetricPattern} m.created1y - * @param {AnyMetricPattern} m.destroyed24h - * @param {AnyMetricPattern} m.destroyed7d - * @param {AnyMetricPattern} m.destroyed30d - * @param {AnyMetricPattern} m.destroyed1y + * @param {Brk.BaseCumulativeSumPattern} valueCreated + * @param {Brk.BaseCumulativeSumPattern} valueDestroyed * @param {(metric: string) => string} title - * @param {string} prefix + * @param {string} [prefix] * @returns {PartialOptionsTree} */ -function singleRollingValueTree(m, title, prefix = "") { +function singleRollingValueTree(valueCreated, valueDestroyed, title, prefix = "") { return [ { name: "Compare", @@ -333,20 +205,20 @@ function singleRollingValueTree(m, title, prefix = "") { name: "Created", title: title(`Rolling ${prefix}Value Created`), bottom: [ - line({ metric: m.created24h, name: "24h", color: colors.time._24h, unit: Unit.usd }), - line({ metric: m.created7d, name: "7d", color: colors.time._1w, unit: Unit.usd }), - line({ metric: m.created30d, name: "30d", color: colors.time._1m, unit: Unit.usd }), - line({ metric: m.created1y, name: "1y", color: colors.time._1y, unit: Unit.usd }), + line({ metric: valueCreated.sum._24h, name: "24h", color: colors.time._24h, unit: Unit.usd }), + line({ metric: valueCreated.sum._1w, name: "7d", color: colors.time._1w, unit: Unit.usd }), + line({ metric: valueCreated.sum._1m, name: "30d", color: colors.time._1m, unit: Unit.usd }), + line({ metric: valueCreated.sum._1y, name: "1y", color: colors.time._1y, unit: Unit.usd }), ], }, { name: "Destroyed", title: title(`Rolling ${prefix}Value Destroyed`), bottom: [ - line({ metric: m.destroyed24h, name: "24h", color: colors.time._24h, unit: Unit.usd }), - line({ metric: m.destroyed7d, name: "7d", color: colors.time._1w, unit: Unit.usd }), - line({ metric: m.destroyed30d, name: "30d", color: colors.time._1m, unit: Unit.usd }), - line({ metric: m.destroyed1y, name: "1y", color: colors.time._1y, unit: Unit.usd }), + line({ metric: valueDestroyed.sum._24h, name: "24h", color: colors.time._24h, unit: Unit.usd }), + line({ metric: valueDestroyed.sum._1w, name: "7d", color: colors.time._1w, unit: Unit.usd }), + line({ metric: valueDestroyed.sum._1m, name: "30d", color: colors.time._1m, unit: Unit.usd }), + line({ metric: valueDestroyed.sum._1y, name: "1y", color: colors.time._1y, unit: Unit.usd }), ], }, ], @@ -355,48 +227,299 @@ function singleRollingValueTree(m, title, prefix = "") { name: "24h", title: title(`${prefix}Value Created & Destroyed (24h)`), bottom: [ - line({ metric: m.created24h, name: "Created", color: colors.usd, unit: Unit.usd }), - line({ metric: m.destroyed24h, name: "Destroyed", color: colors.loss, unit: Unit.usd }), + line({ metric: valueCreated.sum._24h, name: "Created", color: colors.usd, unit: Unit.usd }), + line({ metric: valueDestroyed.sum._24h, name: "Destroyed", color: colors.loss, unit: Unit.usd }), ], }, { name: "7d", title: title(`${prefix}Value Created & Destroyed (7d)`), bottom: [ - line({ metric: m.created7d, name: "Created", color: colors.usd, unit: Unit.usd }), - line({ metric: m.destroyed7d, name: "Destroyed", color: colors.loss, unit: Unit.usd }), + line({ metric: valueCreated.sum._1w, name: "Created", color: colors.usd, unit: Unit.usd }), + line({ metric: valueDestroyed.sum._1w, name: "Destroyed", color: colors.loss, unit: Unit.usd }), ], }, { name: "30d", title: title(`${prefix}Value Created & Destroyed (30d)`), bottom: [ - line({ metric: m.created30d, name: "Created", color: colors.usd, unit: Unit.usd }), - line({ metric: m.destroyed30d, name: "Destroyed", color: colors.loss, unit: Unit.usd }), + line({ metric: valueCreated.sum._1m, name: "Created", color: colors.usd, unit: Unit.usd }), + line({ metric: valueDestroyed.sum._1m, name: "Destroyed", color: colors.loss, unit: Unit.usd }), ], }, { name: "1y", title: title(`${prefix}Value Created & Destroyed (1y)`), bottom: [ - line({ metric: m.created1y, name: "Created", color: colors.usd, unit: Unit.usd }), - line({ metric: m.destroyed1y, name: "Destroyed", color: colors.loss, unit: Unit.usd }), + line({ metric: valueCreated.sum._1y, name: "Created", color: colors.usd, unit: Unit.usd }), + line({ metric: valueDestroyed.sum._1y, name: "Destroyed", color: colors.loss, unit: Unit.usd }), ], }, ]; } /** - * Rolling SOPR charts for grouped cohorts - * @template {{ color: Color, name: string }} T - * @param {readonly T[]} list - * @param {T} all - * @param {(item: T) => AnyMetricPattern} get24h - * @param {(item: T) => AnyMetricPattern} get7d - * @param {(item: T) => AnyMetricPattern} get30d - * @param {(item: T) => AnyMetricPattern} get1y + * Value section for cohorts with full realized (flows + breakdown) + * @param {Brk.BaseCumulativeDistributionRelSumValuePattern} profit + * @param {Brk.BaseCapitulationCumulativeNegativeRelSumValuePattern} loss + * @param {Brk.BaseCumulativeSumPattern} valueCreated + * @param {Brk.BaseCumulativeSumPattern} valueDestroyed + * @param {AnyFetchedSeriesBlueprint[]} extraValueMetrics + * @param {PartialOptionsTree} rollingTree * @param {(metric: string) => string} title - * @param {string} prefix + * @returns {PartialOptionsGroup} + */ +function fullValueSection(profit, loss, valueCreated, valueDestroyed, extraValueMetrics, rollingTree, title) { + return { + name: "Value", + tree: [ + { + name: "Flows", + title: title("Profit & Capitulation Flows"), + bottom: [ + line({ metric: profit.distributionFlow, name: "Distribution Flow", color: colors.profit, unit: Unit.usd }), + line({ metric: loss.capitulationFlow, name: "Capitulation Flow", color: colors.loss, unit: Unit.usd }), + ], + }, + { + name: "Created & Destroyed", + title: title("Value Created & Destroyed"), + bottom: [ + line({ metric: valueCreated.base, name: "Created", color: colors.usd, unit: Unit.usd }), + line({ metric: valueDestroyed.base, name: "Destroyed", color: colors.loss, unit: Unit.usd }), + ...extraValueMetrics, + ], + }, + { + name: "Breakdown", + tree: [ + { + name: "Profit", + title: title("Profit Value Created & Destroyed"), + bottom: [ + line({ metric: profit.valueCreated.base, name: "Created", color: colors.profit, unit: Unit.usd }), + line({ metric: profit.valueDestroyed.base, name: "Destroyed", color: colors.loss, unit: Unit.usd }), + ], + }, + { + name: "Loss", + title: title("Loss Value Created & Destroyed"), + bottom: [ + line({ metric: loss.valueCreated.base, name: "Created", color: colors.profit, unit: Unit.usd }), + line({ metric: loss.valueDestroyed.base, name: "Destroyed", color: colors.loss, unit: Unit.usd }), + ], + }, + ], + }, + { name: "Rolling", tree: rollingTree }, + ], + }; +} + +/** + * Simple value section (created & destroyed + rolling) + * @param {Brk.BaseCumulativeSumPattern} valueCreated + * @param {Brk.BaseCumulativeSumPattern} valueDestroyed + * @param {(metric: string) => string} title + * @returns {PartialOptionsGroup} + */ +function simpleValueSection(valueCreated, valueDestroyed, title) { + return { + name: "Value", + tree: [ + { + name: "Created & Destroyed", + title: title("Value Created & Destroyed"), + bottom: [ + line({ metric: valueCreated.base, name: "Created", color: colors.usd, unit: Unit.usd }), + line({ metric: valueDestroyed.base, name: "Destroyed", color: colors.loss, unit: Unit.usd }), + ], + }, + { + name: "Rolling", + tree: singleRollingValueTree(valueCreated, valueDestroyed, title), + }, + ], + }; +} + +// ============================================================================ +// Single Cohort Activity Sections +// ============================================================================ + +/** + * Full activity with adjusted SOPR (All/STH) + * @param {{ cohort: CohortAll | CohortFull, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createActivitySectionWithAdjusted({ cohort, title }) { + const { tree, color } = cohort; + const r = tree.realized; + const sopr = r.sopr; + + return { + name: "Activity", + tree: [ + ...volumeAndCoinsTree(tree.activity, color, title), + { + name: "SOPR", + tree: [ + { + name: "Normal", + tree: singleRollingSoprTree(sopr.ratio, title), + }, + { + name: "Adjusted", + tree: singleRollingSoprTree(sopr.adjusted.ratio, title, "Adjusted "), + }, + ], + }, + { name: "Sell Side Risk", tree: singleSellSideRiskTree(r.sellSideRiskRatio, title) }, + fullValueSection( + r.profit, r.loss, + sopr.valueCreated, sopr.valueDestroyed, + [ + line({ metric: sopr.adjusted.valueCreated.base, name: "Adjusted Created", color: colors.adjustedCreated, unit: Unit.usd, defaultActive: false }), + line({ metric: sopr.adjusted.valueDestroyed.base, name: "Adjusted Destroyed", color: colors.adjustedDestroyed, unit: Unit.usd, defaultActive: false }), + ], + [ + { + name: "Normal", + tree: singleRollingValueTree(sopr.valueCreated, sopr.valueDestroyed, title), + }, + { + name: "Adjusted", + tree: singleRollingValueTree(sopr.adjusted.valueCreated, sopr.adjusted.valueDestroyed, title, "Adjusted "), + }, + ], + title, + ), + ], + }; +} + +/** + * Activity section for cohorts with rolling SOPR + sell side risk (LTH, also CohortFull | CohortLongTerm) + * @param {{ cohort: CohortFull | CohortLongTerm, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createActivitySection({ cohort, title }) { + const { tree, color } = cohort; + const r = tree.realized; + const sopr = r.sopr; + + return { + name: "Activity", + tree: [ + ...volumeAndCoinsTree(tree.activity, color, title), + { + name: "SOPR", + tree: singleRollingSoprTree(sopr.ratio, title), + }, + { name: "Sell Side Risk", tree: singleSellSideRiskTree(r.sellSideRiskRatio, title) }, + fullValueSection( + r.profit, r.loss, + sopr.valueCreated, sopr.valueDestroyed, + [], + singleRollingValueTree(sopr.valueCreated, sopr.valueDestroyed, title), + title, + ), + ], + }; +} + +/** + * Activity section for cohorts with activity but basic realized (AgeRange/MaxAge — 24h SOPR only) + * @param {{ cohort: CohortAgeRange | CohortWithAdjusted, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createActivitySectionWithActivity({ cohort, title }) { + const { tree, color } = cohort; + const sopr = tree.realized.sopr; + + return { + name: "Activity", + tree: [ + ...volumeAndCoinsTree(tree.activity, color, title), + { + name: "SOPR", + title: title("SOPR (24h)"), + bottom: [dotsBaseline({ metric: sopr.ratio._24h, name: "SOPR", unit: Unit.ratio, base: 1 })], + }, + simpleValueSection(sopr.valueCreated, sopr.valueDestroyed, title), + ], + }; +} + +/** + * Minimal activity section for cohorts without activity field (value only) + * @param {{ cohort: CohortBasicWithMarketCap | CohortBasicWithoutMarketCap | CohortWithoutRelative | CohortAddress | AddressCohortObject, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createActivitySectionMinimal({ cohort, title }) { + const sopr = cohort.tree.realized.sopr; + + return { + name: "Activity", + tree: [ + simpleValueSection(sopr.valueCreated, sopr.valueDestroyed, title), + ], + }; +} + +// ============================================================================ +// Grouped SOPR Helpers +// ============================================================================ + +/** + * @template {{ color: Color, name: string }} T + * @template {{ color: Color, name: string }} A + * @param {readonly T[]} list + * @param {A} all + * @param {(item: T | A) => AnyMetricPattern} getRaw + * @param {(item: T | A) => AnyMetricPattern} get7d + * @param {(item: T | A) => AnyMetricPattern} get30d + * @param {(metric: string) => string} title + * @param {string} [prefix] + * @returns {PartialOptionsTree} + */ +function groupedSoprCharts(list, all, getRaw, get7d, get30d, title, prefix = "") { + return [ + { + name: "Raw", + title: title(`${prefix}SOPR`), + bottom: mapCohortsWithAll(list, all, (item) => + baseline({ metric: getRaw(item), name: item.name, color: item.color, unit: Unit.ratio, base: 1 }), + ), + }, + { + name: "7d", + title: title(`${prefix}SOPR (7d)`), + bottom: mapCohortsWithAll(list, all, (item) => + baseline({ metric: get7d(item), name: item.name, color: item.color, unit: Unit.ratio, base: 1 }), + ), + }, + { + name: "30d", + title: title(`${prefix}SOPR (30d)`), + bottom: mapCohortsWithAll(list, all, (item) => + baseline({ metric: get30d(item), name: item.name, color: item.color, unit: Unit.ratio, base: 1 }), + ), + }, + ]; +} + +/** + * @template {{ color: Color, name: string }} T + * @template {{ color: Color, name: string }} A + * @param {readonly T[]} list + * @param {A} all + * @param {(item: T | A) => AnyMetricPattern} get24h + * @param {(item: T | A) => AnyMetricPattern} get7d + * @param {(item: T | A) => AnyMetricPattern} get30d + * @param {(item: T | A) => AnyMetricPattern} get1y + * @param {(metric: string) => string} title + * @param {string} [prefix] * @returns {PartialOptionsTree} */ function groupedRollingSoprCharts(list, all, get24h, get7d, get30d, get1y, title, prefix = "") { @@ -432,54 +555,18 @@ function groupedRollingSoprCharts(list, all, get24h, get7d, get30d, get1y, title ]; } -/** - * Rolling sell side risk charts for grouped cohorts - * @param {readonly CohortObject[]} list - * @param {CohortObject} all - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function groupedRollingSellSideRiskCharts(list, all, title) { - return [ - { - name: "24h", - title: title("Sell Side Risk (24h)"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ metric: tree.realized.sellSideRiskRatio24h, name, color, unit: Unit.ratio }), - ), - }, - { - name: "7d", - title: title("Sell Side Risk (7d)"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ metric: tree.realized.sellSideRiskRatio7d, name, color, unit: Unit.ratio }), - ), - }, - { - name: "30d", - title: title("Sell Side Risk (30d)"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ metric: tree.realized.sellSideRiskRatio30d, name, color, unit: Unit.ratio }), - ), - }, - { - name: "1y", - title: title("Sell Side Risk (1y)"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ metric: tree.realized.sellSideRiskRatio1y, name, color, unit: Unit.ratio }), - ), - }, - ]; -} +// ============================================================================ +// Grouped Value/Flow Helpers +// ============================================================================ /** - * Rolling value created/destroyed charts for grouped cohorts * @template {{ color: Color, name: string }} T + * @template {{ color: Color, name: string }} A * @param {readonly T[]} list - * @param {T} all - * @param {readonly { name: string, getCreated: (item: T) => AnyMetricPattern, getDestroyed: (item: T) => AnyMetricPattern }[]} windows + * @param {A} all + * @param {readonly { name: string, getCreated: (item: T | A) => AnyMetricPattern, getDestroyed: (item: T | A) => AnyMetricPattern }[]} windows * @param {(metric: string) => string} title - * @param {string} prefix + * @param {string} [prefix] * @returns {PartialOptionsTree} */ function groupedRollingValueCharts(list, all, windows, title, prefix = "") { @@ -507,544 +594,182 @@ function groupedRollingValueCharts(list, all, windows, title, prefix = "") { ]; } -// ============================================================================ -// SOPR Helpers -// ============================================================================ - /** - * 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 r = cohort.tree.realized; - return [ - { - name: "Normal", - tree: singleRollingSoprTree( - { s24h: r.sopr24h, s7d: r.sopr7d, s30d: r.sopr30d, s1y: r.sopr1y, ema24h7d: r.sopr24h7dEma, ema24h30d: r.sopr24h30dEma }, - title, - ), - }, - { - name: "Adjusted", - tree: singleRollingSoprTree( - { s24h: r.adjustedSopr24h, s7d: r.adjustedSopr7d, s30d: r.adjustedSopr30d, s1y: r.adjustedSopr1y, ema24h7d: r.adjustedSopr24h7dEma, ema24h30d: r.adjustedSopr24h30dEma }, - title, - "Adjusted ", - ), - }, - ]; -} - -/** - * Create grouped SOPR tree with separate charts for each variant - * @param {readonly (UtxoCohortObject | CohortWithoutRelative)[]} list + * @param {readonly (CohortFull | CohortLongTerm | CohortAll)[]} list * @param {CohortAll} all - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} */ -function createGroupedSoprTree(list, all, title) { - return groupedSoprCharts( - list, - all, - (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 {CohortAll | CohortFull | CohortWithAdjusted} all - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function createGroupedSoprTreeWithAdjusted(list, all, title) { +function valueWindows(list, all) { return [ - { - name: "Normal", - tree: [ - ...groupedSoprCharts( - list, - all, - (c) => c.tree.realized.sopr, - (c) => c.tree.realized.sopr7dEma, - (c) => c.tree.realized.sopr30dEma, - title, - "", - ), - { - name: "Rolling", - tree: groupedRollingSoprCharts( - list, - all, - (c) => c.tree.realized.sopr24h, - (c) => c.tree.realized.sopr7d, - (c) => c.tree.realized.sopr30d, - (c) => c.tree.realized.sopr1y, - title, - ), - }, - ], - }, - { - name: "Adjusted", - tree: [ - ...groupedSoprCharts( - list, - all, - (c) => c.tree.realized.adjustedSopr, - (c) => c.tree.realized.adjustedSopr7dEma, - (c) => c.tree.realized.adjustedSopr30dEma, - title, - "Adjusted ", - ), - { - name: "Rolling", - tree: groupedRollingSoprCharts( - list, - all, - (c) => c.tree.realized.adjustedSopr24h, - (c) => c.tree.realized.adjustedSopr7d, - (c) => c.tree.realized.adjustedSopr30d, - (c) => c.tree.realized.adjustedSopr1y, - title, - "Adjusted ", - ), - }, - ], - }, + { name: "24h", getCreated: (/** @type {typeof list[number] | typeof all} */ c) => c.tree.realized.sopr.valueCreated.sum._24h, getDestroyed: (/** @type {typeof list[number] | typeof all} */ c) => c.tree.realized.sopr.valueDestroyed.sum._24h }, + { name: "7d", getCreated: (/** @type {typeof list[number] | typeof all} */ c) => c.tree.realized.sopr.valueCreated.sum._1w, getDestroyed: (/** @type {typeof list[number] | typeof all} */ c) => c.tree.realized.sopr.valueDestroyed.sum._1w }, + { name: "30d", getCreated: (/** @type {typeof list[number] | typeof all} */ c) => c.tree.realized.sopr.valueCreated.sum._1m, getDestroyed: (/** @type {typeof list[number] | typeof all} */ c) => c.tree.realized.sopr.valueDestroyed.sum._1m }, + { name: "1y", getCreated: (/** @type {typeof list[number] | typeof all} */ c) => c.tree.realized.sopr.valueCreated.sum._1y, getDestroyed: (/** @type {typeof list[number] | typeof all} */ c) => c.tree.realized.sopr.valueDestroyed.sum._1y }, ]; } // ============================================================================ -// Single Cohort Activity Section +// Grouped Activity Sections // ============================================================================ /** - * 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 - * @param {PartialOptionsTree} [args.valueRollingTree] - Optional value rolling tree override + * @param {{ list: readonly CohortFull[], all: CohortAll, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ -export function createActivitySection({ - cohort, - title, - valueMetrics = [], - soprTree, - valueRollingTree, -}) { - const { tree, color } = cohort; - +export function createGroupedActivitySectionWithAdjusted({ list, all, title }) { return { name: "Activity", tree: [ { name: "Volume", + title: title("Sent Volume"), + bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [ + line({ metric: tree.activity.sent.sum._24h, name, color, unit: Unit.sats }), + ]), + }, + { + name: "SOPR", tree: [ { - name: "Sum", - title: title("Sent Volume"), - bottom: [ - line({ - metric: tree.activity.sent14dEma.sats, - name: "14d EMA", - color: colors.indicator.main, - unit: Unit.sats, - defaultActive: false, - }), - line({ - metric: tree.activity.sent14dEma.btc, - name: "14d EMA", - color: colors.indicator.main, - unit: Unit.btc, - defaultActive: false, - }), - line({ - metric: tree.activity.sent14dEma.usd, - name: "14d EMA", - color: colors.indicator.main, - unit: Unit.usd, - defaultActive: false, - }), - line({ - metric: tree.activity.sent.base.sats, - name: "sum", - color, - unit: Unit.sats, - }), - line({ - metric: tree.activity.sent.base.btc, - name: "sum", - color, - unit: Unit.btc, - }), - line({ - metric: tree.activity.sent.base.usd, - name: "sum", - color, - unit: Unit.usd, - }), + name: "Normal", + tree: [ + ...groupedSoprCharts( + list, all, + (c) => c.tree.realized.sopr.ratio._24h, + (c) => c.tree.realized.sopr.ratio._1w, + (c) => c.tree.realized.sopr.ratio._1m, + title, + ), + { + name: "Rolling", + tree: groupedRollingSoprCharts( + list, all, + (c) => c.tree.realized.sopr.ratio._24h, + (c) => c.tree.realized.sopr.ratio._1w, + (c) => c.tree.realized.sopr.ratio._1m, + (c) => c.tree.realized.sopr.ratio._1y, + title, + ), + }, ], }, { - name: "Cumulative", - title: title("Sent Volume (Total)"), - bottom: [ - line({ - metric: tree.activity.sent.cumulative.sats, - name: "all-time", - color, - unit: Unit.sats, - }), - line({ - metric: tree.activity.sent.cumulative.btc, - name: "all-time", - color, - unit: Unit.btc, - }), - line({ - metric: tree.activity.sent.cumulative.usd, - name: "all-time", - color, - unit: Unit.usd, - }), + name: "Adjusted", + tree: [ + ...groupedSoprCharts( + list, all, + (c) => c.tree.realized.sopr.adjusted.ratio._24h, + (c) => c.tree.realized.sopr.adjusted.ratio._1w, + (c) => c.tree.realized.sopr.adjusted.ratio._1m, + title, + "Adjusted ", + ), + { + name: "Rolling", + tree: groupedRollingSoprCharts( + list, all, + (c) => c.tree.realized.sopr.adjusted.ratio._24h, + (c) => c.tree.realized.sopr.adjusted.ratio._1w, + (c) => c.tree.realized.sopr.adjusted.ratio._1m, + (c) => c.tree.realized.sopr.adjusted.ratio._1y, + title, + "Adjusted ", + ), + }, ], }, ], }, - { - name: "SOPR", - tree: - soprTree ?? - singleRollingSoprTree( - { s24h: tree.realized.sopr24h, s7d: tree.realized.sopr7d, s30d: tree.realized.sopr30d, s1y: tree.realized.sopr1y, ema24h7d: tree.realized.sopr24h7dEma, ema24h30d: tree.realized.sopr24h30dEma }, - title, - ), - }, { name: "Sell Side Risk", - tree: singleRollingSellSideRiskTree(tree.realized, title), + tree: [ + { name: "24h", title: title("Sell Side Risk (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.sellSideRiskRatio._24h.ratio, name, color, unit: Unit.ratio })) }, + { name: "7d", title: title("Sell Side Risk (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.sellSideRiskRatio._1w.ratio, name, color, unit: Unit.ratio })) }, + { name: "30d", title: title("Sell Side Risk (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.sellSideRiskRatio._1m.ratio, name, color, unit: Unit.ratio })) }, + { name: "1y", title: title("Sell Side Risk (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.sellSideRiskRatio._1y.ratio, name, color, unit: Unit.ratio })) }, + ], }, { 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: "Distribution", title: title("Distribution Flow"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.profit.distributionFlow, name, color, unit: Unit.usd })) }, + { name: "Capitulation", title: title("Capitulation Flow"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.loss.capitulationFlow, name, color, unit: Unit.usd })) }, ], }, + { name: "Created", title: title("Value Created"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.sopr.valueCreated.base, name, color, unit: Unit.usd })) }, + { name: "Destroyed", title: title("Value Destroyed"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.sopr.valueDestroyed.base, name, color, unit: Unit.usd })) }, { name: "Rolling", - tree: - valueRollingTree ?? - singleRollingValueTree( - { - created24h: tree.realized.valueCreated24h, created7d: tree.realized.valueCreated7d, - created30d: tree.realized.valueCreated30d, created1y: tree.realized.valueCreated1y, - destroyed24h: tree.realized.valueDestroyed24h, destroyed7d: tree.realized.valueDestroyed7d, - destroyed30d: tree.realized.valueDestroyed30d, destroyed1y: tree.realized.valueDestroyed1y, - }, - title, - ), + tree: [ + { + name: "Normal", + tree: groupedRollingValueCharts(list, all, valueWindows(list, all), title), + }, + { + name: "Adjusted", + tree: groupedRollingValueCharts( + list, all, + [ + { name: "24h", getCreated: (c) => c.tree.realized.sopr.adjusted.valueCreated.sum._24h, getDestroyed: (c) => c.tree.realized.sopr.adjusted.valueDestroyed.sum._24h }, + { name: "7d", getCreated: (c) => c.tree.realized.sopr.adjusted.valueCreated.sum._1w, getDestroyed: (c) => c.tree.realized.sopr.adjusted.valueDestroyed.sum._1w }, + { name: "30d", getCreated: (c) => c.tree.realized.sopr.adjusted.valueCreated.sum._1m, getDestroyed: (c) => c.tree.realized.sopr.adjusted.valueDestroyed.sum._1m }, + { name: "1y", getCreated: (c) => c.tree.realized.sopr.adjusted.valueCreated.sum._1y, getDestroyed: (c) => c.tree.realized.sopr.adjusted.valueDestroyed.sum._1y }, + ], + title, + "Adjusted ", + ), + }, + ], }, ], }, { name: "Coins Destroyed", - tree: [ - { - name: "Sum", - title: title("Coins Destroyed"), - bottom: [ - line({ - metric: tree.activity.coinblocksDestroyed.sum._24h, - name: "Coinblocks", - color, - unit: Unit.coinblocks, - }), - line({ - metric: tree.activity.coindaysDestroyed.sum._24h, - 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, - }), - ], - }, - ], + title: title("Coindays Destroyed"), + bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [ + line({ metric: tree.activity.coindaysDestroyed.sum._24h, name, color, unit: Unit.coindays }), + ]), }, ], }; } /** - * Activity section with adjusted values (for cohorts with RealizedPattern3/4) - * @param {{ cohort: CohortAll | CohortFull | CohortWithAdjusted, title: (metric: string) => string }} args + * Grouped activity for cohorts with rolling SOPR + sell side risk (LTH-like) + * @param {{ list: readonly (CohortFull | CohortLongTerm)[], all: CohortAll, 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, - }), - ], - valueRollingTree: [ - { - name: "Normal", - tree: singleRollingValueTree( - { - created24h: tree.realized.valueCreated24h, created7d: tree.realized.valueCreated7d, - created30d: tree.realized.valueCreated30d, created1y: tree.realized.valueCreated1y, - destroyed24h: tree.realized.valueDestroyed24h, destroyed7d: tree.realized.valueDestroyed7d, - destroyed30d: tree.realized.valueDestroyed30d, destroyed1y: tree.realized.valueDestroyed1y, - }, - title, - ), - }, - { - name: "Adjusted", - tree: singleRollingValueTree( - { - created24h: tree.realized.adjustedValueCreated24h, created7d: tree.realized.adjustedValueCreated7d, - created30d: tree.realized.adjustedValueCreated30d, created1y: tree.realized.adjustedValueCreated1y, - destroyed24h: tree.realized.adjustedValueDestroyed24h, destroyed7d: tree.realized.adjustedValueDestroyed7d, - destroyed30d: tree.realized.adjustedValueDestroyed30d, destroyed1y: tree.realized.adjustedValueDestroyed1y, - }, - title, - "Adjusted ", - ), - }, - ], - }); -} - -// ============================================================================ -// 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 {T} all - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function groupedFlowsTree(list, all, title) { - return [ - { - name: "Profit", - title: title("Profit Flow"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.realized.profitFlow, - name, - color, - unit: Unit.usd, - }), - ), - }, - { - name: "Capitulation", - title: title("Capitulation Flow"), - bottom: mapCohortsWithAll(list, all, ({ name, color, 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 {T} all - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function createGroupedValueTree(list, all, title) { - return [ - { name: "Flows", tree: groupedFlowsTree(list, all, title) }, - { - name: "Created", - title: title("Value Created"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.realized.valueCreated, - name, - color, - unit: Unit.usd, - }), - ), - }, - { - name: "Destroyed", - title: title("Value Destroyed"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.realized.valueDestroyed, - name, - color, - unit: Unit.usd, - }), - ), - }, - { name: "Breakdown", tree: valueBreakdownTree(list, all, title) }, - ]; -} - -/** - * Grouped activity section builder - * @param {{ list: readonly (UtxoCohortObject | CohortWithoutRelative)[], all: CohortAll, title: (metric: string) => string, soprTree?: PartialOptionsTree, valueTree?: PartialOptionsTree }} args - * @returns {PartialOptionsGroup} - */ -export function createGroupedActivitySection({ - list, - all, - title, - soprTree, - valueTree, -}) { +export function createGroupedActivitySection({ list, all, title }) { return { name: "Activity", tree: [ { name: "Volume", - tree: [ - { - name: "14d EMA", - title: title("Sent Volume 14d EMA"), - bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => - satsBtcUsd({ pattern: tree.activity.sent14dEma, name, color }), - ), - }, - { - name: "Sum", - title: title("Sent Volume"), - bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => - satsBtcUsd({ - pattern: { - sats: tree.activity.sent.base.sats, - btc: tree.activity.sent.base.btc, - usd: tree.activity.sent.base.usd, - }, - name, - color, - }), - ), - }, - ], + title: title("Sent Volume"), + bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [ + line({ metric: tree.activity.sent.sum._24h, name, color, unit: Unit.sats }), + ]), }, { name: "SOPR", - tree: soprTree ?? [ - ...createGroupedSoprTree(list, all, title), + tree: [ + ...groupedSoprCharts( + list, all, + (c) => c.tree.realized.sopr.ratio._24h, + (c) => c.tree.realized.sopr.ratio._1w, + (c) => c.tree.realized.sopr.ratio._1m, + title, + ), { name: "Rolling", tree: groupedRollingSoprCharts( - list, - all, - (c) => c.tree.realized.sopr24h, - (c) => c.tree.realized.sopr7d, - (c) => c.tree.realized.sopr30d, - (c) => c.tree.realized.sopr1y, + list, all, + (c) => c.tree.realized.sopr.ratio._24h, + (c) => c.tree.realized.sopr.ratio._1w, + (c) => c.tree.realized.sopr.ratio._1m, + (c) => c.tree.realized.sopr.ratio._1y, title, ), }, @@ -1052,195 +777,94 @@ export function createGroupedActivitySection({ }, { name: "Sell Side Risk", - tree: groupedRollingSellSideRiskCharts(list, all, title), + tree: [ + { name: "24h", title: title("Sell Side Risk (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.sellSideRiskRatio._24h.ratio, name, color, unit: Unit.ratio })) }, + { name: "7d", title: title("Sell Side Risk (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.sellSideRiskRatio._1w.ratio, name, color, unit: Unit.ratio })) }, + { name: "30d", title: title("Sell Side Risk (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.sellSideRiskRatio._1m.ratio, name, color, unit: Unit.ratio })) }, + { name: "1y", title: title("Sell Side Risk (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.sellSideRiskRatio._1y.ratio, name, color, unit: Unit.ratio })) }, + ], }, { name: "Value", - tree: valueTree ?? [ - ...createGroupedValueTree(list, all, title), + tree: [ + { + name: "Flows", + tree: [ + { name: "Distribution", title: title("Distribution Flow"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.profit.distributionFlow, name, color, unit: Unit.usd })) }, + { name: "Capitulation", title: title("Capitulation Flow"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.loss.capitulationFlow, name, color, unit: Unit.usd })) }, + ], + }, + { name: "Created", title: title("Value Created"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.sopr.valueCreated.base, name, color, unit: Unit.usd })) }, + { name: "Destroyed", title: title("Value Destroyed"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.sopr.valueDestroyed.base, name, color, unit: Unit.usd })) }, { name: "Rolling", - tree: groupedRollingValueCharts( - list, - all, - [ - { name: "24h", getCreated: (c) => c.tree.realized.valueCreated24h, getDestroyed: (c) => c.tree.realized.valueDestroyed24h }, - { name: "7d", getCreated: (c) => c.tree.realized.valueCreated7d, getDestroyed: (c) => c.tree.realized.valueDestroyed7d }, - { name: "30d", getCreated: (c) => c.tree.realized.valueCreated30d, getDestroyed: (c) => c.tree.realized.valueDestroyed30d }, - { name: "1y", getCreated: (c) => c.tree.realized.valueCreated1y, getDestroyed: (c) => c.tree.realized.valueDestroyed1y }, - ], - title, - ), + tree: groupedRollingValueCharts(list, all, valueWindows(list, all), title), }, ], }, - { name: "Coins Destroyed", tree: coinsDestroyedTree(list, all, title) }, + { + name: "Coins Destroyed", + title: title("Coindays Destroyed"), + bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [ + line({ metric: tree.activity.coindaysDestroyed.sum._24h, name, color, unit: Unit.coindays }), + ]), + }, ], }; } /** - * Create grouped value tree with adjusted values (Flows, Normal, Adjusted, Breakdown) - * @param {readonly (CohortAll | CohortFull | CohortWithAdjusted)[]} list - * @param {CohortAll | CohortFull | CohortWithAdjusted} all - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function createGroupedValueTreeWithAdjusted(list, all, title) { - return [ - { name: "Flows", tree: groupedFlowsTree(list, all, title) }, - { - name: "Normal", - tree: [ - { - name: "Created", - title: title("Value Created"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.realized.valueCreated, - name, - color, - unit: Unit.usd, - }), - ), - }, - { - name: "Destroyed", - title: title("Value Destroyed"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.realized.valueDestroyed, - name, - color, - unit: Unit.usd, - }), - ), - }, - ], - }, - { - name: "Adjusted", - tree: [ - { - name: "Created", - title: title("Adjusted Value Created"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.realized.adjustedValueCreated, - name, - color, - unit: Unit.usd, - }), - ), - }, - { - name: "Destroyed", - title: title("Adjusted Value Destroyed"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.realized.adjustedValueDestroyed, - name, - color, - unit: Unit.usd, - }), - ), - }, - ], - }, - { name: "Breakdown", tree: valueBreakdownTree(list, all, title) }, - { - name: "Rolling", - tree: [ - { - name: "Normal", - tree: groupedRollingValueCharts( - list, - all, - [ - { name: "24h", getCreated: (c) => c.tree.realized.valueCreated24h, getDestroyed: (c) => c.tree.realized.valueDestroyed24h }, - { name: "7d", getCreated: (c) => c.tree.realized.valueCreated7d, getDestroyed: (c) => c.tree.realized.valueDestroyed7d }, - { name: "30d", getCreated: (c) => c.tree.realized.valueCreated30d, getDestroyed: (c) => c.tree.realized.valueDestroyed30d }, - { name: "1y", getCreated: (c) => c.tree.realized.valueCreated1y, getDestroyed: (c) => c.tree.realized.valueDestroyed1y }, - ], - title, - ), - }, - { - name: "Adjusted", - tree: groupedRollingValueCharts( - list, - all, - [ - { name: "24h", getCreated: (c) => c.tree.realized.adjustedValueCreated24h, getDestroyed: (c) => c.tree.realized.adjustedValueDestroyed24h }, - { name: "7d", getCreated: (c) => c.tree.realized.adjustedValueCreated7d, getDestroyed: (c) => c.tree.realized.adjustedValueDestroyed7d }, - { name: "30d", getCreated: (c) => c.tree.realized.adjustedValueCreated30d, getDestroyed: (c) => c.tree.realized.adjustedValueDestroyed30d }, - { name: "1y", getCreated: (c) => c.tree.realized.adjustedValueCreated1y, getDestroyed: (c) => c.tree.realized.adjustedValueDestroyed1y }, - ], - title, - "Adjusted ", - ), - }, - ], - }, - ]; -} - -/** - * Grouped activity section with adjusted values (for cohorts with RealizedPattern3/4) - * @param {{ list: readonly (CohortAll | CohortFull | CohortWithAdjusted)[], all: CohortAll, title: (metric: string) => string }} args + * Grouped activity for cohorts with activity but basic realized (AgeRange/MaxAge) + * @param {{ list: readonly (CohortAgeRange | CohortWithAdjusted)[], all: CohortAll, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ -export function createGroupedActivitySectionWithAdjusted({ list, all, title }) { - return createGroupedActivitySection({ - list, - all, - title, - soprTree: createGroupedSoprTreeWithAdjusted(list, all, title), - valueTree: createGroupedValueTreeWithAdjusted(list, all, title), - }); -} - - -/** - * Create value created & destroyed series for single cohort - * @param {{ realized: AnyRealizedPattern }} tree - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function createSingleValueCreatedDestroyedSeries(tree) { - return [ - line({ - metric: tree.realized.valueCreated, - name: "Created", - color: colors.usd, - unit: Unit.usd, - }), - line({ - metric: tree.realized.valueDestroyed, - name: "Destroyed", - color: colors.loss, - unit: Unit.usd, - }), - ]; +export function createGroupedActivitySectionWithActivity({ list, all, title }) { + return { + name: "Activity", + tree: [ + { + name: "Volume", + title: title("Sent Volume"), + bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [ + line({ metric: tree.activity.sent.sum._24h, name, color, unit: Unit.sats }), + ]), + }, + { + name: "SOPR", + title: title("SOPR (24h)"), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + baseline({ metric: tree.realized.sopr.ratio._24h, name, color, unit: Unit.ratio, base: 1 }), + ), + }, + { + name: "Value", + tree: [ + { name: "Created", title: title("Value Created"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.sopr.valueCreated.base, name, color, unit: Unit.usd })) }, + { name: "Destroyed", title: title("Value Destroyed"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.sopr.valueDestroyed.base, name, color, unit: Unit.usd })) }, + ], + }, + { + name: "Coins Destroyed", + title: title("Coindays Destroyed"), + bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [ + line({ metric: tree.activity.coindaysDestroyed.sum._24h, name, color, unit: Unit.coindays }), + ]), + }, + ], + }; } /** - * Create capitulation & profit flow series for single cohort - * @param {{ realized: AnyRealizedPattern }} tree - * @returns {AnyFetchedSeriesBlueprint[]} + * Grouped minimal activity (value only, no activity field) + * @param {{ list: readonly (UtxoCohortObject | CohortWithoutRelative | CohortAddress | AddressCohortObject)[], all: CohortAll, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} */ -function createSingleCapitulationProfitFlowSeries(tree) { - return [ - line({ - metric: tree.realized.profitFlow, - name: "Profit Flow", - color: colors.profit, - unit: Unit.usd, - }), - line({ - metric: tree.realized.capitulationFlow, - name: "Capitulation Flow", - color: colors.loss, - unit: Unit.usd, - }), - ]; +export function createGroupedActivitySectionMinimal({ list, all, title }) { + return { + name: "Activity", + tree: [ + { name: "Value Created", title: title("Value Created"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.sopr.valueCreated.base, name, color, unit: Unit.usd })) }, + { name: "Value Destroyed", title: title("Value Destroyed"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.sopr.valueDestroyed.base, name, color, unit: Unit.usd })) }, + ], + }; } diff --git a/website/scripts/options/distribution/cost-basis.js b/website/scripts/options/distribution/cost-basis.js index 0409d9037..a1ea12fbc 100644 --- a/website/scripts/options/distribution/cost-basis.js +++ b/website/scripts/options/distribution/cost-basis.js @@ -5,9 +5,9 @@ * - Summary: Key stats (avg + median active, quartiles/extremes available) * - By Coin: BTC-weighted percentiles (IQR active: p25, p50, p75) * - By Capital: USD-weighted percentiles (IQR active: p25, p50, p75) - * - Price Position: Spot percentile (both perspectives active) + * - Supply Density: Cost basis supply density percentage * - * For cohorts WITHOUT percentiles: Summary only + * Only for cohorts WITH costBasis (All, STH, LTH) */ import { colors } from "../../utils/colors.js"; @@ -38,37 +38,14 @@ function createCorePercentileSeries(p, n = (x) => x) { } /** - * @param {UtxoCohortObject | CohortWithoutRelative} cohort + * @param {CohortAll | CohortFull | CohortLongTerm} cohort * @returns {FetchedPriceSeriesBlueprint[]} */ -function createSingleSummarySeriesBasic(cohort) { - const { color, tree } = cohort; - return [ - price({ metric: tree.realized.realizedPrice, name: "Average", color }), - price({ - metric: tree.costBasis.max, - name: "Max", - color: colors.stat.max, - defaultActive: false, - }), - price({ - metric: tree.costBasis.min, - name: "Min", - color: colors.stat.min, - defaultActive: false, - }), - ]; -} - -/** - * @param {CohortAll | CohortFull | CohortWithPercentiles} cohort - * @returns {FetchedPriceSeriesBlueprint[]} - */ -function createSingleSummarySeriesWithPercentiles(cohort) { +function createSingleSummarySeries(cohort) { const { color, tree } = cohort; const p = tree.costBasis.percentiles; return [ - price({ metric: tree.realized.realizedPrice, name: "Average", color }), + price({ metric: tree.realized.price, name: "Average", color }), price({ metric: tree.costBasis.max, name: "Max (p100)", @@ -98,25 +75,25 @@ function createSingleSummarySeriesWithPercentiles(cohort) { } /** - * @param {readonly CohortObject[]} list + * @param {readonly (CohortAll | CohortFull | CohortLongTerm)[]} list * @param {CohortAll} all * @returns {FetchedPriceSeriesBlueprint[]} */ function createGroupedSummarySeries(list, all) { return mapCohortsWithAll(list, all, ({ name, color, tree }) => - price({ metric: tree.realized.realizedPrice, name, color }), + price({ metric: tree.realized.price, name, color }), ); } /** - * @param {CohortAll | CohortFull | CohortWithPercentiles} cohort + * @param {CohortAll | CohortFull | CohortLongTerm} cohort * @returns {FetchedPriceSeriesBlueprint[]} */ function createSingleByCoinSeries(cohort) { const { color, tree } = cohort; const cb = tree.costBasis; return [ - price({ metric: tree.realized.realizedPrice, name: "Average", color }), + price({ metric: tree.realized.price, name: "Average", color }), price({ metric: cb.max, name: "p100", @@ -134,59 +111,36 @@ function createSingleByCoinSeries(cohort) { } /** - * @param {CohortAll | CohortFull | CohortWithPercentiles} cohort + * @param {CohortAll | CohortFull | CohortLongTerm} cohort * @returns {FetchedPriceSeriesBlueprint[]} */ function createSingleByCapitalSeries(cohort) { const { color, tree } = cohort; return [ - price({ metric: tree.realized.investorPrice, name: "Average", color }), + price({ metric: tree.realized.investor.price, name: "Average", color }), ...createCorePercentileSeries(tree.costBasis.investedCapital), ]; } /** - * @param {CohortAll | CohortFull | CohortWithPercentiles} cohort + * @param {CohortAll | CohortFull | CohortLongTerm} cohort * @returns {AnyFetchedSeriesBlueprint[]} */ -function createSinglePricePositionSeries(cohort) { +function createSingleSupplyDensitySeries(cohort) { const { tree } = cohort; return [ line({ - metric: tree.costBasis.spotCostBasisPercentile, - name: "By Coin", + metric: tree.costBasis.supplyDensity.percent, + name: "Supply Density", color: colors.bitcoin, unit: Unit.percentage, }), - line({ - metric: tree.costBasis.spotInvestedCapitalPercentile, - name: "By Capital", - color: colors.usd, - unit: Unit.percentage, - }), ...priceLines({ numbers: [100, 50, 0], unit: Unit.percentage }), ]; } /** - * @param {{ cohort: UtxoCohortObject | CohortWithoutRelative, title: (metric: string) => string }} args - * @returns {PartialOptionsGroup} - */ -export function createCostBasisSection({ cohort, title }) { - return { - name: "Cost Basis", - tree: [ - { - name: "Summary", - title: title("Cost Basis Summary"), - top: createSingleSummarySeriesBasic(cohort), - }, - ], - }; -} - -/** - * @param {{ cohort: CohortAll | CohortFull | CohortWithPercentiles, title: (metric: string) => string }} args + * @param {{ cohort: CohortAll | CohortFull | CohortLongTerm, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ export function createCostBasisSectionWithPercentiles({ cohort, title }) { @@ -196,7 +150,7 @@ export function createCostBasisSectionWithPercentiles({ cohort, title }) { { name: "Summary", title: title("Cost Basis Summary"), - top: createSingleSummarySeriesWithPercentiles(cohort), + top: createSingleSummarySeries(cohort), }, { name: "By Coin", @@ -209,33 +163,16 @@ export function createCostBasisSectionWithPercentiles({ cohort, title }) { top: createSingleByCapitalSeries(cohort), }, { - name: "Price Position", - title: title("Current Price Position"), - bottom: createSinglePricePositionSeries(cohort), + name: "Supply Density", + title: title("Cost Basis Supply Density"), + bottom: createSingleSupplyDensitySeries(cohort), }, ], }; } /** - * @param {{ list: readonly (UtxoCohortObject | CohortWithoutRelative)[], all: CohortAll, title: (metric: string) => string }} args - * @returns {PartialOptionsGroup} - */ -export function createGroupedCostBasisSection({ list, all, title }) { - return { - name: "Cost Basis", - tree: [ - { - name: "Summary", - title: title("Cost Basis Summary"), - top: createGroupedSummarySeries(list, all), - }, - ], - }; -} - -/** - * @param {{ list: readonly (CohortAll | CohortFull | CohortWithPercentiles)[], all: CohortAll, title: (metric: string) => string }} args + * @param {{ list: readonly (CohortAll | CohortFull | CohortLongTerm)[], all: CohortAll, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ export function createGroupedCostBasisSectionWithPercentiles({ @@ -258,7 +195,7 @@ export function createGroupedCostBasisSectionWithPercentiles({ name: "Average", title: title("Realized Price Comparison"), top: mapCohortsWithAll(list, all, ({ name, color, tree }) => - price({ metric: tree.realized.realizedPrice, name, color }), + price({ metric: tree.realized.price, name, color }), ), }, { @@ -291,7 +228,7 @@ export function createGroupedCostBasisSectionWithPercentiles({ name: "Average", title: title("Investor Price Comparison"), top: mapCohortsWithAll(list, all, ({ name, color, tree }) => - price({ metric: tree.realized.investorPrice, name, color }), + price({ metric: tree.realized.investor.price, name, color }), ), }, { @@ -330,39 +267,16 @@ export function createGroupedCostBasisSectionWithPercentiles({ ], }, { - name: "Price Position", - tree: [ - { - name: "By Coin", - title: title("Price Position (BTC-weighted)"), - bottom: [ - ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.costBasis.spotCostBasisPercentile, - name, - color, - unit: Unit.percentage, - }), - ), - ...priceLines({ numbers: [100, 50, 0], unit: Unit.percentage }), - ], - }, - { - name: "By Capital", - title: title("Price Position (USD-weighted)"), - bottom: [ - ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.costBasis.spotInvestedCapitalPercentile, - name, - color, - unit: Unit.percentage, - }), - ), - ...priceLines({ numbers: [100, 50, 0], unit: Unit.percentage }), - ], - }, - ], + name: "Supply Density", + title: title("Cost Basis Supply Density"), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + line({ + metric: tree.costBasis.supplyDensity.percent, + name, + color, + unit: Unit.percentage, + }), + ), }, ], }; diff --git a/website/scripts/options/distribution/data.js b/website/scripts/options/distribution/data.js index 541ee4340..7650078c7 100644 --- a/website/scripts/options/distribution/data.js +++ b/website/scripts/options/distribution/data.js @@ -16,7 +16,7 @@ const ADDRESSABLE_TYPES = [ /** @type {(key: SpendableType) => key is AddressableType} */ const isAddressable = (key) => - ADDRESSABLE_TYPES.includes(/** @type {any} */ (key)); + /** @type {readonly string[]} */ (ADDRESSABLE_TYPES).includes(key); export function buildCohortData() { const utxoCohorts = brk.metrics.cohorts.utxo; diff --git a/website/scripts/options/distribution/holdings.js b/website/scripts/options/distribution/holdings.js index 050a32dd9..d89266f99 100644 --- a/website/scripts/options/distribution/holdings.js +++ b/website/scripts/options/distribution/holdings.js @@ -1,23 +1,18 @@ /** * Holdings section builders * - * Structure (Option C - optimized for UX): - * - Supply: Total BTC held (flat, one click) - * - UTXO Count: Number of UTXOs (flat, one click) - * - Address Count: Number of addresses (when available, flat) - * - 30d Changes/: Folder for change metrics - * - Supply: 30d supply change - * - Relative: % of circulating supply (when available) - * - * Rationale: Most-used metrics (Supply, UTXO Count) are immediately accessible. - * 30d changes are grouped together for consistency and cleaner navigation. + * Supply pattern capabilities by cohort type: + * - DeltaHalfInRelTotalPattern2 (STH/LTH): inProfit + inLoss + relToCirculating + relToOwn + * - MetricsTree_Cohorts_Utxo_All_Supply (All): inProfit + inLoss + relToOwn (no relToCirculating) + * - DeltaHalfInRelTotalPattern (AgeRange/MaxAge/Epoch): inProfit + inLoss + relToCirculating (no relToOwn) + * - DeltaHalfInTotalPattern2 (Type.*): inProfit + inLoss (no rel) + * - DeltaHalfTotalPattern (Empty/UtxoAmount/AddrAmount): total + half only */ import { Unit } from "../../utils/units.js"; import { line, baseline } from "../series.js"; import { satsBtcUsd, - satsBtcUsdBaseline, mapCohorts, mapCohortsWithAll, flatMapCohortsWithAll, @@ -26,29 +21,50 @@ import { colors } from "../../utils/colors.js"; import { priceLines } from "../constants.js"; /** - * Base supply series (total, profit, loss, halved) - * @param {{ supply: { total: AnyValuePattern, halved: AnyValuePattern }, unrealized: { supplyInProfit: AnyValuePattern, supplyInLoss: AnyValuePattern } }} tree + * Simple supply series (total + half only, no profit/loss) + * @param {{ total: AnyValuePattern, half: AnyValuePattern }} supply * @returns {AnyFetchedSeriesBlueprint[]} */ -function baseSupplySeries(tree) { +function simpleSupplySeries(supply) { return [ ...satsBtcUsd({ - pattern: tree.supply.total, + pattern: supply.total, name: "Total", color: colors.default, }), ...satsBtcUsd({ - pattern: tree.unrealized.supplyInProfit, + pattern: supply.half, + name: "Halved", + color: colors.gray, + style: 4, + }), + ]; +} + +/** + * Full supply series (total, profit, loss, halved) + * @param {{ total: AnyValuePattern, half: AnyValuePattern, inProfit: AnyValuePattern, inLoss: AnyValuePattern }} supply + * @returns {AnyFetchedSeriesBlueprint[]} + */ +function fullSupplySeries(supply) { + return [ + ...satsBtcUsd({ + pattern: supply.total, + name: "Total", + color: colors.default, + }), + ...satsBtcUsd({ + pattern: supply.inProfit, name: "In Profit", color: colors.profit, }), ...satsBtcUsd({ - pattern: tree.unrealized.supplyInLoss, + pattern: supply.inLoss, name: "In Loss", color: colors.loss, }), ...satsBtcUsd({ - pattern: tree.supply.halved, + pattern: supply.half, name: "Halved", color: colors.gray, style: 4, @@ -58,19 +74,19 @@ function baseSupplySeries(tree) { /** * % of Own Supply series (profit/loss relative to own supply) - * @param {{ relative: { supplyInProfitRelToOwnSupply: AnyMetricPattern, supplyInLossRelToOwnSupply: AnyMetricPattern } }} tree + * @param {{ inProfit: { relToOwn: { percent: AnyMetricPattern } }, inLoss: { relToOwn: { percent: AnyMetricPattern } } }} supply * @returns {AnyFetchedSeriesBlueprint[]} */ -function ownSupplyPctSeries(tree) { +function ownSupplyPctSeries(supply) { return [ line({ - metric: tree.relative.supplyInProfitRelToOwnSupply, + metric: supply.inProfit.relToOwn.percent, name: "In Profit", color: colors.profit, unit: Unit.pctOwn, }), line({ - metric: tree.relative.supplyInLossRelToOwnSupply, + metric: supply.inLoss.relToOwn.percent, name: "In Loss", color: colors.loss, unit: Unit.pctOwn, @@ -81,25 +97,25 @@ function ownSupplyPctSeries(tree) { /** * % of Circulating Supply series (total, profit, loss) - * @param {{ relative: { supplyRelToCirculatingSupply: AnyMetricPattern, supplyInProfitRelToCirculatingSupply: AnyMetricPattern, supplyInLossRelToCirculatingSupply: AnyMetricPattern } }} tree + * @param {{ relToCirculating: { percent: AnyMetricPattern }, inProfit: { relToCirculating: { percent: AnyMetricPattern } }, inLoss: { relToCirculating: { percent: AnyMetricPattern } } }} supply * @returns {AnyFetchedSeriesBlueprint[]} */ -function circulatingSupplyPctSeries(tree) { +function circulatingSupplyPctSeries(supply) { return [ line({ - metric: tree.relative.supplyRelToCirculatingSupply, + metric: supply.relToCirculating.percent, name: "Total", color: colors.default, unit: Unit.pctSupply, }), line({ - metric: tree.relative.supplyInProfitRelToCirculatingSupply, + metric: supply.inProfit.relToCirculating.percent, name: "In Profit", color: colors.profit, unit: Unit.pctSupply, }), line({ - metric: tree.relative.supplyInLossRelToCirculatingSupply, + metric: supply.inLoss.relToCirculating.percent, name: "In Loss", color: colors.loss, unit: Unit.pctSupply, @@ -117,7 +133,12 @@ function groupedUtxoCountChart(list, all, title) { name: "UTXO Count", title: title("UTXO Count"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ metric: tree.outputs.utxoCount, name, color, unit: Unit.count }), + line({ + metric: tree.outputs.unspentCount.inner, + name, + color, + unit: Unit.count, + }), ), }; } @@ -131,8 +152,13 @@ function grouped30dSupplyChangeChart(list, all, title) { return { name: "Supply", title: title("Supply 30d Change"), - bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => - satsBtcUsdBaseline({ pattern: tree.supply._30dChange, name, color }), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + baseline({ + metric: tree.supply.delta.change._1m, + name, + color, + unit: Unit.sats, + }), ), }; } @@ -148,7 +174,7 @@ function grouped30dUtxoCountChangeChart(list, all, title) { title: title("UTXO Count 30d Change"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ - metric: tree.outputs.utxoCount30dChange, + metric: tree.outputs.unspentCount.delta.change._1m, name, unit: Unit.count, color, @@ -158,7 +184,6 @@ function grouped30dUtxoCountChangeChart(list, all, title) { } /** - * Single cohort UTXO count chart * @param {UtxoCohortObject | CohortWithoutRelative} cohort * @param {(metric: string) => string} title * @returns {PartialChartOption} @@ -167,12 +192,18 @@ function singleUtxoCountChart(cohort, title) { return { name: "UTXO Count", title: title("UTXO Count"), - bottom: createSingleUtxoCountSeries(cohort), + bottom: [ + line({ + metric: cohort.tree.outputs.unspentCount.inner, + name: "UTXO Count", + color: cohort.color, + unit: Unit.count, + }), + ], }; } /** - * Single cohort 30d supply change chart * @param {UtxoCohortObject | CohortWithoutRelative} cohort * @param {(metric: string) => string} title * @returns {PartialChartOption} @@ -181,12 +212,17 @@ function single30dSupplyChangeChart(cohort, title) { return { name: "Supply", title: title("Supply 30d Change"), - bottom: createSingle30dChangeSeries(cohort), + bottom: [ + baseline({ + metric: cohort.tree.supply.delta.change._1m, + name: "30d Change", + unit: Unit.sats, + }), + ], }; } /** - * Single cohort 30d UTXO count change chart * @param {UtxoCohortObject | CohortWithoutRelative} cohort * @param {(metric: string) => string} title * @returns {PartialChartOption} @@ -195,13 +231,18 @@ function single30dUtxoCountChangeChart(cohort, title) { return { name: "UTXO Count", title: title("UTXO Count 30d Change"), - bottom: createSingleUtxoCount30dChangeSeries(cohort), + bottom: [ + baseline({ + metric: cohort.tree.outputs.unspentCount.delta.change._1m, + name: "30d Change", + unit: Unit.count, + }), + ], }; } /** - * Single cohort address count chart - * @param {CohortAll | CohortAddress} cohort + * @param {CohortAll | CohortAddress | AddressCohortObject} cohort * @param {(metric: string) => string} title * @returns {PartialChartOption} */ @@ -221,8 +262,7 @@ function singleAddressCountChart(cohort, title) { } /** - * Single cohort 30d address count change chart - * @param {CohortAll | CohortAddress} cohort + * @param {CohortAll | CohortAddress | AddressCohortObject} cohort * @param {(metric: string) => string} title * @returns {PartialChartOption} */ @@ -230,131 +270,23 @@ function single30dAddressCountChangeChart(cohort, title) { return { name: "Address Count", title: title("Address Count 30d Change"), - bottom: createSingleAddrCount30dChangeSeries(cohort), + bottom: [ + baseline({ + metric: cohort.addressCount.delta.change._1m, + name: "30d Change", + unit: Unit.count, + }), + ], }; } -/** - * @param {UtxoCohortObject | CohortWithoutRelative} cohort - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function createSingleSupplySeries(cohort) { - return baseSupplySeries(cohort.tree); -} - -/** - * Supply series for CohortAll (has % of Own Supply but not % of Circulating) - * @param {CohortAll} cohort - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function createSingleSupplySeriesAll(cohort) { - return [...baseSupplySeries(cohort.tree), ...ownSupplyPctSeries(cohort.tree)]; -} - -/** - * @param {UtxoCohortObject | CohortWithoutRelative} cohort - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function createSingle30dChangeSeries(cohort) { - return satsBtcUsdBaseline({ - pattern: cohort.tree.supply._30dChange, - name: "30d Change", - }); -} - -/** - * @param {UtxoCohortObject | CohortWithoutRelative} cohort - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function createSingleUtxoCountSeries(cohort) { - const { color, tree } = cohort; - return [ - line({ - metric: tree.outputs.utxoCount, - name: "UTXO Count", - color, - unit: Unit.count, - }), - ]; -} - -/** - * @param {UtxoCohortObject | CohortWithoutRelative} cohort - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function createSingleUtxoCount30dChangeSeries(cohort) { - return [ - baseline({ - metric: cohort.tree.outputs.utxoCount30dChange, - name: "30d Change", - unit: Unit.count, - }), - ]; -} - -/** - * @param {CohortAll | CohortAddress} cohort - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function createSingleAddrCount30dChangeSeries(cohort) { - return [ - baseline({ - metric: cohort.addressCount.delta.change._1m, - name: "30d Change", - unit: Unit.count, - }), - ]; -} - -/** - * Create supply series with % of Circulating (for cohorts with relative data) - * @param {CohortFull | CohortWithAdjusted | CohortBasicWithMarketCap} cohort - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function createSingleSupplySeriesWithRelative(cohort) { - const { tree } = cohort; - return [ - ...baseSupplySeries(tree), - ...circulatingSupplyPctSeries(tree), - ...ownSupplyPctSeries(tree), - ]; -} - -/** - * Supply series with % of Own Supply only (for cohorts without % of Circulating) - * Note: Different order - profit/loss before total for visual emphasis - * @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, - }), - ...satsBtcUsd({ - pattern: tree.supply.halved, - name: "Halved", - color: colors.gray, - style: 4, - }), - ...ownSupplyPctSeries(tree), - ]; -} +// ============================================================================ +// Single Cohort Holdings Sections +// ============================================================================ /** + * Basic holdings (total + half only, no supply breakdown) + * For: CohortWithoutRelative, CohortBasicWithMarketCap, CohortBasicWithoutMarketCap * @param {{ cohort: UtxoCohortObject | CohortWithoutRelative, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ @@ -365,58 +297,7 @@ export function createHoldingsSection({ cohort, title }) { { name: "Supply", title: title("Supply"), - bottom: createSingleSupplySeries(cohort), - }, - singleUtxoCountChart(cohort, title), - { - name: "30d Changes", - tree: [ - single30dSupplyChangeChart(cohort, title), - single30dUtxoCountChangeChart(cohort, title), - ], - }, - ], - }; -} - -/** - * 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 createHoldingsSectionWithOwnSupply({ cohort, title }) { - return { - name: "Holdings", - tree: [ - { - name: "Supply", - title: title("Supply"), - bottom: createSingleSupplySeriesWithOwnSupply(cohort), - }, - singleUtxoCountChart(cohort, title), - { - name: "30d Changes", - tree: [ - single30dSupplyChangeChart(cohort, title), - single30dUtxoCountChangeChart(cohort, title), - ], - }, - ], - }; -} - -/** - * @param {{ cohort: CohortFull | CohortWithAdjusted | CohortBasicWithMarketCap, title: (metric: string) => string }} args - * @returns {PartialOptionsGroup} - */ -export function createHoldingsSectionWithRelative({ cohort, title }) { - return { - name: "Holdings", - tree: [ - { - name: "Supply", - title: title("Supply"), - bottom: createSingleSupplySeriesWithRelative(cohort), + bottom: simpleSupplySeries(cohort.tree.supply), }, singleUtxoCountChart(cohort, title), { @@ -431,17 +312,22 @@ export function createHoldingsSectionWithRelative({ cohort, title }) { } /** + * Holdings for CohortAll (has inProfit/inLoss with relToOwn but no relToCirculating) * @param {{ cohort: CohortAll, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ export function createHoldingsSectionAll({ cohort, title }) { + const { supply } = cohort.tree; return { name: "Holdings", tree: [ { name: "Supply", title: title("Supply"), - bottom: createSingleSupplySeriesAll(cohort), + bottom: [ + ...fullSupplySeries(supply), + ...ownSupplyPctSeries(supply), + ], }, singleUtxoCountChart(cohort, title), singleAddressCountChart(cohort, title), @@ -458,6 +344,70 @@ export function createHoldingsSectionAll({ cohort, title }) { } /** + * Holdings with full relative metrics (relToCirculating + relToOwn) + * For: CohortFull, CohortLongTerm (have DeltaHalfInRelTotalPattern2) + * @param {{ cohort: CohortFull | CohortLongTerm, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createHoldingsSectionWithRelative({ cohort, title }) { + const { supply } = cohort.tree; + return { + name: "Holdings", + tree: [ + { + name: "Supply", + title: title("Supply"), + bottom: [ + ...fullSupplySeries(supply), + ...circulatingSupplyPctSeries(supply), + ...ownSupplyPctSeries(supply), + ], + }, + singleUtxoCountChart(cohort, title), + { + name: "30d Changes", + tree: [ + single30dSupplyChangeChart(cohort, title), + single30dUtxoCountChangeChart(cohort, title), + ], + }, + ], + }; +} + +/** + * Holdings with inProfit/inLoss + relToCirculating (no relToOwn) + * For: CohortWithAdjusted, CohortAgeRange (have DeltaHalfInRelTotalPattern) + * @param {{ cohort: CohortWithAdjusted | CohortAgeRange, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createHoldingsSectionWithOwnSupply({ cohort, title }) { + const { supply } = cohort.tree; + return { + name: "Holdings", + tree: [ + { + name: "Supply", + title: title("Supply"), + bottom: [ + ...fullSupplySeries(supply), + ...circulatingSupplyPctSeries(supply), + ], + }, + singleUtxoCountChart(cohort, title), + { + name: "30d Changes", + tree: [ + single30dSupplyChangeChart(cohort, title), + single30dUtxoCountChangeChart(cohort, title), + ], + }, + ], + }; +} + +/** + * Holdings for CohortAddress (has inProfit/inLoss but no rel, plus address count) * @param {{ cohort: CohortAddress, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ @@ -468,7 +418,7 @@ export function createHoldingsSectionAddress({ cohort, title }) { { name: "Supply", title: title("Supply"), - bottom: createSingleSupplySeriesWithOwnSupply(cohort), + bottom: fullSupplySeries(cohort.tree.supply), }, singleUtxoCountChart(cohort, title), singleAddressCountChart(cohort, title), @@ -485,7 +435,7 @@ export function createHoldingsSectionAddress({ cohort, title }) { } /** - * Holdings section for address amount cohorts (has relative supply + address count) + * Holdings for address amount cohorts (no inProfit/inLoss, has address count) * @param {{ cohort: AddressCohortObject, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ @@ -496,7 +446,7 @@ export function createHoldingsSectionAddressAmount({ cohort, title }) { { name: "Supply", title: title("Supply"), - bottom: createSingleSupplySeriesWithRelative(cohort), + bottom: simpleSupplySeries(cohort.tree.supply), }, singleUtxoCountChart(cohort, title), singleAddressCountChart(cohort, title), @@ -512,6 +462,10 @@ export function createHoldingsSectionAddressAmount({ cohort, title }) { }; } +// ============================================================================ +// Grouped Cohort Holdings Sections +// ============================================================================ + /** * @param {{ list: readonly CohortAddress[], all: CohortAll, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} @@ -533,46 +487,24 @@ export function createGroupedHoldingsSectionAddress({ list, all, title }) { { name: "In Profit", title: title("Supply In Profit"), - bottom: [ - ...flatMapCohortsWithAll(list, all, ({ name, color, tree }) => - satsBtcUsd({ - pattern: tree.unrealized.supplyInProfit, - name, - color, - }), - ), - ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.relative.supplyInProfitRelToOwnSupply, - name, - color, - unit: Unit.pctOwn, - }), - ), - ...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwn }), - ], + bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => + satsBtcUsd({ + pattern: tree.supply.inProfit, + name, + color, + }), + ), }, { name: "In Loss", title: title("Supply In Loss"), - bottom: [ - ...flatMapCohortsWithAll(list, all, ({ name, color, tree }) => - satsBtcUsd({ - pattern: tree.unrealized.supplyInLoss, - name, - color, - }), - ), - ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.relative.supplyInLossRelToOwnSupply, - name, - color, - unit: Unit.pctOwn, - }), - ), - ...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwn }), - ], + bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => + satsBtcUsd({ + pattern: tree.supply.inLoss, + name, + color, + }), + ), }, ], }, @@ -608,7 +540,7 @@ export function createGroupedHoldingsSectionAddress({ list, all, title }) { } /** - * Grouped holdings section for address amount cohorts (has relative supply + address count) + * Grouped holdings for address amount cohorts (no inProfit/inLoss, has address count) * @param {{ list: readonly AddressCohortObject[], all: CohortAll, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ @@ -616,6 +548,96 @@ export function createGroupedHoldingsSectionAddressAmount({ list, all, title, +}) { + return { + name: "Holdings", + tree: [ + { + name: "Supply", + tree: [ + { + name: "Total", + title: title("Supply"), + bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => + satsBtcUsd({ pattern: tree.supply.total, name, color }), + ), + }, + ], + }, + groupedUtxoCountChart(list, all, title), + { + name: "Address Count", + title: title("Address Count"), + bottom: mapCohortsWithAll(list, all, ({ name, color, addressCount }) => + line({ metric: addressCount.inner, name, color, unit: Unit.count }), + ), + }, + { + name: "30d Changes", + tree: [ + grouped30dSupplyChangeChart(list, all, title), + grouped30dUtxoCountChangeChart(list, all, title), + { + name: "Address Count", + title: title("Address Count 30d Change"), + bottom: mapCohortsWithAll(list, all, ({ name, color, addressCount }) => + baseline({ + metric: addressCount.delta.change._1m, + name, + unit: Unit.count, + color, + }), + ), + }, + ], + }, + ], + }; +} + +/** + * Basic grouped holdings (total + half only) + * @param {{ list: readonly (UtxoCohortObject | CohortWithoutRelative)[], all: CohortAll, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createGroupedHoldingsSection({ list, all, title }) { + return { + name: "Holdings", + tree: [ + { + name: "Supply", + tree: [ + { + name: "Total", + title: title("Supply"), + bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => + satsBtcUsd({ pattern: tree.supply.total, name, color }), + ), + }, + ], + }, + groupedUtxoCountChart(list, all, title), + { + name: "30d Changes", + tree: [ + grouped30dSupplyChangeChart(list, all, title), + grouped30dUtxoCountChangeChart(list, all, title), + ], + }, + ], + }; +} + +/** + * Grouped holdings with inProfit/inLoss + relToCirculating (no relToOwn) + * For: CohortWithAdjusted, CohortAgeRange + * @param {{ list: readonly (CohortWithAdjusted | CohortAgeRange)[], all: CohortAll, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createGroupedHoldingsSectionWithOwnSupply({ + list, + all, + title, }) { return { name: "Holdings", @@ -632,7 +654,7 @@ export function createGroupedHoldingsSectionAddressAmount({ ), ...mapCohorts(list, ({ name, color, tree }) => line({ - metric: tree.relative.supplyRelToCirculatingSupply, + metric: tree.supply.relToCirculating.percent, name, color, unit: Unit.pctSupply, @@ -646,28 +668,19 @@ export function createGroupedHoldingsSectionAddressAmount({ bottom: [ ...flatMapCohortsWithAll(list, all, ({ name, color, tree }) => satsBtcUsd({ - pattern: tree.unrealized.supplyInProfit, + pattern: tree.supply.inProfit, name, color, }), ), ...mapCohorts(list, ({ name, color, tree }) => line({ - metric: tree.relative.supplyInProfitRelToCirculatingSupply, + metric: tree.supply.inProfit.relToCirculating.percent, name, color, unit: Unit.pctSupply, }), ), - ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.relative.supplyInProfitRelToOwnSupply, - name, - color, - unit: Unit.pctOwn, - }), - ), - ...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwn }), ], }, { @@ -676,106 +689,24 @@ export function createGroupedHoldingsSectionAddressAmount({ bottom: [ ...flatMapCohortsWithAll(list, all, ({ name, color, tree }) => satsBtcUsd({ - pattern: tree.unrealized.supplyInLoss, + pattern: tree.supply.inLoss, name, color, }), ), ...mapCohorts(list, ({ name, color, tree }) => line({ - metric: tree.relative.supplyInLossRelToCirculatingSupply, + metric: tree.supply.inLoss.relToCirculating.percent, name, color, unit: Unit.pctSupply, }), ), - ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.relative.supplyInLossRelToOwnSupply, - name, - color, - unit: Unit.pctOwn, - }), - ), - ...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwn }), ], }, ], }, groupedUtxoCountChart(list, all, title), - { - name: "Address Count", - title: title("Address Count"), - bottom: mapCohortsWithAll(list, all, ({ name, color, addressCount }) => - line({ metric: addressCount.inner, name, color, unit: Unit.count }), - ), - }, - { - name: "30d Changes", - tree: [ - grouped30dSupplyChangeChart(list, all, title), - grouped30dUtxoCountChangeChart(list, all, title), - { - name: "Address Count", - title: title("Address Count 30d Change"), - bottom: mapCohortsWithAll(list, all, ({ name, color, addressCount }) => - baseline({ - metric: addressCount.delta.change._1m, - name, - unit: Unit.count, - color, - }), - ), - }, - ], - }, - ], - }; -} - -/** - * @param {{ list: readonly (UtxoCohortObject | CohortWithoutRelative)[], all: CohortAll, title: (metric: string) => string }} args - * @returns {PartialOptionsGroup} - */ -export function createGroupedHoldingsSection({ list, all, title }) { - return { - name: "Holdings", - tree: [ - { - name: "Supply", - tree: [ - { - name: "Total", - title: title("Supply"), - bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => - satsBtcUsd({ pattern: tree.supply.total, name, color }), - ), - }, - { - name: "In Profit", - title: title("Supply In Profit"), - bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => - satsBtcUsd({ - pattern: tree.unrealized.supplyInProfit, - name, - color, - }), - ), - }, - { - name: "In Loss", - title: title("Supply In Loss"), - bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => - satsBtcUsd({ - pattern: tree.unrealized.supplyInLoss, - name, - color, - }), - ), - }, - ], - }, - groupedUtxoCountChart(list, all, title), { name: "30d Changes", tree: [ @@ -788,88 +719,9 @@ export function createGroupedHoldingsSection({ list, all, title }) { } /** - * Grouped holdings section with % of Own Supply only (for cohorts without % of Circulating) - * @param {{ list: readonly (CohortAgeRange | CohortBasicWithoutMarketCap)[], all: CohortAll, title: (metric: string) => string }} args - * @returns {PartialOptionsGroup} - */ -export function createGroupedHoldingsSectionWithOwnSupply({ - list, - all, - title, -}) { - return { - name: "Holdings", - tree: [ - { - name: "Supply", - tree: [ - { - name: "In Profit", - title: title("Supply In Profit"), - bottom: [ - ...flatMapCohortsWithAll(list, all, ({ name, color, tree }) => - satsBtcUsd({ - pattern: tree.unrealized.supplyInProfit, - name, - color, - }), - ), - ...mapCohortsWithAll(list, all, ({ 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: [ - ...flatMapCohortsWithAll(list, all, ({ name, color, tree }) => - satsBtcUsd({ - pattern: tree.unrealized.supplyInLoss, - name, - color, - }), - ), - ...mapCohortsWithAll(list, all, ({ 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: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => - satsBtcUsd({ pattern: tree.supply.total, name, color }), - ), - }, - ], - }, - groupedUtxoCountChart(list, all, title), - { - name: "30d Changes", - tree: [ - grouped30dSupplyChangeChart(list, all, title), - grouped30dUtxoCountChangeChart(list, all, title), - ], - }, - ], - }; -} - -/** - * @param {{ list: readonly (CohortFull | CohortWithAdjusted | CohortBasicWithMarketCap)[], all: CohortAll, title: (metric: string) => string }} args + * Grouped holdings with full relative metrics (relToCirculating + relToOwn) + * For: CohortFull, CohortLongTerm + * @param {{ list: readonly (CohortFull | CohortLongTerm)[], all: CohortAll, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ export function createGroupedHoldingsSectionWithRelative({ list, all, title }) { @@ -888,7 +740,7 @@ export function createGroupedHoldingsSectionWithRelative({ list, all, title }) { ), ...mapCohorts(list, ({ name, color, tree }) => line({ - metric: tree.relative.supplyRelToCirculatingSupply, + metric: tree.supply.relToCirculating.percent, name, color, unit: Unit.pctSupply, @@ -902,14 +754,14 @@ export function createGroupedHoldingsSectionWithRelative({ list, all, title }) { bottom: [ ...flatMapCohortsWithAll(list, all, ({ name, color, tree }) => satsBtcUsd({ - pattern: tree.unrealized.supplyInProfit, + pattern: tree.supply.inProfit, name, color, }), ), ...mapCohorts(list, ({ name, color, tree }) => line({ - metric: tree.relative.supplyInProfitRelToCirculatingSupply, + metric: tree.supply.inProfit.relToCirculating.percent, name, color, unit: Unit.pctSupply, @@ -917,7 +769,7 @@ export function createGroupedHoldingsSectionWithRelative({ list, all, title }) { ), ...mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ - metric: tree.relative.supplyInProfitRelToOwnSupply, + metric: tree.supply.inProfit.relToOwn.percent, name, color, unit: Unit.pctOwn, @@ -932,14 +784,14 @@ export function createGroupedHoldingsSectionWithRelative({ list, all, title }) { bottom: [ ...flatMapCohortsWithAll(list, all, ({ name, color, tree }) => satsBtcUsd({ - pattern: tree.unrealized.supplyInLoss, + pattern: tree.supply.inLoss, name, color, }), ), ...mapCohorts(list, ({ name, color, tree }) => line({ - metric: tree.relative.supplyInLossRelToCirculatingSupply, + metric: tree.supply.inLoss.relToCirculating.percent, name, color, unit: Unit.pctSupply, @@ -947,7 +799,7 @@ export function createGroupedHoldingsSectionWithRelative({ list, all, title }) { ), ...mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ - metric: tree.relative.supplyInLossRelToOwnSupply, + metric: tree.supply.inLoss.relToOwn.percent, name, color, unit: Unit.pctOwn, diff --git a/website/scripts/options/distribution/index.js b/website/scripts/options/distribution/index.js index 93975c547..2f8c0be95 100644 --- a/website/scripts/options/distribution/index.js +++ b/website/scripts/options/distribution/index.js @@ -38,9 +38,7 @@ import { createGroupedPricesSection, } from "./prices.js"; import { - createCostBasisSection, createCostBasisSectionWithPercentiles, - createGroupedCostBasisSection, createGroupedCostBasisSectionWithPercentiles, } from "./cost-basis.js"; import { @@ -60,8 +58,12 @@ import { import { createActivitySection, createActivitySectionWithAdjusted, + createActivitySectionWithActivity, + createActivitySectionMinimal, createGroupedActivitySection, createGroupedActivitySectionWithAdjusted, + createGroupedActivitySectionWithActivity, + createGroupedActivitySectionMinimal, } from "./activity.js"; // Re-export data builder @@ -121,12 +123,11 @@ export function createCohortFolderWithAdjusted(cohort) { return { name: cohort.name || "all", tree: [ - createHoldingsSectionWithRelative({ cohort, title }), + createHoldingsSectionWithOwnSupply({ cohort, title }), createValuationSection({ cohort, title }), createPricesSectionBasic({ cohort, title }), - createCostBasisSection({ cohort, title }), createProfitabilitySectionWithNupl({ cohort, title }), - createActivitySectionWithAdjusted({ cohort, title }), + createActivitySectionWithActivity({ cohort, title }), ], }; } @@ -182,11 +183,10 @@ export function createCohortFolderAgeRange(cohort) { name: cohort.name || "all", tree: [ createHoldingsSectionWithOwnSupply({ cohort, title }), - createValuationSectionFull({ cohort, title }), - createPricesSectionFull({ cohort, title }), - createCostBasisSectionWithPercentiles({ cohort, title }), + createValuationSection({ cohort, title }), + createPricesSectionBasic({ cohort, title }), createProfitabilitySectionWithInvestedCapitalPct({ cohort, title }), - createActivitySection({ cohort, title }), + createActivitySectionWithActivity({ cohort, title }), ], }; } @@ -201,12 +201,11 @@ export function createCohortFolderBasicWithMarketCap(cohort) { return { name: cohort.name || "all", tree: [ - createHoldingsSectionWithRelative({ cohort, title }), + createHoldingsSection({ cohort, title }), createValuationSection({ cohort, title }), createPricesSectionBasic({ cohort, title }), - createCostBasisSection({ cohort, title }), createProfitabilitySectionWithNupl({ cohort, title }), - createActivitySection({ cohort, title }), + createActivitySectionMinimal({ cohort, title }), ], }; } @@ -221,12 +220,11 @@ export function createCohortFolderBasicWithoutMarketCap(cohort) { return { name: cohort.name || "all", tree: [ - createHoldingsSectionWithOwnSupply({ cohort, title }), + createHoldingsSection({ cohort, title }), createValuationSection({ cohort, title }), createPricesSectionBasic({ cohort, title }), - createCostBasisSection({ cohort, title }), createProfitabilitySectionBasicWithInvestedCapitalPct({ cohort, title }), - createActivitySection({ cohort, title }), + createActivitySectionMinimal({ cohort, title }), ], }; } @@ -244,9 +242,8 @@ export function createCohortFolderAddress(cohort) { createHoldingsSectionAddress({ cohort, title }), createValuationSection({ cohort, title }), createPricesSectionBasic({ cohort, title }), - createCostBasisSection({ cohort, title }), createProfitabilitySectionBasicWithInvestedCapitalPct({ cohort, title }), - createActivitySection({ cohort, title }), + createActivitySectionMinimal({ cohort, title }), ], }; } @@ -264,9 +261,8 @@ export function createCohortFolderWithoutRelative(cohort) { createHoldingsSection({ cohort, title }), createValuationSection({ cohort, title }), createPricesSectionBasic({ cohort, title }), - createCostBasisSection({ cohort, title }), createProfitabilitySection({ cohort, title }), - createActivitySection({ cohort, title }), + createActivitySectionMinimal({ cohort, title }), ], }; } @@ -284,9 +280,8 @@ export function createAddressCohortFolder(cohort) { createHoldingsSectionAddressAmount({ cohort, title }), createValuationSection({ cohort, title }), createPricesSectionBasic({ cohort, title }), - createCostBasisSection({ cohort, title }), createProfitabilitySectionWithNupl({ cohort, title }), - createActivitySection({ cohort, title }), + createActivitySectionMinimal({ cohort, title }), ], }; } @@ -333,12 +328,11 @@ export function createGroupedCohortFolderWithAdjusted({ return { name: name || "all", tree: [ - createGroupedHoldingsSectionWithRelative({ list, all, title }), + createGroupedHoldingsSectionWithOwnSupply({ list, all, title }), createGroupedValuationSection({ list, all, title }), createGroupedPricesSection({ list, all, title }), - createGroupedCostBasisSection({ list, all, title }), - createGroupedProfitabilitySectionWithNupl({ list, all, title }), - createGroupedActivitySectionWithAdjusted({ list, all, title }), + createGroupedProfitabilitySectionWithInvestedCapitalPct({ list, all, title }), + createGroupedActivitySectionWithActivity({ list, all, title }), ], }; } @@ -406,15 +400,14 @@ export function createGroupedCohortFolderAgeRange({ name: name || "all", tree: [ createGroupedHoldingsSectionWithOwnSupply({ list, all, title }), - createGroupedValuationSectionWithOwnMarketCap({ list, all, title }), + createGroupedValuationSection({ list, all, title }), createGroupedPricesSection({ list, all, title }), - createGroupedCostBasisSectionWithPercentiles({ list, all, title }), createGroupedProfitabilitySectionWithInvestedCapitalPct({ list, all, title, }), - createGroupedActivitySection({ list, all, title }), + createGroupedActivitySectionWithActivity({ list, all, title }), ], }; } @@ -433,12 +426,11 @@ export function createGroupedCohortFolderBasicWithMarketCap({ return { name: name || "all", tree: [ - createGroupedHoldingsSectionWithRelative({ list, all, title }), + createGroupedHoldingsSection({ list, all, title }), createGroupedValuationSection({ list, all, title }), createGroupedPricesSection({ list, all, title }), - createGroupedCostBasisSection({ list, all, title }), - createGroupedProfitabilitySectionWithNupl({ list, all, title }), - createGroupedActivitySection({ list, all, title }), + createGroupedProfitabilitySection({ list, all, title }), + createGroupedActivitySectionMinimal({ list, all, title }), ], }; } @@ -457,16 +449,15 @@ export function createGroupedCohortFolderBasicWithoutMarketCap({ return { name: name || "all", tree: [ - createGroupedHoldingsSectionWithOwnSupply({ list, all, title }), + createGroupedHoldingsSection({ list, all, title }), createGroupedValuationSection({ list, all, title }), createGroupedPricesSection({ list, all, title }), - createGroupedCostBasisSection({ list, all, title }), createGroupedProfitabilitySectionBasicWithInvestedCapitalPct({ list, all, title, }), - createGroupedActivitySection({ list, all, title }), + createGroupedActivitySectionMinimal({ list, all, title }), ], }; } @@ -488,13 +479,12 @@ export function createGroupedCohortFolderAddress({ createGroupedHoldingsSectionAddress({ list, all, title }), createGroupedValuationSection({ list, all, title }), createGroupedPricesSection({ list, all, title }), - createGroupedCostBasisSection({ list, all, title }), createGroupedProfitabilitySectionBasicWithInvestedCapitalPct({ list, all, title, }), - createGroupedActivitySection({ list, all, title }), + createGroupedActivitySectionMinimal({ list, all, title }), ], }; } @@ -516,9 +506,8 @@ export function createGroupedCohortFolderWithoutRelative({ createGroupedHoldingsSection({ list, all, title }), createGroupedValuationSection({ list, all, title }), createGroupedPricesSection({ list, all, title }), - createGroupedCostBasisSection({ list, all, title }), createGroupedProfitabilitySection({ list, all, title }), - createGroupedActivitySection({ list, all, title }), + createGroupedActivitySectionMinimal({ list, all, title }), ], }; } @@ -540,9 +529,8 @@ export function createGroupedAddressCohortFolder({ createGroupedHoldingsSectionAddressAmount({ list, all, title }), createGroupedValuationSection({ list, all, title }), createGroupedPricesSection({ list, all, title }), - createGroupedCostBasisSection({ list, all, title }), - createGroupedProfitabilitySectionWithNupl({ list, all, title }), - createGroupedActivitySection({ list, all, title }), + createGroupedProfitabilitySection({ list, all, title }), + createGroupedActivitySectionMinimal({ list, all, title }), ], }; } diff --git a/website/scripts/options/distribution/prices.js b/website/scripts/options/distribution/prices.js index fb15905b4..4a72efd70 100644 --- a/website/scripts/options/distribution/prices.js +++ b/website/scripts/options/distribution/prices.js @@ -19,47 +19,9 @@ import { baseline, price } from "../series.js"; import { Unit } from "../../utils/units.js"; /** - * @param {{ realized: { realizedPrice: ActivePricePattern, investorPrice: ActivePricePattern, lowerPriceBand: ActivePricePattern, upperPriceBand: ActivePricePattern } }} tree - * @param {(metric: string) => string} title - * @returns {PartialChartOption} - */ -function createCompareChart(tree, title) { - return { - name: "Compare", - title: title("Prices"), - top: [ - price({ - metric: tree.realized.realizedPrice, - name: "Realized", - color: colors.realized, - }), - price({ - metric: tree.realized.investorPrice, - name: "Investor", - color: colors.investor, - }), - price({ - metric: tree.realized.upperPriceBand, - name: "I²/R", - color: colors.stat.max, - style: 2, - defaultActive: false, - }), - price({ - metric: tree.realized.lowerPriceBand, - name: "R²/I", - color: colors.stat.min, - style: 2, - defaultActive: false, - }), - ], - }; -} - -/** - * Create prices section for cohorts with full ActivePriceRatioPattern - * (CohortAll, CohortFull, CohortWithPercentiles) - * @param {{ cohort: CohortAll | CohortFull | CohortWithPercentiles, title: (metric: string) => string }} args + * Create prices section for cohorts with full ratio patterns + * (CohortAll, CohortFull, CohortLongTerm) + * @param {{ cohort: CohortAll | CohortFull | CohortLongTerm, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ export function createPricesSectionFull({ cohort, title }) { @@ -67,14 +29,23 @@ export function createPricesSectionFull({ cohort, title }) { return { name: "Prices", tree: [ - createCompareChart(tree, title), + { + name: "Compare", + title: title("Prices"), + top: [ + price({ metric: tree.realized.price, name: "Realized", color: colors.realized }), + price({ metric: tree.realized.investor.price, name: "Investor", color: colors.investor }), + price({ metric: tree.realized.investor.upperPriceBand, name: "I²/R", color: colors.stat.max, style: 2, defaultActive: false }), + price({ metric: tree.realized.investor.lowerPriceBand, name: "R²/I", color: colors.stat.min, style: 2, defaultActive: false }), + ], + }, { name: "Realized", tree: createPriceRatioCharts({ context: cohort.name, legend: "Realized", - pricePattern: tree.realized.realizedPrice, - ratio: { ...tree.realized.realizedPriceExtra, ...tree.realized.realizedPriceRatioExt }, + pricePattern: tree.realized.price, + ratio: tree.realized.price, color, priceTitle: title("Realized Price"), titlePrefix: "Realized Price", @@ -82,52 +53,19 @@ export function createPricesSectionFull({ cohort, title }) { }, { name: "Investor", - tree: createPriceRatioCharts({ - context: cohort.name, - legend: "Investor", - pricePattern: tree.realized.investorPrice, - ratio: { ...tree.realized.investorPriceExtra, ...tree.realized.investorPriceRatioExt }, - color, - priceTitle: title("Investor Price"), - titlePrefix: "Investor Price", - }), - }, - ], - }; -} - -/** - * Create prices section for cohorts with basic ratio patterns only - * (CohortWithAdjusted, CohortBasic, CohortAddress, CohortWithoutRelative) - * @param {{ cohort: CohortWithAdjusted | CohortBasic | CohortAddress | CohortWithoutRelative, title: (metric: string) => string }} args - * @returns {PartialOptionsGroup} - */ -export function createPricesSectionBasic({ cohort, title }) { - const { tree, color } = cohort; - return { - name: "Prices", - tree: [ - createCompareChart(tree, title), - { - name: "Realized", tree: [ { name: "Price", - title: title("Realized Price"), - top: [ - price({ - metric: tree.realized.realizedPrice, - name: "Realized", - color, - }), - ], + title: title("Investor Price"), + top: [price({ metric: tree.realized.investor.price, name: "Investor", color })], }, { name: "Ratio", - title: title("Realized Price Ratio"), + title: title("Investor Price Ratio"), + top: [price({ metric: tree.realized.investor.price, name: "Investor", color })], bottom: [ baseline({ - metric: tree.realized.realizedPriceExtra.ratio, + metric: tree.realized.investor.price.ratio, name: "Ratio", unit: Unit.ratio, base: 1, @@ -136,27 +74,36 @@ export function createPricesSectionBasic({ cohort, title }) { }, ], }, + ], + }; +} + +/** + * Create prices section for cohorts with basic ratio patterns only + * (CohortWithAdjusted, CohortBasic, CohortAddress, CohortWithoutRelative) + * @param {{ cohort: CohortWithAdjusted | CohortBasic | CohortAddress | CohortWithoutRelative | CohortAgeRange, title: (metric: string) => string }} args + * @returns {PartialOptionsGroup} + */ +export function createPricesSectionBasic({ cohort, title }) { + const { tree, color } = cohort; + return { + name: "Prices", + tree: [ { - name: "Investor", + name: "Realized", tree: [ { name: "Price", - title: title("Investor Price"), - top: [ - price({ - metric: tree.realized.investorPrice, - name: "Investor", - color, - }), - ], + title: title("Realized Price"), + top: [price({ metric: tree.realized.price, name: "Realized", color })], }, { name: "Ratio", - title: title("Investor Price Ratio"), + title: title("Realized Price Ratio"), bottom: [ baseline({ - metric: tree.realized.investorPriceExtra.ratio, - name: "Ratio", + metric: tree.realized.mvrv, + name: "MVRV", unit: Unit.ratio, base: 1, }), @@ -184,40 +131,15 @@ export function createGroupedPricesSection({ list, all, title }) { name: "Price", title: title("Realized Price"), top: mapCohortsWithAll(list, all, ({ name, color, tree }) => - price({ metric: tree.realized.realizedPrice, name, color }), + price({ metric: tree.realized.price, name, color }), ), }, { name: "Ratio", - title: title("Realized Price Ratio"), + title: title("MVRV"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ - metric: tree.realized.realizedPriceExtra.ratio, - name, - color, - unit: Unit.ratio, - base: 1, - }), - ), - }, - ], - }, - { - name: "Investor", - tree: [ - { - name: "Price", - title: title("Investor Price"), - top: mapCohortsWithAll(list, all, ({ name, color, tree }) => - price({ metric: tree.realized.investorPrice, name, color }), - ), - }, - { - name: "Ratio", - title: title("Investor Price Ratio"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.realized.investorPriceExtra.ratio, + metric: tree.realized.mvrv, name, color, unit: Unit.ratio, diff --git a/website/scripts/options/distribution/profitability.js b/website/scripts/options/distribution/profitability.js index dafaf312e..cb3cc5295 100644 --- a/website/scripts/options/distribution/profitability.js +++ b/website/scripts/options/distribution/profitability.js @@ -1,5 +1,13 @@ /** * Profitability section builders + * + * Capability tiers: + * - Full (All/STH/LTH): full unrealized with rel metrics, invested capital, sentiment; + * full realized with relToRcap, peakRegret, profitToLossRatio, grossPnl + * - Mid (AgeRange/MaxAge): unrealized profit/loss/netPnl/nupl (no rel, no invested, no sentiment); + * realized with netPnl + delta (no relToRcap, no peakRegret) + * - Basic (UtxoAmount, Empty, Address): nupl only unrealized; + * basic realized profit/loss (no netPnl, no relToRcap) */ import { Unit } from "../../utils/units.js"; @@ -7,15 +15,12 @@ import { line, baseline, dots, dotsBaseline } from "../series.js"; import { colors } from "../../utils/colors.js"; import { priceLine, priceLines } from "../constants.js"; import { - satsBtcUsd, - satsBtcUsdFrom, mapCohorts, mapCohortsWithAll, - flatMapCohortsWithAll, } from "../shared.js"; // ============================================================================ -// Core Series Builders (Composable Primitives) +// Core Series Builders // ============================================================================ /** @@ -23,49 +28,27 @@ import { * @property {AnyMetricPattern} profit * @property {AnyMetricPattern} loss * @property {AnyMetricPattern} negLoss - * @property {AnyMetricPattern} [total] + * @property {AnyMetricPattern} [gross] */ /** - * Create profit/loss line series for a unit - * @param {PnlSeriesConfig} metrics + * @param {PnlSeriesConfig} m * @param {Unit} unit * @returns {AnyFetchedSeriesBlueprint[]} */ -function pnlLines(metrics, unit) { +function pnlLines(m, unit) { const series = [ - line({ - metric: metrics.profit, - name: "Profit", - color: colors.profit, - unit, - }), - line({ metric: metrics.loss, name: "Loss", color: colors.loss, unit }), + line({ metric: m.profit, name: "Profit", color: colors.profit, unit }), + line({ metric: m.loss, name: "Loss", color: colors.loss, unit }), ]; - if (metrics.total) { - series.push( - line({ - metric: metrics.total, - name: "Total", - color: colors.default, - unit, - }), - ); + if (m.gross) { + series.push(line({ metric: m.gross, name: "Total", color: colors.default, unit })); } - series.push( - line({ - metric: metrics.negLoss, - name: "Negative Loss", - color: colors.loss, - unit, - defaultActive: false, - }), - ); + series.push(line({ metric: m.negLoss, name: "Negative Loss", color: colors.loss, unit, defaultActive: false })); return series; } /** - * Create net P&L baseline * @param {AnyMetricPattern} metric * @param {Unit} unit * @returns {AnyFetchedSeriesBlueprint} @@ -79,197 +62,84 @@ function netBaseline(metric, unit) { // ============================================================================ /** - * @typedef {Object} UnrealizedMetrics - * @property {AnyMetricPattern} profit - * @property {AnyMetricPattern} loss - * @property {AnyMetricPattern} negLoss - * @property {AnyMetricPattern} total - * @property {AnyMetricPattern} net - */ - -/** - * Extract unrealized metrics from tree - * @param {{ unrealized: UnrealizedPattern }} tree - * @returns {UnrealizedMetrics} - */ -function getUnrealizedMetrics(tree) { - return { - profit: tree.unrealized.unrealizedProfit, - loss: tree.unrealized.unrealizedLoss, - negLoss: tree.unrealized.negUnrealizedLoss, - total: tree.unrealized.totalUnrealizedPnl, - net: tree.unrealized.netUnrealizedPnl, - }; -} - -/** - * Base unrealized P&L (USD only) - * @param {UnrealizedMetrics} m + * Unrealized P&L (USD + relToMcap) for All cohort + * @param {Brk.MetricsTree_Cohorts_Utxo_All_Unrealized} u * @returns {AnyFetchedSeriesBlueprint[]} */ -function unrealizedUsd(m) { +function unrealizedAll(u) { return [ ...pnlLines( - { profit: m.profit, loss: m.loss, negLoss: m.negLoss, total: m.total }, + { profit: u.profit.base.usd, loss: u.loss.base.usd, negLoss: u.loss.negative, gross: u.grossPnl.usd }, Unit.usd, ), priceLine({ unit: Unit.usd, defaultActive: false }), - ]; -} - -/** - * Unrealized P&L with % of Market Cap - * @param {UnrealizedMetrics} m - * @param {RelativeWithNupl} rel - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function unrealizedWithMarketCap(m, rel) { - return [ - ...unrealizedUsd(m), - ...pnlLines( - { - profit: rel.unrealizedProfitRelToMarketCap, - loss: rel.unrealizedLossRelToMarketCap, - negLoss: rel.negUnrealizedLossRelToMarketCap, - }, - Unit.pctMcap, - ), + line({ metric: u.profit.relToMcap.ratio, name: "Profit", color: colors.profit, unit: Unit.pctMcap }), + line({ metric: u.loss.relToMcap.ratio, name: "Loss", color: colors.loss, unit: Unit.pctMcap }), priceLine({ unit: Unit.pctMcap, defaultActive: false }), - ]; -} - -/** - * Unrealized P&L with % of Own Market Cap + % of Own P&L - * @param {UnrealizedMetrics} m - * @param {RelativeWithOwnMarketCap} rel - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function unrealizedWithOwnMarketCap(m, rel) { - return [ - ...unrealizedUsd(m), - ...pnlLines( - { - profit: rel.unrealizedProfitRelToOwnMarketCap, - loss: rel.unrealizedLossRelToOwnMarketCap, - negLoss: rel.negUnrealizedLossRelToOwnMarketCap, - }, - Unit.pctOwnMcap, - ), - priceLine({ unit: Unit.pctOwnMcap, defaultActive: false }), - ...pnlLines( - { - profit: rel.unrealizedProfitRelToOwnTotalUnrealizedPnl, - loss: rel.unrealizedLossRelToOwnTotalUnrealizedPnl, - negLoss: rel.negUnrealizedLossRelToOwnTotalUnrealizedPnl, - }, - Unit.pctOwnPnl, - ), + line({ metric: u.profit.relToOwnGross.ratio, name: "Profit", color: colors.profit, unit: Unit.pctOwnPnl }), + line({ metric: u.loss.relToOwnGross.ratio, name: "Loss", color: colors.loss, unit: Unit.pctOwnPnl }), ...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwnPnl }), ]; } /** - * Unrealized P&L for "all" cohort (% M.Cap + % Own P&L, no Own M.Cap) - * @param {UnrealizedMetrics} m - * @param {AllRelativePattern} rel + * Unrealized P&L (USD + relToMcap + relToOwnMcap + relToOwnGross) for Full cohorts (STH/LTH) + * @param {Brk.GrossInvestedLossNetNuplProfitSentimentPattern2} u * @returns {AnyFetchedSeriesBlueprint[]} */ -function unrealizedAll(m, rel) { +function unrealizedFull(u) { return [ - ...unrealizedUsd(m), ...pnlLines( - { - profit: rel.unrealizedProfitRelToMarketCap, - loss: rel.unrealizedLossRelToMarketCap, - negLoss: rel.negUnrealizedLossRelToMarketCap, - }, - Unit.pctMcap, + { profit: u.profit.base.usd, loss: u.loss.base.usd, negLoss: u.loss.negative, gross: u.grossPnl.usd }, + Unit.usd, ), + priceLine({ unit: Unit.usd, defaultActive: false }), + line({ metric: u.profit.relToMcap.ratio, name: "Profit", color: colors.profit, unit: Unit.pctMcap }), + line({ metric: u.loss.relToMcap.ratio, name: "Loss", color: colors.loss, unit: Unit.pctMcap }), priceLine({ unit: Unit.pctMcap, defaultActive: false }), - ...pnlLines( - { - profit: rel.unrealizedProfitRelToOwnTotalUnrealizedPnl, - loss: rel.unrealizedLossRelToOwnTotalUnrealizedPnl, - negLoss: rel.negUnrealizedLossRelToOwnTotalUnrealizedPnl, - }, - Unit.pctOwnPnl, - ), + line({ metric: u.profit.relToOwnMcap.ratio, name: "Profit", color: colors.profit, unit: Unit.pctOwnMcap }), + line({ metric: u.loss.relToOwnMcap.ratio, name: "Loss", color: colors.loss, unit: Unit.pctOwnMcap }), + priceLine({ unit: Unit.pctOwnMcap, defaultActive: false }), + line({ metric: u.profit.relToOwnGross.ratio, name: "Profit", color: colors.profit, unit: Unit.pctOwnPnl }), + line({ metric: u.loss.relToOwnGross.ratio, name: "Loss", color: colors.loss, unit: Unit.pctOwnPnl }), ...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwnPnl }), ]; } /** - * Unrealized P&L for Full cohorts (all relative metrics) - * @param {UnrealizedMetrics} m - * @param {FullRelativePattern} rel + * Unrealized P&L for LTH (loss relToMcap only + relToOwnMcap + relToOwnGross) + * @param {Brk.GrossInvestedLossNetNuplProfitSentimentPattern2} u * @returns {AnyFetchedSeriesBlueprint[]} */ -function unrealizedFull(m, rel) { +function unrealizedLongTerm(u) { return [ - ...unrealizedUsd(m), ...pnlLines( - { - profit: rel.unrealizedProfitRelToMarketCap, - loss: rel.unrealizedLossRelToMarketCap, - negLoss: rel.negUnrealizedLossRelToMarketCap, - }, - Unit.pctMcap, - ), - priceLine({ unit: Unit.pctMcap, defaultActive: false }), - ...pnlLines( - { - profit: rel.unrealizedProfitRelToOwnMarketCap, - loss: rel.unrealizedLossRelToOwnMarketCap, - negLoss: rel.negUnrealizedLossRelToOwnMarketCap, - }, - Unit.pctOwnMcap, + { profit: u.profit.base.usd, loss: u.loss.base.usd, negLoss: u.loss.negative, gross: u.grossPnl.usd }, + Unit.usd, ), + priceLine({ unit: Unit.usd, defaultActive: false }), + line({ metric: u.loss.relToMcap.ratio, name: "Loss", color: colors.loss, unit: Unit.pctMcap }), + line({ metric: u.profit.relToOwnMcap.ratio, name: "Profit", color: colors.profit, unit: Unit.pctOwnMcap }), + line({ metric: u.loss.relToOwnMcap.ratio, name: "Loss", color: colors.loss, unit: Unit.pctOwnMcap }), priceLine({ unit: Unit.pctOwnMcap, defaultActive: false }), - ...pnlLines( - { - profit: rel.unrealizedProfitRelToOwnTotalUnrealizedPnl, - loss: rel.unrealizedLossRelToOwnTotalUnrealizedPnl, - negLoss: rel.negUnrealizedLossRelToOwnTotalUnrealizedPnl, - }, - Unit.pctOwnPnl, - ), + line({ metric: u.profit.relToOwnGross.ratio, name: "Profit", color: colors.profit, unit: Unit.pctOwnPnl }), + line({ metric: u.loss.relToOwnGross.ratio, name: "Loss", color: colors.loss, unit: Unit.pctOwnPnl }), ...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwnPnl }), ]; } /** - * Unrealized P&L for LongTerm (% M.Cap loss only + Own M.Cap + Own P&L) - * @param {UnrealizedMetrics} m - * @param {RelativeWithOwnMarketCap & RelativeWithNupl} rel + * Unrealized P&L (USD only) for mid-tier cohorts (AgeRange/MaxAge) + * @param {Brk.LossNetNuplProfitPattern} u * @returns {AnyFetchedSeriesBlueprint[]} */ -function unrealizedLongTerm(m, rel) { +function unrealizedMid(u) { return [ - ...unrealizedUsd(m), - line({ - metric: rel.unrealizedLossRelToMarketCap, - name: "Loss", - color: colors.loss, - unit: Unit.pctMcap, - }), ...pnlLines( - { - profit: rel.unrealizedProfitRelToOwnMarketCap, - loss: rel.unrealizedLossRelToOwnMarketCap, - negLoss: rel.negUnrealizedLossRelToOwnMarketCap, - }, - Unit.pctOwnMcap, + { profit: u.profit.base.usd, loss: u.loss.base.usd, negLoss: u.loss.negative }, + Unit.usd, ), - priceLine({ unit: Unit.pctOwnMcap, defaultActive: false }), - ...pnlLines( - { - profit: rel.unrealizedProfitRelToOwnTotalUnrealizedPnl, - loss: rel.unrealizedLossRelToOwnTotalUnrealizedPnl, - negLoss: rel.negUnrealizedLossRelToOwnTotalUnrealizedPnl, - }, - Unit.pctOwnPnl, - ), - ...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwnPnl }), + priceLine({ unit: Unit.usd, defaultActive: false }), ]; } @@ -278,466 +148,140 @@ function unrealizedLongTerm(m, rel) { // ============================================================================ /** - * Net P&L (USD only) - * @param {AnyMetricPattern} net + * Net P&L for All cohort + * @param {Brk.MetricsTree_Cohorts_Utxo_All_Unrealized} u * @returns {AnyFetchedSeriesBlueprint[]} */ -function netUnrealizedUsd(net) { - return [netBaseline(net, Unit.usd)]; -} - -/** - * Net P&L with % of Market Cap - * @param {AnyMetricPattern} net - * @param {RelativeWithNupl} rel - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function netUnrealizedWithMarketCap(net, rel) { +function netUnrealizedAll(u) { return [ - netBaseline(net, Unit.usd), - netBaseline(rel.netUnrealizedPnlRelToMarketCap, Unit.pctMcap), + netBaseline(u.netPnl.usd, Unit.usd), + netBaseline(u.netPnl.relToOwnGross.ratio, Unit.pctOwnPnl), ]; } /** - * Net P&L with % of Own Market Cap + % of Own P&L - * @param {AnyMetricPattern} net - * @param {RelativeWithOwnMarketCap} rel + * Net P&L for Full cohorts (STH/LTH) + * @param {Brk.GrossInvestedLossNetNuplProfitSentimentPattern2} u * @returns {AnyFetchedSeriesBlueprint[]} */ -function netUnrealizedWithOwnMarketCap(net, rel) { +function netUnrealizedFull(u) { return [ - netBaseline(net, Unit.usd), - netBaseline(rel.netUnrealizedPnlRelToOwnMarketCap, Unit.pctOwnMcap), - netBaseline(rel.netUnrealizedPnlRelToOwnTotalUnrealizedPnl, Unit.pctOwnPnl), + netBaseline(u.netPnl.usd, Unit.usd), + netBaseline(u.netPnl.relToOwnMcap.ratio, Unit.pctOwnMcap), + netBaseline(u.netPnl.relToOwnGross.ratio, Unit.pctOwnPnl), ]; } /** - * Net P&L for "all" cohort - * @param {AnyMetricPattern} net - * @param {AllRelativePattern} rel + * Net P&L for mid-tier cohorts + * @param {Brk.LossNetNuplProfitPattern} u * @returns {AnyFetchedSeriesBlueprint[]} */ -function netUnrealizedAll(net, rel) { - return [ - netBaseline(net, Unit.usd), - netBaseline(rel.netUnrealizedPnlRelToMarketCap, Unit.pctMcap), - netBaseline(rel.netUnrealizedPnlRelToOwnTotalUnrealizedPnl, Unit.pctOwnPnl), - ]; -} - -/** - * Net P&L for Full cohorts - * @param {AnyMetricPattern} net - * @param {FullRelativePattern} rel - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function netUnrealizedFull(net, rel) { - return [ - netBaseline(net, Unit.usd), - netBaseline(rel.netUnrealizedPnlRelToMarketCap, Unit.pctMcap), - netBaseline(rel.netUnrealizedPnlRelToOwnMarketCap, Unit.pctOwnMcap), - netBaseline(rel.netUnrealizedPnlRelToOwnTotalUnrealizedPnl, Unit.pctOwnPnl), - ]; +function netUnrealizedMid(u) { + return [netBaseline(u.netPnl.usd, Unit.usd)]; } // ============================================================================ -// Invested Capital & Other Unrealized +// Invested Capital, Sentiment, NUPL // ============================================================================ /** - * Invested capital (USD only) - * @param {{ unrealized: UnrealizedPattern }} tree + * Invested capital (Full unrealized only) + * @param {Brk.GrossInvestedLossNetNuplProfitSentimentPattern2 | Brk.MetricsTree_Cohorts_Utxo_All_Unrealized} u * @returns {AnyFetchedSeriesBlueprint[]} */ -function investedCapitalAbsolute(tree) { +function investedCapitalSeries(u) { 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, - }), + line({ metric: u.investedCapital.inProfit.usd, name: "In Profit", color: colors.profit, unit: Unit.usd }), + line({ metric: u.investedCapital.inLoss.usd, name: "In Loss", color: colors.loss, unit: Unit.usd }), ]; } /** - * Invested capital with % of Own R.Cap - * @param {{ unrealized: UnrealizedPattern, relative: RelativeWithInvestedCapitalPct }} tree + * Sentiment (Full unrealized only) + * @param {Brk.GrossInvestedLossNetNuplProfitSentimentPattern2 | Brk.MetricsTree_Cohorts_Utxo_All_Unrealized} u * @returns {AnyFetchedSeriesBlueprint[]} */ -function investedCapitalWithPct(tree) { +function sentimentSeries(u) { return [ - ...investedCapitalAbsolute(tree), - 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 }), + baseline({ metric: u.sentiment.net.usd, name: "Net Sentiment", unit: Unit.usd }), + line({ metric: u.sentiment.greedIndex.usd, name: "Greed Index", color: colors.profit, unit: Unit.usd, defaultActive: false }), + line({ metric: u.sentiment.painIndex.usd, name: "Pain Index", color: colors.loss, unit: Unit.usd, defaultActive: false }), ]; } /** * NUPL series - * @param {RelativeWithNupl} rel + * @param {Brk.BpsRatioPattern} nupl * @returns {AnyFetchedSeriesBlueprint[]} */ -function nuplSeries(rel) { - return [baseline({ metric: rel.nupl, name: "NUPL", unit: Unit.ratio })]; -} - -/** - * Sentiment series - * @param {{ unrealized: UnrealizedPattern }} tree - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function sentimentSeries(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, - }), - ]; -} - -/** - * Sentiment chart for single cohort - * @param {{ unrealized: UnrealizedPattern }} tree - * @param {(metric: string) => string} title - * @returns {PartialChartOption} - */ -function sentimentChart(tree, title) { - return { - name: "Sentiment", - title: title("Market Sentiment"), - bottom: sentimentSeries(tree), - }; -} - -/** - * Volume subfolder for single cohort - * @param {{ realized: AnyRealizedPattern }} tree - * @param {(metric: string) => string} title - * @returns {PartialOptionsGroup} - */ -function volumeSubfolder(tree, title) { - return { name: "Volume", tree: sentInPnlTree(tree, title) }; +function nuplSeries(nupl) { + return [baseline({ metric: nupl.ratio, name: "NUPL", unit: Unit.ratio })]; } // ============================================================================ -// Realized P&L Builders +// Realized P&L Builders — Full (All/STH/LTH) // ============================================================================ /** - * Realized P&L sum series - * @param {{ realized: AnyRealizedPattern }} tree + * @param {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern | Brk.MetricsTree_Cohorts_Utxo_Lth_Realized} r * @returns {AnyFetchedSeriesBlueprint[]} */ -function realizedPnlSum(tree) { - const r = tree.realized; +function realizedPnlSumFull(r) { return [ - line({ - metric: r.realizedProfit7dEma, - name: "Profit 7d EMA", - color: colors.profit, - unit: Unit.usd, - }), - line({ - metric: r.realizedLoss7dEma, - name: "Loss 7d EMA", - color: colors.loss, - unit: Unit.usd, - }), - dots({ - metric: r.realizedProfit.height, - name: "Profit", - color: colors.profit, - unit: Unit.usd, - defaultActive: false, - }), - dots({ - metric: r.negRealizedLoss, - name: "Negative Loss", - color: colors.loss, - unit: Unit.usd, - defaultActive: false, - }), - dots({ - metric: r.realizedLoss.height, - name: "Loss", - color: colors.loss, - unit: Unit.usd, - defaultActive: false, - }), - dots({ - metric: r.realizedValue, - name: "Value", - color: colors.default, - unit: Unit.usd, - defaultActive: false, - }), - baseline({ - metric: r.realizedProfitRelToRealizedCap, - name: "Profit", - color: colors.profit, - unit: Unit.pctRcap, - }), - baseline({ - metric: r.realizedLossRelToRealizedCap, - name: "Loss", - color: colors.loss, - unit: Unit.pctRcap, - }), + dots({ metric: r.profit.base.usd, name: "Profit", color: colors.profit, unit: Unit.usd }), + dots({ metric: r.loss.negative, name: "Negative Loss", color: colors.loss, unit: Unit.usd, defaultActive: false }), + dots({ metric: r.loss.base.usd, name: "Loss", color: colors.loss, unit: Unit.usd, defaultActive: false }), + baseline({ metric: r.profit.relToRcap.ratio, name: "Profit", color: colors.profit, unit: Unit.pctRcap }), + baseline({ metric: r.loss.relToRcap.ratio, name: "Loss", color: colors.loss, unit: Unit.pctRcap }), ]; } /** - * Realized Net P&L sum series - * @param {{ realized: AnyRealizedPattern }} tree + * @param {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern | Brk.MetricsTree_Cohorts_Utxo_Lth_Realized} r * @returns {AnyFetchedSeriesBlueprint[]} */ -function realizedNetPnlSum(tree) { - const r = tree.realized; +function realizedNetPnlSumFull(r) { return [ - baseline({ - metric: r.netRealizedPnl7dEma, - name: "Net 7d EMA", - unit: Unit.usd, - }), - dotsBaseline({ - metric: r.netRealizedPnl.height, - name: "Net", - unit: Unit.usd, - defaultActive: false, - }), - baseline({ - metric: r.netRealizedPnlRelToRealizedCap, - name: "Net", - unit: Unit.pctRcap, - }), + dotsBaseline({ metric: r.netPnl.base.usd, name: "Net", unit: Unit.usd, defaultActive: false }), + baseline({ metric: r.netPnl.relToRcap.ratio, name: "Net", unit: Unit.pctRcap }), ]; } /** - * Realized P&L cumulative series - * @param {{ realized: AnyRealizedPattern }} tree + * @param {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern | Brk.MetricsTree_Cohorts_Utxo_Lth_Realized} r * @returns {AnyFetchedSeriesBlueprint[]} */ -function realizedPnlCumulative(tree) { - const r = tree.realized; +function realizedPnlCumulativeFull(r) { return [ - line({ - metric: r.realizedProfit.cumulative, - name: "Profit", - color: colors.profit, - unit: Unit.usd, - }), - line({ - metric: r.realizedLoss.cumulative, - name: "Loss", - color: colors.loss, - unit: Unit.usd, - }), - line({ - metric: r.negRealizedLoss, - name: "Negative Loss", - color: colors.loss, - unit: Unit.usd, - defaultActive: false, - }), - baseline({ - metric: r.realizedProfitRelToRealizedCap, - name: "Profit", - color: colors.profit, - unit: Unit.pctRcap, - }), - baseline({ - metric: r.realizedLossRelToRealizedCap, - name: "Loss", - color: colors.loss, - unit: Unit.pctRcap, - }), + line({ metric: r.profit.cumulative.usd, name: "Profit", color: colors.profit, unit: Unit.usd }), + line({ metric: r.loss.cumulative.usd, name: "Loss", color: colors.loss, unit: Unit.usd }), + line({ metric: r.loss.negative, name: "Negative Loss", color: colors.loss, unit: Unit.usd, defaultActive: false }), + baseline({ metric: r.profit.relToRcap.ratio, name: "Profit", color: colors.profit, unit: Unit.pctRcap }), + baseline({ metric: r.loss.relToRcap.ratio, name: "Loss", color: colors.loss, unit: Unit.pctRcap }), ]; } /** - * Realized Net P&L cumulative series - * @param {{ realized: AnyRealizedPattern }} tree + * @param {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern | Brk.MetricsTree_Cohorts_Utxo_Lth_Realized} r * @returns {AnyFetchedSeriesBlueprint[]} */ -function realizedNetPnlCumulative(tree) { - const r = tree.realized; +function realized30dChangeFull(r) { return [ - baseline({ - metric: r.netRealizedPnl.cumulative, - name: "Net", - unit: Unit.usd, - }), - baseline({ - metric: r.netRealizedPnlRelToRealizedCap, - name: "Net", - unit: Unit.pctRcap, - }), + baseline({ metric: r.netPnl.delta.change._1m.usd, name: "30d Change", unit: Unit.usd }), + baseline({ metric: r.netPnl.change1m.relToMcap.ratio, name: "30d Change", unit: Unit.pctMcap }), + baseline({ metric: r.netPnl.change1m.relToRcap.ratio, name: "30d Change", unit: Unit.pctRcap }), ]; } /** - * Realized 30d change series - * @param {{ realized: AnyRealizedPattern }} tree - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function realized30dChange(tree) { - const r = tree.realized; - return [ - baseline({ - metric: r.netRealizedPnlCumulative30dDelta, - name: "30d Change", - unit: Unit.usd, - }), - baseline({ - metric: r.netRealizedPnlCumulative30dDeltaRelToMarketCap, - name: "30d Change", - unit: Unit.pctMcap, - }), - baseline({ - metric: r.netRealizedPnlCumulative30dDeltaRelToRealizedCap, - name: "30d Change", - unit: Unit.pctRcap, - }), - ]; -} - -/** - * Sent in profit/loss tree - * @param {{ realized: AnyRealizedPattern }} tree + * Rolling realized with P/L and ratio (full realized only) + * @param {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern | Brk.MetricsTree_Cohorts_Utxo_Lth_Realized} r * @param {(metric: string) => string} title * @returns {PartialOptionsTree} */ -function sentInPnlTree(tree, title) { - const r = tree.realized; +function singleRollingRealizedTreeFull(r, title) { return [ - { - name: "Sum", - title: title("Sent In Profit & Loss"), - bottom: [ - ...satsBtcUsd({ - pattern: r.sentInProfit14dEma, - name: "In Profit 14d EMA", - color: colors.profit, - defaultActive: false, - }), - ...satsBtcUsd({ - pattern: r.sentInLoss14dEma, - name: "In Loss 14d EMA", - color: colors.loss, - defaultActive: false, - }), - ...satsBtcUsdFrom({ - source: r.sentInProfit, - key: "base", - name: "In Profit", - color: colors.profit, - }), - ...satsBtcUsdFrom({ - source: r.sentInLoss, - key: "base", - name: "In Loss", - color: colors.loss, - }), - ], - }, - { - name: "Cumulative", - title: title("Cumulative Sent In Profit & Loss"), - bottom: [ - ...satsBtcUsdFrom({ - source: r.sentInProfit, - key: "cumulative", - name: "In Profit", - color: colors.profit, - }), - ...satsBtcUsdFrom({ - source: r.sentInLoss, - key: "cumulative", - name: "In Loss", - color: colors.loss, - }), - ], - }, - ]; -} - -// ============================================================================ -// Rolling Realized Helpers -// ============================================================================ - -/** - * Rolling realized value tree for single cohort (available on all realized patterns) - * @param {AnyRealizedPattern} r - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function singleRollingRealizedValueTree(r, title) { - return [ - { - name: "Compare", - title: title("Rolling Realized Value"), - bottom: [ - line({ metric: r.realizedValue24h, name: "24h", color: colors.time._24h, unit: Unit.usd }), - line({ metric: r.realizedValue7d, name: "7d", color: colors.time._1w, unit: Unit.usd }), - line({ metric: r.realizedValue30d, name: "30d", color: colors.time._1m, unit: Unit.usd }), - line({ metric: r.realizedValue1y, name: "1y", color: colors.time._1y, unit: Unit.usd }), - ], - }, - { name: "24h", title: title("Realized Value (24h)"), bottom: [line({ metric: r.realizedValue24h, name: "Value", unit: Unit.usd })] }, - { name: "7d", title: title("Realized Value (7d)"), bottom: [line({ metric: r.realizedValue7d, name: "Value", unit: Unit.usd })] }, - { name: "30d", title: title("Realized Value (30d)"), bottom: [line({ metric: r.realizedValue30d, name: "Value", unit: Unit.usd })] }, - { name: "1y", title: title("Realized Value (1y)"), bottom: [line({ metric: r.realizedValue1y, name: "Value", unit: Unit.usd })] }, - ]; -} - -/** - * Rolling realized tree with P/L for single cohort (for RealizedWithExtras patterns) - * @param {RealizedWithExtras} r - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function singleRollingRealizedTreeWithExtras(r, title) { - return [ - { - name: "Value", - tree: singleRollingRealizedValueTree(r, title), - }, { name: "Profit", tree: [ @@ -745,16 +289,16 @@ function singleRollingRealizedTreeWithExtras(r, title) { name: "Compare", title: title("Rolling Realized Profit"), bottom: [ - line({ metric: r.realizedProfit24h, name: "24h", color: colors.time._24h, unit: Unit.usd }), - line({ metric: r.realizedProfit7d, name: "7d", color: colors.time._1w, unit: Unit.usd }), - line({ metric: r.realizedProfit30d, name: "30d", color: colors.time._1m, unit: Unit.usd }), - line({ metric: r.realizedProfit1y, name: "1y", color: colors.time._1y, unit: Unit.usd }), + line({ metric: r.profit.sum._24h.usd, name: "24h", color: colors.time._24h, unit: Unit.usd }), + line({ metric: r.profit.sum._1w.usd, name: "7d", color: colors.time._1w, unit: Unit.usd }), + line({ metric: r.profit.sum._1m.usd, name: "30d", color: colors.time._1m, unit: Unit.usd }), + line({ metric: r.profit.sum._1y.usd, name: "1y", color: colors.time._1y, unit: Unit.usd }), ], }, - { name: "24h", title: title("Realized Profit (24h)"), bottom: [line({ metric: r.realizedProfit24h, name: "Profit", color: colors.profit, unit: Unit.usd })] }, - { name: "7d", title: title("Realized Profit (7d)"), bottom: [line({ metric: r.realizedProfit7d, name: "Profit", color: colors.profit, unit: Unit.usd })] }, - { name: "30d", title: title("Realized Profit (30d)"), bottom: [line({ metric: r.realizedProfit30d, name: "Profit", color: colors.profit, unit: Unit.usd })] }, - { name: "1y", title: title("Realized Profit (1y)"), bottom: [line({ metric: r.realizedProfit1y, name: "Profit", color: colors.profit, unit: Unit.usd })] }, + { name: "24h", title: title("Realized Profit (24h)"), bottom: [line({ metric: r.profit.sum._24h.usd, name: "Profit", color: colors.profit, unit: Unit.usd })] }, + { name: "7d", title: title("Realized Profit (7d)"), bottom: [line({ metric: r.profit.sum._1w.usd, name: "Profit", color: colors.profit, unit: Unit.usd })] }, + { name: "30d", title: title("Realized Profit (30d)"), bottom: [line({ metric: r.profit.sum._1m.usd, name: "Profit", color: colors.profit, unit: Unit.usd })] }, + { name: "1y", title: title("Realized Profit (1y)"), bottom: [line({ metric: r.profit.sum._1y.usd, name: "Profit", color: colors.profit, unit: Unit.usd })] }, ], }, { @@ -764,16 +308,16 @@ function singleRollingRealizedTreeWithExtras(r, title) { name: "Compare", title: title("Rolling Realized Loss"), bottom: [ - line({ metric: r.realizedLoss24h, name: "24h", color: colors.time._24h, unit: Unit.usd }), - line({ metric: r.realizedLoss7d, name: "7d", color: colors.time._1w, unit: Unit.usd }), - line({ metric: r.realizedLoss30d, name: "30d", color: colors.time._1m, unit: Unit.usd }), - line({ metric: r.realizedLoss1y, name: "1y", color: colors.time._1y, unit: Unit.usd }), + line({ metric: r.loss.sum._24h.usd, name: "24h", color: colors.time._24h, unit: Unit.usd }), + line({ metric: r.loss.sum._1w.usd, name: "7d", color: colors.time._1w, unit: Unit.usd }), + line({ metric: r.loss.sum._1m.usd, name: "30d", color: colors.time._1m, unit: Unit.usd }), + line({ metric: r.loss.sum._1y.usd, name: "1y", color: colors.time._1y, unit: Unit.usd }), ], }, - { name: "24h", title: title("Realized Loss (24h)"), bottom: [line({ metric: r.realizedLoss24h, name: "Loss", color: colors.loss, unit: Unit.usd })] }, - { name: "7d", title: title("Realized Loss (7d)"), bottom: [line({ metric: r.realizedLoss7d, name: "Loss", color: colors.loss, unit: Unit.usd })] }, - { name: "30d", title: title("Realized Loss (30d)"), bottom: [line({ metric: r.realizedLoss30d, name: "Loss", color: colors.loss, unit: Unit.usd })] }, - { name: "1y", title: title("Realized Loss (1y)"), bottom: [line({ metric: r.realizedLoss1y, name: "Loss", color: colors.loss, unit: Unit.usd })] }, + { name: "24h", title: title("Realized Loss (24h)"), bottom: [line({ metric: r.loss.sum._24h.usd, name: "Loss", color: colors.loss, unit: Unit.usd })] }, + { name: "7d", title: title("Realized Loss (7d)"), bottom: [line({ metric: r.loss.sum._1w.usd, name: "Loss", color: colors.loss, unit: Unit.usd })] }, + { name: "30d", title: title("Realized Loss (30d)"), bottom: [line({ metric: r.loss.sum._1m.usd, name: "Loss", color: colors.loss, unit: Unit.usd })] }, + { name: "1y", title: title("Realized Loss (1y)"), bottom: [line({ metric: r.loss.sum._1y.usd, name: "Loss", color: colors.loss, unit: Unit.usd })] }, ], }, { @@ -783,75 +327,46 @@ function singleRollingRealizedTreeWithExtras(r, title) { name: "Compare", title: title("Rolling Realized P/L Ratio"), bottom: [ - baseline({ metric: r.realizedProfitToLossRatio24h, name: "24h", color: colors.time._24h, unit: Unit.ratio }), - baseline({ metric: r.realizedProfitToLossRatio7d, name: "7d", color: colors.time._1w, unit: Unit.ratio }), - baseline({ metric: r.realizedProfitToLossRatio30d, name: "30d", color: colors.time._1m, unit: Unit.ratio }), - baseline({ metric: r.realizedProfitToLossRatio1y, name: "1y", color: colors.time._1y, unit: Unit.ratio }), + baseline({ metric: r.profitToLossRatio._24h, name: "24h", color: colors.time._24h, unit: Unit.ratio }), + baseline({ metric: r.profitToLossRatio._1w, name: "7d", color: colors.time._1w, unit: Unit.ratio }), + baseline({ metric: r.profitToLossRatio._1m, name: "30d", color: colors.time._1m, unit: Unit.ratio }), + baseline({ metric: r.profitToLossRatio._1y, name: "1y", color: colors.time._1y, unit: Unit.ratio }), ], }, - { name: "24h", title: title("Realized P/L Ratio (24h)"), bottom: [baseline({ metric: r.realizedProfitToLossRatio24h, name: "P/L Ratio", unit: Unit.ratio })] }, - { name: "7d", title: title("Realized P/L Ratio (7d)"), bottom: [baseline({ metric: r.realizedProfitToLossRatio7d, name: "P/L Ratio", unit: Unit.ratio })] }, - { name: "30d", title: title("Realized P/L Ratio (30d)"), bottom: [baseline({ metric: r.realizedProfitToLossRatio30d, name: "P/L Ratio", unit: Unit.ratio })] }, - { name: "1y", title: title("Realized P/L Ratio (1y)"), bottom: [baseline({ metric: r.realizedProfitToLossRatio1y, name: "P/L Ratio", unit: Unit.ratio })] }, + { name: "24h", title: title("Realized P/L Ratio (24h)"), bottom: [baseline({ metric: r.profitToLossRatio._24h, name: "P/L Ratio", unit: Unit.ratio })] }, + { name: "7d", title: title("Realized P/L Ratio (7d)"), bottom: [baseline({ metric: r.profitToLossRatio._1w, name: "P/L Ratio", unit: Unit.ratio })] }, + { name: "30d", title: title("Realized P/L Ratio (30d)"), bottom: [baseline({ metric: r.profitToLossRatio._1m, name: "P/L Ratio", unit: Unit.ratio })] }, + { name: "1y", title: title("Realized P/L Ratio (1y)"), bottom: [baseline({ metric: r.profitToLossRatio._1y, name: "P/L Ratio", unit: Unit.ratio })] }, ], }, ]; } /** - * Grouped rolling realized value charts (available on all realized patterns) - * @param {readonly CohortObject[]} list - * @param {CohortObject} all + * Rolling realized profit/loss sums (basic — no P/L ratio) + * @param {Brk.BaseCumulativeSumPattern3} profit + * @param {Brk.BaseCumulativeSumPattern3} loss * @param {(metric: string) => string} title * @returns {PartialOptionsTree} */ -function groupedRollingRealizedValueCharts(list, all, title) { +function singleRollingRealizedTreeBasic(profit, loss, title) { return [ - { name: "24h", title: title("Realized Value (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedValue24h, name, color, unit: Unit.usd })) }, - { name: "7d", title: title("Realized Value (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedValue7d, name, color, unit: Unit.usd })) }, - { name: "30d", title: title("Realized Value (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedValue30d, name, color, unit: Unit.usd })) }, - { name: "1y", title: title("Realized Value (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedValue1y, name, color, unit: Unit.usd })) }, - ]; -} - -/** - * Grouped rolling realized charts with P/L (for RealizedWithExtras cohorts) - * @param {readonly (CohortAgeRange | CohortLongTerm | CohortAll | CohortFull)[]} list - * @param {CohortAll} all - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function groupedRollingRealizedChartsWithExtras(list, all, title) { - return [ - { - name: "Value", - tree: groupedRollingRealizedValueCharts(list, all, title), - }, { name: "Profit", tree: [ - { name: "24h", title: title("Realized Profit (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedProfit24h, name, color, unit: Unit.usd })) }, - { name: "7d", title: title("Realized Profit (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedProfit7d, name, color, unit: Unit.usd })) }, - { name: "30d", title: title("Realized Profit (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedProfit30d, name, color, unit: Unit.usd })) }, - { name: "1y", title: title("Realized Profit (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedProfit1y, name, color, unit: Unit.usd })) }, + { name: "24h", title: title("Realized Profit (24h)"), bottom: [line({ metric: profit.sum._24h.usd, name: "Profit", color: colors.profit, unit: Unit.usd })] }, + { name: "7d", title: title("Realized Profit (7d)"), bottom: [line({ metric: profit.sum._1w.usd, name: "Profit", color: colors.profit, unit: Unit.usd })] }, + { name: "30d", title: title("Realized Profit (30d)"), bottom: [line({ metric: profit.sum._1m.usd, name: "Profit", color: colors.profit, unit: Unit.usd })] }, + { name: "1y", title: title("Realized Profit (1y)"), bottom: [line({ metric: profit.sum._1y.usd, name: "Profit", color: colors.profit, unit: Unit.usd })] }, ], }, { name: "Loss", tree: [ - { name: "24h", title: title("Realized Loss (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedLoss24h, name, color, unit: Unit.usd })) }, - { name: "7d", title: title("Realized Loss (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedLoss7d, name, color, unit: Unit.usd })) }, - { name: "30d", title: title("Realized Loss (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedLoss30d, name, color, unit: Unit.usd })) }, - { name: "1y", title: title("Realized Loss (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedLoss1y, name, color, unit: Unit.usd })) }, - ], - }, - { - name: "P/L Ratio", - tree: [ - { name: "24h", title: title("Realized P/L Ratio (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ metric: tree.realized.realizedProfitToLossRatio24h, name, color, unit: Unit.ratio })) }, - { name: "7d", title: title("Realized P/L Ratio (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ metric: tree.realized.realizedProfitToLossRatio7d, name, color, unit: Unit.ratio })) }, - { name: "30d", title: title("Realized P/L Ratio (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ metric: tree.realized.realizedProfitToLossRatio30d, name, color, unit: Unit.ratio })) }, - { name: "1y", title: title("Realized P/L Ratio (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ metric: tree.realized.realizedProfitToLossRatio1y, name, color, unit: Unit.ratio })) }, + { name: "24h", title: title("Realized Loss (24h)"), bottom: [line({ metric: loss.sum._24h.usd, name: "Loss", color: colors.loss, unit: Unit.usd })] }, + { name: "7d", title: title("Realized Loss (7d)"), bottom: [line({ metric: loss.sum._1w.usd, name: "Loss", color: colors.loss, unit: Unit.usd })] }, + { name: "30d", title: title("Realized Loss (30d)"), bottom: [line({ metric: loss.sum._1m.usd, name: "Loss", color: colors.loss, unit: Unit.usd })] }, + { name: "1y", title: title("Realized Loss (1y)"), bottom: [line({ metric: loss.sum._1y.usd, name: "Loss", color: colors.loss, unit: Unit.usd })] }, ], }, ]; @@ -862,86 +377,54 @@ function groupedRollingRealizedChartsWithExtras(list, all, title) { // ============================================================================ /** - * Base realized subfolder (no P/L ratio) - * @param {{ realized: AnyRealizedPattern }} tree + * Full realized subfolder (All/STH/LTH) + * @param {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern | Brk.MetricsTree_Cohorts_Utxo_Lth_Realized} r * @param {(metric: string) => string} title - * @param {PartialOptionsTree} [rollingTree] * @returns {PartialOptionsGroup} */ -function realizedSubfolder(tree, title, rollingTree) { - const r = tree.realized; +function realizedSubfolderFull(r, title) { return { name: "Realized", tree: [ - { - name: "P&L", - title: title("Realized P&L"), - bottom: realizedPnlSum(tree), - }, - { - name: "Net", - title: title("Net Realized P&L"), - bottom: realizedNetPnlSum(tree), - }, - { - name: "30d Change", - title: title("Realized P&L 30d Change"), - bottom: realized30dChange(tree), - }, + { name: "P&L", title: title("Realized P&L"), bottom: realizedPnlSumFull(r) }, + { name: "Net", title: title("Net Realized P&L"), bottom: realizedNetPnlSumFull(r) }, + { name: "30d Change", title: title("Realized P&L 30d Change"), bottom: realized30dChangeFull(r) }, { name: "Total", title: title("Total Realized P&L"), - bottom: [ - line({ - metric: r.totalRealizedPnl, - name: "Total", - unit: Unit.usd, - color: colors.bitcoin, - }), - ], + bottom: [line({ metric: r.grossPnl.cumulative.usd, name: "Total", unit: Unit.usd, color: colors.bitcoin })], + }, + { + name: "P/L Ratio", + title: title("Realized Profit/Loss Ratio"), + bottom: [baseline({ metric: r.profitToLossRatio._1y, name: "P/L Ratio", unit: Unit.ratio })], }, { name: "Peak Regret", title: title("Realized Peak Regret"), bottom: [ - line({ - metric: r.peakRegret.height, - name: "Peak Regret", - unit: Unit.usd, - }), + line({ metric: r.peakRegret.base, name: "Peak Regret", unit: Unit.usd }), ], }, - { - name: "Rolling", - tree: rollingTree ?? singleRollingRealizedValueTree(r, title), - }, + { name: "Rolling", tree: singleRollingRealizedTreeFull(r, title) }, { name: "Cumulative", tree: [ - { - name: "P&L", - title: title("Cumulative Realized P&L"), - bottom: realizedPnlCumulative(tree), - }, + { name: "P&L", title: title("Cumulative Realized P&L"), bottom: realizedPnlCumulativeFull(r) }, { name: "Net", title: title("Cumulative Net Realized P&L"), - bottom: realizedNetPnlCumulative(tree), + bottom: [ + baseline({ metric: r.netPnl.cumulative.usd, name: "Net", unit: Unit.usd }), + baseline({ metric: r.netPnl.relToRcap.ratio, name: "Net", unit: Unit.pctRcap }), + ], }, { name: "Peak Regret", title: title("Cumulative Realized Peak Regret"), bottom: [ - line({ - metric: r.peakRegret.cumulative, - name: "Peak Regret", - unit: Unit.usd, - }), - line({ - metric: r.peakRegretRelToRealizedCap, - name: "Peak Regret", - unit: Unit.pctRcap, - }), + line({ metric: r.peakRegret.cumulative, name: "Peak Regret", unit: Unit.usd }), + line({ metric: r.peakRegret.relToRcap.ratio, name: "Peak Regret", unit: Unit.pctRcap }), ], }, ], @@ -951,27 +434,86 @@ function realizedSubfolder(tree, title, rollingTree) { } /** - * Realized subfolder with P/L ratio and rolling P/L - * @param {{ realized: RealizedWithExtras }} tree + * Mid realized subfolder (AgeRange/MaxAge — has netPnl + delta, no relToRcap/peakRegret) + * @param {Brk.CapLossMvrvNetPriceProfitSoprPattern} r * @param {(metric: string) => string} title * @returns {PartialOptionsGroup} */ -function realizedSubfolderWithExtras(tree, title) { - const r = tree.realized; - const base = realizedSubfolder(tree, title, singleRollingRealizedTreeWithExtras(r, title)); - // Insert P/L Ratio after Total (index 3) - base.tree.splice(4, 0, { - name: "P/L Ratio", - title: title("Realized Profit/Loss Ratio"), - bottom: [ - baseline({ - metric: r.realizedProfitToLossRatio1y, - name: "P/L Ratio", - unit: Unit.ratio, - }), +function realizedSubfolderMid(r, title) { + return { + name: "Realized", + tree: [ + { + name: "P&L", + title: title("Realized P&L"), + bottom: [ + dots({ metric: r.profit.base.usd, name: "Profit", color: colors.profit, unit: Unit.usd }), + dots({ metric: r.loss.negative, name: "Negative Loss", color: colors.loss, unit: Unit.usd, defaultActive: false }), + dots({ metric: r.loss.base.usd, name: "Loss", color: colors.loss, unit: Unit.usd, defaultActive: false }), + ], + }, + { + name: "Net", + title: title("Net Realized P&L"), + bottom: [dotsBaseline({ metric: r.netPnl.base.usd, name: "Net", unit: Unit.usd })], + }, + { + name: "30d Change", + title: title("Realized P&L 30d Change"), + bottom: [baseline({ metric: r.netPnl.delta.change._1m.usd, name: "30d Change", unit: Unit.usd })], + }, + { name: "Rolling", tree: singleRollingRealizedTreeBasic(r.profit, r.loss, title) }, + { + name: "Cumulative", + tree: [ + { + name: "P&L", + title: title("Cumulative Realized P&L"), + bottom: [ + line({ metric: r.profit.cumulative.usd, name: "Profit", color: colors.profit, unit: Unit.usd }), + line({ metric: r.loss.cumulative.usd, name: "Loss", color: colors.loss, unit: Unit.usd }), + ], + }, + { + name: "Net", + title: title("Cumulative Net Realized P&L"), + bottom: [baseline({ metric: r.netPnl.cumulative.usd, name: "Net", unit: Unit.usd })], + }, + ], + }, ], - }); - return base; + }; +} + +/** + * Basic realized subfolder (no netPnl, no relToRcap) + * @param {Brk.CapLossMvrvPriceProfitSoprPattern} r + * @param {(metric: string) => string} title + * @returns {PartialOptionsGroup} + */ +function realizedSubfolderBasic(r, title) { + return { + name: "Realized", + tree: [ + { + name: "P&L", + title: title("Realized P&L"), + bottom: [ + dots({ metric: r.profit.base.usd, name: "Profit", color: colors.profit, unit: Unit.usd }), + dots({ metric: r.loss.base.usd, name: "Loss", color: colors.loss, unit: Unit.usd, defaultActive: false }), + ], + }, + { name: "Rolling", tree: singleRollingRealizedTreeBasic(r.profit, r.loss, title) }, + { + name: "Cumulative", + title: title("Cumulative Realized P&L"), + bottom: [ + line({ metric: r.profit.cumulative.usd, name: "Profit", color: colors.profit, unit: Unit.usd }), + line({ metric: r.loss.cumulative.usd, name: "Loss", color: colors.loss, unit: Unit.usd }), + ], + }, + ], + }; } // ============================================================================ @@ -979,169 +521,103 @@ function realizedSubfolderWithExtras(tree, title) { // ============================================================================ /** - * Basic profitability section (USD only unrealized) + * Basic profitability section (NUPL only unrealized, basic realized) * @param {{ cohort: UtxoCohortObject | CohortWithoutRelative, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ export function createProfitabilitySection({ cohort, title }) { const { tree } = cohort; - const m = getUnrealizedMetrics(tree); return { name: "Profitability", tree: [ { name: "Unrealized", tree: [ - { - name: "P&L", - title: title("Unrealized P&L"), - bottom: unrealizedUsd(m), - }, - { - name: "Net P&L", - title: title("Net Unrealized P&L"), - bottom: netUnrealizedUsd(m.net), - }, + { name: "NUPL", title: title("NUPL"), bottom: nuplSeries(tree.unrealized.nupl) }, ], }, - realizedSubfolder(tree, title), - volumeSubfolder(tree, title), - { - name: "Invested Capital", - tree: [ - { - name: "Absolute", - title: title("Invested Capital In Profit & Loss"), - bottom: investedCapitalAbsolute(tree), - }, - ], - }, - sentimentChart(tree, title), + realizedSubfolderBasic(tree.realized, title), ], }; } /** - * Section with invested capital % but no unrealized relative (basic cohorts) - * @param {{ cohort: CohortBasicWithoutMarketCap, title: (metric: string) => string }} args + * Section for All cohort + * @param {{ cohort: CohortAll, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ -export function createProfitabilitySectionBasicWithInvestedCapitalPct({ - cohort, - title, -}) { - const { tree } = cohort; - const m = getUnrealizedMetrics(tree); +export function createProfitabilitySectionAll({ cohort, title }) { + const u = cohort.tree.unrealized; + const r = cohort.tree.realized; return { name: "Profitability", tree: [ { name: "Unrealized", tree: [ - { - name: "P&L", - title: title("Unrealized P&L"), - bottom: unrealizedUsd(m), - }, - { - name: "Net P&L", - title: title("Net Unrealized P&L"), - bottom: netUnrealizedUsd(m.net), - }, + { name: "P&L", title: title("Unrealized P&L"), bottom: unrealizedAll(u) }, + { name: "Net P&L", title: title("Net Unrealized P&L"), bottom: netUnrealizedAll(u) }, + { name: "NUPL", title: title("NUPL"), bottom: nuplSeries(u.nupl) }, ], }, - realizedSubfolder(tree, title), - volumeSubfolder(tree, title), + realizedSubfolderFull(r, title), { name: "Invested Capital", title: title("Invested Capital In Profit & Loss"), - bottom: investedCapitalWithPct(tree), + bottom: investedCapitalSeries(u), }, - sentimentChart(tree, title), + { name: "Sentiment", title: title("Market Sentiment"), bottom: sentimentSeries(u) }, ], }; } /** - * Section for ageRange cohorts (Own M.Cap + Own P&L) - * @param {{ cohort: CohortAgeRange, title: (metric: string) => string }} args + * Section for Full cohorts (STH) + * @param {{ cohort: CohortFull, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ -export function createProfitabilitySectionWithInvestedCapitalPct({ - cohort, - title, -}) { - const { tree } = cohort; - const m = getUnrealizedMetrics(tree); +export function createProfitabilitySectionFull({ cohort, title }) { + const u = cohort.tree.unrealized; + const r = cohort.tree.realized; return { name: "Profitability", tree: [ { name: "Unrealized", tree: [ - { - name: "P&L", - title: title("Unrealized P&L"), - bottom: unrealizedWithOwnMarketCap(m, tree.relative), - }, - { - name: "Net P&L", - title: title("Net Unrealized P&L"), - bottom: netUnrealizedWithOwnMarketCap(m.net, tree.relative), - }, + { name: "P&L", title: title("Unrealized P&L"), bottom: unrealizedFull(u) }, + { name: "Net P&L", title: title("Net Unrealized P&L"), bottom: netUnrealizedFull(u) }, + { name: "NUPL", title: title("NUPL"), bottom: nuplSeries(u.nupl) }, ], }, - realizedSubfolderWithExtras(tree, title), - volumeSubfolder(tree, title), + realizedSubfolderFull(r, title), { name: "Invested Capital", title: title("Invested Capital In Profit & Loss"), - bottom: investedCapitalWithPct(tree), + bottom: investedCapitalSeries(u), }, - sentimentChart(tree, title), + { name: "Sentiment", title: title("Market Sentiment"), bottom: sentimentSeries(u) }, ], }; } /** - * Section with NUPL (basic cohorts with market cap) + * Section with NUPL (basic cohorts with market cap — NuplPattern unrealized) * @param {{ cohort: CohortBasicWithMarketCap, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ export function createProfitabilitySectionWithNupl({ cohort, title }) { const { tree } = cohort; - const m = getUnrealizedMetrics(tree); return { name: "Profitability", tree: [ { name: "Unrealized", tree: [ - { - name: "P&L", - title: title("Unrealized P&L"), - bottom: unrealizedWithMarketCap(m, tree.relative), - }, - { - name: "Net P&L", - title: title("Net Unrealized P&L"), - bottom: netUnrealizedWithMarketCap(m.net, tree.relative), - }, - { - name: "NUPL", - title: title("NUPL"), - bottom: nuplSeries(tree.relative), - }, + { name: "NUPL", title: title("NUPL"), bottom: nuplSeries(tree.unrealized.nupl) }, ], }, - realizedSubfolder(tree, title), - volumeSubfolder(tree, title), - { - name: "Invested Capital", - title: title("Invested Capital In Profit & Loss"), - bottom: investedCapitalWithPct(tree), - }, - sentimentChart(tree, title), + realizedSubfolderBasic(tree.realized, title), ], }; } @@ -1152,125 +628,71 @@ export function createProfitabilitySectionWithNupl({ cohort, title }) { * @returns {PartialOptionsGroup} */ export function createProfitabilitySectionLongTerm({ cohort, title }) { - const { tree } = cohort; - const m = getUnrealizedMetrics(tree); + const u = cohort.tree.unrealized; + const r = cohort.tree.realized; return { name: "Profitability", tree: [ { name: "Unrealized", tree: [ - { - name: "P&L", - title: title("Unrealized P&L"), - bottom: unrealizedLongTerm(m, tree.relative), - }, - { - name: "Net P&L", - title: title("Net Unrealized P&L"), - bottom: netUnrealizedWithOwnMarketCap(m.net, tree.relative), - }, - { - name: "NUPL", - title: title("NUPL"), - bottom: nuplSeries(tree.relative), - }, + { name: "P&L", title: title("Unrealized P&L"), bottom: unrealizedLongTerm(u) }, + { name: "Net P&L", title: title("Net Unrealized P&L"), bottom: netUnrealizedFull(u) }, + { name: "NUPL", title: title("NUPL"), bottom: nuplSeries(u.nupl) }, ], }, - realizedSubfolderWithExtras(tree, title), - volumeSubfolder(tree, title), + realizedSubfolderFull(r, title), { name: "Invested Capital", title: title("Invested Capital In Profit & Loss"), - bottom: investedCapitalWithPct(tree), + bottom: investedCapitalSeries(u), }, - sentimentChart(tree, title), + { name: "Sentiment", title: title("Market Sentiment"), bottom: sentimentSeries(u) }, ], }; } /** - * Section for Full cohorts (all relative metrics) - * @param {{ cohort: CohortFull, title: (metric: string) => string }} args + * Section for AgeRange cohorts (mid-tier: has unrealized profit/loss/netPnl, mid realized) + * @param {{ cohort: CohortAgeRange, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ -export function createProfitabilitySectionFull({ cohort, title }) { - const { tree } = cohort; - const m = getUnrealizedMetrics(tree); +export function createProfitabilitySectionWithInvestedCapitalPct({ cohort, title }) { + const u = cohort.tree.unrealized; + const r = cohort.tree.realized; return { name: "Profitability", tree: [ { name: "Unrealized", tree: [ - { - name: "P&L", - title: title("Unrealized P&L"), - bottom: unrealizedFull(m, tree.relative), - }, - { - name: "Net P&L", - title: title("Net Unrealized P&L"), - bottom: netUnrealizedFull(m.net, tree.relative), - }, - { - name: "NUPL", - title: title("NUPL"), - bottom: nuplSeries(tree.relative), - }, + { name: "P&L", title: title("Unrealized P&L"), bottom: unrealizedMid(u) }, + { name: "Net P&L", title: title("Net Unrealized P&L"), bottom: netUnrealizedMid(u) }, + { name: "NUPL", title: title("NUPL"), bottom: nuplSeries(u.nupl) }, ], }, - realizedSubfolderWithExtras(tree, title), - volumeSubfolder(tree, title), - { - name: "Invested Capital", - title: title("Invested Capital In Profit & Loss"), - bottom: investedCapitalWithPct(tree), - }, - sentimentChart(tree, title), + realizedSubfolderMid(r, title), ], }; } /** - * Section for "all" cohort - * @param {{ cohort: CohortAll, title: (metric: string) => string }} args + * Section with invested capital % but no unrealized relative (basic cohorts) + * @param {{ cohort: CohortBasicWithoutMarketCap, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ -export function createProfitabilitySectionAll({ cohort, title }) { +export function createProfitabilitySectionBasicWithInvestedCapitalPct({ cohort, title }) { const { tree } = cohort; - const m = getUnrealizedMetrics(tree); return { name: "Profitability", tree: [ { name: "Unrealized", tree: [ - { - name: "P&L", - title: title("Unrealized P&L"), - bottom: unrealizedAll(m, tree.relative), - }, - { - name: "Net P&L", - title: title("Net Unrealized P&L"), - bottom: netUnrealizedAll(m.net, tree.relative), - }, - { - name: "NUPL", - title: title("NUPL"), - bottom: nuplSeries(tree.relative), - }, + { name: "NUPL", title: title("NUPL"), bottom: nuplSeries(tree.unrealized.nupl) }, ], }, - realizedSubfolderWithExtras(tree, title), - volumeSubfolder(tree, title), - { - name: "Invested Capital", - title: title("Invested Capital In Profit & Loss"), - bottom: investedCapitalWithPct(tree), - }, - sentimentChart(tree, title), + realizedSubfolderBasic(tree.realized, title), ], }; } @@ -1280,56 +702,233 @@ export function createProfitabilitySectionAll({ cohort, title }) { // ============================================================================ /** - * Grouped P&L charts (USD only) - * @param {readonly CohortObject[]} list - * @param {CohortAll} all + * Grouped realized P&L sum (basic — all cohorts have profit/loss) + * @template {{ name: string, color: Color, tree: { realized: { profit: Brk.BaseCumulativeSumPattern3, loss: Brk.BaseCumulativeSumPattern3 } } }} T + * @template {{ name: string, color: Color, tree: { realized: { profit: Brk.BaseCumulativeSumPattern3, loss: Brk.BaseCumulativeSumPattern3 } } }} A + * @param {readonly T[]} list + * @param {A} all * @param {(metric: string) => string} title * @returns {PartialOptionsTree} */ -function groupedPnlCharts(list, all, title) { +function groupedRealizedPnlSum(list, all, title) { return [ { name: "Profit", - title: title("Unrealized Profit"), + title: title("Realized Profit"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.unrealized.unrealizedProfit, - name, - color, - unit: Unit.usd, - }), + line({ metric: tree.realized.profit.base.usd, name, color, unit: Unit.usd }), ), }, { name: "Loss", - title: title("Unrealized Loss"), + title: title("Realized Loss"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.unrealized.negUnrealizedLoss, - name, - color, - unit: Unit.usd, - }), - ), - }, - { - name: "Net P&L", - title: title("Net Unrealized P&L"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.unrealized.netUnrealizedPnl, - name, - color, - unit: Unit.usd, - }), + line({ metric: tree.realized.loss.base.usd, name, color, unit: Unit.usd }), ), }, ]; } /** - * Grouped P&L with % of Market Cap - * @param {readonly (CohortFull | CohortBasicWithMarketCap | CohortLongTerm)[]} list + * Grouped realized P&L sum with extras (full cohorts) + * @param {readonly (CohortAll | CohortFull | CohortLongTerm)[]} list + * @param {CohortAll} all + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function groupedRealizedPnlSumFull(list, all, title) { + return [ + ...groupedRealizedPnlSum(list, all, title), + { + name: "Total", + title: title("Total Realized P&L"), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + line({ metric: tree.realized.grossPnl.cumulative.usd, name, color, unit: Unit.usd }), + ), + }, + { + name: "P/L Ratio", + title: title("Realized Profit/Loss Ratio"), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + baseline({ metric: tree.realized.profitToLossRatio._1y, name, color, unit: Unit.ratio }), + ), + }, + ]; +} + +/** + * Grouped rolling realized charts (basic — profit/loss sums only) + * @template {{ name: string, color: Color, tree: { realized: { profit: Brk.BaseCumulativeSumPattern3, loss: Brk.BaseCumulativeSumPattern3 } } }} T + * @template {{ name: string, color: Color, tree: { realized: { profit: Brk.BaseCumulativeSumPattern3, loss: Brk.BaseCumulativeSumPattern3 } } }} A + * @param {readonly T[]} list + * @param {A} all + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function groupedRollingRealizedCharts(list, all, title) { + return [ + { + name: "Profit", + tree: [ + { name: "24h", title: title("Realized Profit (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.profit.sum._24h.usd, name, color, unit: Unit.usd })) }, + { name: "7d", title: title("Realized Profit (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.profit.sum._1w.usd, name, color, unit: Unit.usd })) }, + { name: "30d", title: title("Realized Profit (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.profit.sum._1m.usd, name, color, unit: Unit.usd })) }, + { name: "1y", title: title("Realized Profit (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.profit.sum._1y.usd, name, color, unit: Unit.usd })) }, + ], + }, + { + name: "Loss", + tree: [ + { name: "24h", title: title("Realized Loss (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.loss.sum._24h.usd, name, color, unit: Unit.usd })) }, + { name: "7d", title: title("Realized Loss (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.loss.sum._1w.usd, name, color, unit: Unit.usd })) }, + { name: "30d", title: title("Realized Loss (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.loss.sum._1m.usd, name, color, unit: Unit.usd })) }, + { name: "1y", title: title("Realized Loss (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.loss.sum._1y.usd, name, color, unit: Unit.usd })) }, + ], + }, + ]; +} + +/** + * Grouped rolling realized with P/L ratio (full cohorts) + * @param {readonly (CohortAll | CohortFull | CohortLongTerm)[]} list + * @param {CohortAll} all + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function groupedRollingRealizedChartsFull(list, all, title) { + return [ + ...groupedRollingRealizedCharts(list, all, title), + { + name: "P/L Ratio", + tree: [ + { name: "24h", title: title("Realized P/L Ratio (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ metric: tree.realized.profitToLossRatio._24h, name, color, unit: Unit.ratio })) }, + { name: "7d", title: title("Realized P/L Ratio (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ metric: tree.realized.profitToLossRatio._1w, name, color, unit: Unit.ratio })) }, + { name: "30d", title: title("Realized P/L Ratio (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ metric: tree.realized.profitToLossRatio._1m, name, color, unit: Unit.ratio })) }, + { name: "1y", title: title("Realized P/L Ratio (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ metric: tree.realized.profitToLossRatio._1y, name, color, unit: Unit.ratio })) }, + ], + }, + ]; +} + +/** + * Grouped realized subfolder (basic) + * @template {{ name: string, color: Color, tree: { realized: { profit: Brk.BaseCumulativeSumPattern3, loss: Brk.BaseCumulativeSumPattern3 } } }} T + * @template {{ name: string, color: Color, tree: { realized: { profit: Brk.BaseCumulativeSumPattern3, loss: Brk.BaseCumulativeSumPattern3 } } }} A + * @param {readonly T[]} list + * @param {A} all + * @param {(metric: string) => string} title + * @returns {PartialOptionsGroup} + */ +function groupedRealizedSubfolder(list, all, title) { + return { + name: "Realized", + tree: [ + { name: "P&L", tree: groupedRealizedPnlSum(list, all, title) }, + { name: "Rolling", tree: groupedRollingRealizedCharts(list, all, title) }, + { + name: "Cumulative", + tree: [ + { + name: "Profit", + title: title("Cumulative Realized Profit"), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + line({ metric: tree.realized.profit.cumulative.usd, name, color, unit: Unit.usd }), + ), + }, + { + name: "Loss", + title: title("Cumulative Realized Loss"), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + line({ metric: tree.realized.loss.cumulative.usd, name, color, unit: Unit.usd }), + ), + }, + ], + }, + ], + }; +} + +/** + * Grouped realized subfolder for full cohorts + * @param {readonly (CohortAll | CohortFull | CohortLongTerm)[]} list + * @param {CohortAll} all + * @param {(metric: string) => string} title + * @returns {PartialOptionsGroup} + */ +function groupedRealizedSubfolderFull(list, all, title) { + return { + name: "Realized", + tree: [ + { name: "P&L", tree: groupedRealizedPnlSumFull(list, all, title) }, + { + name: "Net", + title: title("Net Realized P&L"), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + baseline({ metric: tree.realized.netPnl.base.usd, name, color, unit: Unit.usd }), + ), + }, + { + name: "30d Change", + title: title("Realized P&L 30d Change"), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + baseline({ metric: tree.realized.netPnl.delta.change._1m.usd, name, color, unit: Unit.usd }), + ), + }, + { name: "Rolling", tree: groupedRollingRealizedChartsFull(list, all, title) }, + { + name: "Cumulative", + tree: [ + { + name: "Profit", + title: title("Cumulative Realized Profit"), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + line({ metric: tree.realized.profit.cumulative.usd, name, color, unit: Unit.usd }), + ), + }, + { + name: "Loss", + title: title("Cumulative Realized Loss"), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + line({ metric: tree.realized.loss.cumulative.usd, name, color, unit: Unit.usd }), + ), + }, + { + name: "Net", + title: title("Cumulative Net Realized P&L"), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + baseline({ metric: tree.realized.netPnl.cumulative.usd, name, color, unit: Unit.usd }), + ), + }, + ], + }, + ], + }; +} + +/** + * Grouped unrealized P&L (USD only — for all cohorts that at least have nupl) + * @template {{ name: string, color: Color, tree: { unrealized: { nupl: Brk.BpsRatioPattern } } }} T + * @template {{ name: string, color: Color, tree: { unrealized: { nupl: Brk.BpsRatioPattern } } }} A + * @param {readonly T[]} list + * @param {A} all + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function groupedNuplCharts(list, all, title) { + return [ + { + name: "NUPL", + title: title("NUPL"), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + baseline({ metric: tree.unrealized.nupl.ratio, name, color, unit: Unit.ratio }), + ), + }, + ]; +} + +/** + * Grouped unrealized for full cohorts with relToMcap + * @param {readonly (CohortFull | CohortLongTerm)[]} list * @param {CohortAll} all * @param {(metric: string) => string} title * @returns {PartialOptionsTree} @@ -1341,20 +940,10 @@ function groupedPnlChartsWithMarketCap(list, all, title) { title: title("Unrealized Profit"), bottom: [ ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.unrealized.unrealizedProfit, - name, - color, - unit: Unit.usd, - }), + line({ metric: tree.unrealized.profit.base.usd, name, color, unit: Unit.usd }), ), ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.relative.unrealizedProfitRelToMarketCap, - name, - color, - unit: Unit.pctMcap, - }), + baseline({ metric: tree.unrealized.profit.relToMcap.ratio, name, color, unit: Unit.pctMcap }), ), ], }, @@ -1363,51 +952,26 @@ function groupedPnlChartsWithMarketCap(list, all, title) { title: title("Unrealized Loss"), bottom: [ ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.unrealized.negUnrealizedLoss, - name, - color, - unit: Unit.usd, - }), + line({ metric: tree.unrealized.loss.base.usd, name, color, unit: Unit.usd }), ), ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.relative.negUnrealizedLossRelToMarketCap, - name, - color, - unit: Unit.pctMcap, - }), + baseline({ metric: tree.unrealized.loss.relToMcap.ratio, name, color, unit: Unit.pctMcap }), ), ], }, { name: "Net P&L", title: title("Net Unrealized P&L"), - bottom: [ - ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.unrealized.netUnrealizedPnl, - name, - color, - unit: Unit.usd, - }), - ), - ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.relative.netUnrealizedPnlRelToMarketCap, - name, - color, - unit: Unit.pctMcap, - }), - ), - ], + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + baseline({ metric: tree.unrealized.netPnl.usd, name, color, unit: Unit.usd }), + ), }, ]; } /** - * Grouped P&L with % of Own Market Cap - * @param {readonly CohortAgeRange[]} list + * Grouped unrealized for AgeRange/MaxAge (profit/loss without relToMcap) + * @param {readonly (CohortAgeRange | CohortWithAdjusted)[]} list * @param {CohortAll} all * @param {(metric: string) => string} title * @returns {PartialOptionsTree} @@ -1417,101 +981,29 @@ function groupedPnlChartsWithOwnMarketCap(list, all, title) { { name: "Profit", title: title("Unrealized Profit"), - bottom: [ - ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.unrealized.unrealizedProfit, - name, - color, - unit: Unit.usd, - }), - ), - // OwnMarketCap properties don't exist on CohortAll - use mapCohorts - ...mapCohorts(list, ({ name, color, tree }) => - line({ - metric: tree.relative.unrealizedProfitRelToOwnMarketCap, - name, - color, - unit: Unit.pctOwnMcap, - }), - ), - ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.relative.unrealizedProfitRelToOwnTotalUnrealizedPnl, - name, - color, - unit: Unit.pctOwnPnl, - }), - ), - ], + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + line({ metric: tree.unrealized.profit.base.usd, name, color, unit: Unit.usd }), + ), }, { name: "Loss", title: title("Unrealized Loss"), - bottom: [ - ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.unrealized.negUnrealizedLoss, - name, - color, - unit: Unit.usd, - }), - ), - // OwnMarketCap properties don't exist on CohortAll - use mapCohorts - ...mapCohorts(list, ({ name, color, tree }) => - line({ - metric: tree.relative.negUnrealizedLossRelToOwnMarketCap, - name, - color, - unit: Unit.pctOwnMcap, - }), - ), - ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.relative.negUnrealizedLossRelToOwnTotalUnrealizedPnl, - name, - color, - unit: Unit.pctOwnPnl, - }), - ), - ], + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + line({ metric: tree.unrealized.loss.base.usd, name, color, unit: Unit.usd }), + ), }, { name: "Net P&L", title: title("Net Unrealized P&L"), - bottom: [ - ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.unrealized.netUnrealizedPnl, - name, - color, - unit: Unit.usd, - }), - ), - // OwnMarketCap properties don't exist on CohortAll - use mapCohorts - ...mapCohorts(list, ({ name, color, tree }) => - baseline({ - metric: tree.relative.netUnrealizedPnlRelToOwnMarketCap, - name, - color, - unit: Unit.pctOwnMcap, - }), - ), - ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.relative.netUnrealizedPnlRelToOwnTotalUnrealizedPnl, - name, - color, - unit: Unit.pctOwnPnl, - }), - ), - ], + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + baseline({ metric: tree.unrealized.netPnl.usd, name, color, unit: Unit.usd }), + ), }, ]; } /** - * Grouped P&L for LongTerm cohorts + * Grouped unrealized for LongTerm (profit/loss with relToOwnMcap + relToOwnGross) * @param {readonly CohortLongTerm[]} list * @param {CohortAll} all * @param {(metric: string) => string} title @@ -1524,29 +1016,13 @@ function groupedPnlChartsLongTerm(list, all, title) { title: title("Unrealized Profit"), bottom: [ ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.unrealized.unrealizedProfit, - name, - color, - unit: Unit.usd, - }), + line({ metric: tree.unrealized.profit.base.usd, name, color, unit: Unit.usd }), ), - // OwnMarketCap properties don't exist on CohortAll - use mapCohorts ...mapCohorts(list, ({ name, color, tree }) => - line({ - metric: tree.relative.unrealizedProfitRelToOwnMarketCap, - name, - color, - unit: Unit.pctOwnMcap, - }), + line({ metric: tree.unrealized.profit.relToOwnMcap.ratio, name, color, unit: Unit.pctOwnMcap }), ), ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.relative.unrealizedProfitRelToOwnTotalUnrealizedPnl, - name, - color, - unit: Unit.pctOwnPnl, - }), + line({ metric: tree.unrealized.profit.relToOwnGross.ratio, name, color, unit: Unit.pctOwnPnl }), ), ], }, @@ -1555,37 +1031,16 @@ function groupedPnlChartsLongTerm(list, all, title) { title: title("Unrealized Loss"), bottom: [ ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.unrealized.negUnrealizedLoss, - name, - color, - unit: Unit.usd, - }), + line({ metric: tree.unrealized.loss.base.usd, name, color, unit: Unit.usd }), ), ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.relative.unrealizedLossRelToMarketCap, - name, - color, - unit: Unit.pctMcap, - }), + line({ metric: tree.unrealized.loss.relToMcap.ratio, name, color, unit: Unit.pctMcap }), ), - // OwnMarketCap properties don't exist on CohortAll - use mapCohorts ...mapCohorts(list, ({ name, color, tree }) => - line({ - metric: tree.relative.negUnrealizedLossRelToOwnMarketCap, - name, - color, - unit: Unit.pctOwnMcap, - }), + line({ metric: tree.unrealized.loss.relToOwnMcap.ratio, name, color, unit: Unit.pctOwnMcap }), ), ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.relative.negUnrealizedLossRelToOwnTotalUnrealizedPnl, - name, - color, - unit: Unit.pctOwnPnl, - }), + line({ metric: tree.unrealized.loss.relToOwnGross.ratio, name, color, unit: Unit.pctOwnPnl }), ), ], }, @@ -1594,29 +1049,13 @@ function groupedPnlChartsLongTerm(list, all, title) { title: title("Net Unrealized P&L"), bottom: [ ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.unrealized.netUnrealizedPnl, - name, - color, - unit: Unit.usd, - }), + baseline({ metric: tree.unrealized.netPnl.usd, name, color, unit: Unit.usd }), ), - // OwnMarketCap properties don't exist on CohortAll - use mapCohorts ...mapCohorts(list, ({ name, color, tree }) => - baseline({ - metric: tree.relative.netUnrealizedPnlRelToOwnMarketCap, - name, - color, - unit: Unit.pctOwnMcap, - }), + baseline({ metric: tree.unrealized.netPnl.relToOwnMcap.ratio, name, color, unit: Unit.pctOwnMcap }), ), ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.relative.netUnrealizedPnlRelToOwnTotalUnrealizedPnl, - name, - color, - unit: Unit.pctOwnPnl, - }), + baseline({ metric: tree.unrealized.netPnl.relToOwnGross.ratio, name, color, unit: Unit.pctOwnPnl }), ), ], }, @@ -1624,313 +1063,8 @@ function groupedPnlChartsLongTerm(list, all, title) { } /** - * Grouped invested capital (absolute only) - * @param {readonly CohortObject[]} list - * @param {CohortAll} all - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function groupedInvestedCapitalAbsolute(list, all, title) { - return [ - { - name: "In Profit", - title: title("Invested Capital In Profit"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.unrealized.investedCapitalInProfit, - name, - color, - unit: Unit.usd, - }), - ), - }, - { - name: "In Loss", - title: title("Invested Capital In Loss"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.unrealized.investedCapitalInLoss, - name, - color, - unit: Unit.usd, - }), - ), - }, - ]; -} - -/** - * Grouped invested capital with % - * @param {readonly (CohortBasicWithoutMarketCap | CohortAgeRange | CohortFull | CohortBasicWithMarketCap | CohortLongTerm)[]} list - * @param {CohortAll} all - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function groupedInvestedCapital(list, all, title) { - return [ - { - name: "In Profit", - title: title("Invested Capital In Profit"), - bottom: [ - ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.unrealized.investedCapitalInProfit, - name, - color, - unit: Unit.usd, - }), - ), - ...mapCohortsWithAll(list, all, ({ name, color, 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: [ - ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.unrealized.investedCapitalInLoss, - name, - color, - unit: Unit.usd, - }), - ), - ...mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.relative.investedCapitalInLossPct, - name, - color, - unit: Unit.pctOwnRcap, - }), - ), - ...priceLines({ numbers: [100, 50], unit: Unit.pctOwnRcap }), - ], - }, - ]; -} - -/** - * Grouped realized P&L sum - * @param {readonly CohortObject[]} list - * @param {CohortAll} all - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function groupedRealizedPnlSum(list, all, title) { - return [ - { - name: "Profit", - title: title("Realized Profit"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.realized.realizedProfit.height, - name, - color, - unit: Unit.usd, - }), - ), - }, - { - name: "Loss", - title: title("Realized Loss"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.realized.negRealizedLoss, - name, - color, - unit: Unit.usd, - }), - ), - }, - { - name: "Total", - title: title("Total Realized P&L"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.realized.totalRealizedPnl, - name, - color, - unit: Unit.usd, - }), - ), - }, - { - name: "Value", - title: title("Realized Value"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.realized.realizedValue, - name, - color, - unit: Unit.usd, - }), - ), - }, - ]; -} - -/** - * Grouped realized P&L sum with P/L ratio - * @param {readonly (CohortAgeRange | CohortLongTerm | CohortAll | CohortFull)[]} list - * @param {CohortAll} all - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function groupedRealizedPnlSumWithExtras(list, all, title) { - return [ - ...groupedRealizedPnlSum(list, all, title), - { - name: "P/L Ratio", - title: title("Realized Profit/Loss Ratio"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.realized.realizedProfitToLossRatio1y, - name, - color, - unit: Unit.ratio, - }), - ), - }, - ]; -} - -/** - * Grouped realized cumulative - * @param {readonly CohortObject[]} list - * @param {CohortAll} all - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function groupedRealizedPnlCumulative(list, all, title) { - return [ - { - name: "Profit", - title: title("Cumulative Realized Profit"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.realized.realizedProfit.cumulative, - name, - color, - unit: Unit.usd, - }), - ), - }, - { - name: "Loss", - title: title("Cumulative Realized Loss"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.realized.realizedLoss.cumulative, - name, - color, - unit: Unit.usd, - }), - ), - }, - ]; -} - -/** - * Grouped sent in P/L - * @param {readonly CohortObject[]} list - * @param {CohortAll} all - * @param {(metric: string) => string} title - * @returns {PartialOptionsTree} - */ -function groupedSentInPnl(list, all, title) { - return [ - { - name: "Sum", - tree: [ - { - name: "In Profit", - title: title("Sent In Profit"), - bottom: [ - ...flatMapCohortsWithAll(list, all, ({ name, color, tree }) => - satsBtcUsd({ - pattern: tree.realized.sentInProfit14dEma, - name: `${name} 14d EMA`, - color, - defaultActive: false, - }), - ), - ...flatMapCohortsWithAll(list, all, ({ name, color, tree }) => - satsBtcUsdFrom({ - source: tree.realized.sentInProfit, - key: "base", - name, - color, - }), - ), - ], - }, - { - name: "In Loss", - title: title("Sent In Loss"), - bottom: [ - ...flatMapCohortsWithAll(list, all, ({ name, color, tree }) => - satsBtcUsd({ - pattern: tree.realized.sentInLoss14dEma, - name: `${name} 14d EMA`, - color, - defaultActive: false, - }), - ), - ...flatMapCohortsWithAll(list, all, ({ name, color, tree }) => - satsBtcUsdFrom({ - source: tree.realized.sentInLoss, - key: "base", - name, - color, - }), - ), - ], - }, - ], - }, - { - name: "Cumulative", - tree: [ - { - name: "In Profit", - title: title("Cumulative Sent In Profit"), - bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => - satsBtcUsdFrom({ - source: tree.realized.sentInProfit, - key: "cumulative", - name, - color, - }), - ), - }, - { - name: "In Loss", - title: title("Cumulative Sent In Loss"), - bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => - satsBtcUsdFrom({ - source: tree.realized.sentInLoss, - key: "cumulative", - name, - color, - }), - ), - }, - ], - }, - ]; -} - -/** - * Grouped sentiment - * @param {readonly CohortObject[]} list + * Grouped sentiment (full unrealized only) + * @param {readonly (CohortAll | CohortFull | CohortLongTerm)[]} list * @param {CohortAll} all * @param {(metric: string) => string} title * @returns {PartialOptionsGroup} @@ -1943,172 +1077,33 @@ function groupedSentiment(list, all, title) { name: "Net", title: title("Net Sentiment"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.unrealized.netSentiment, - name, - color, - unit: Unit.usd, - }), + baseline({ metric: tree.unrealized.sentiment.net.usd, name, color, unit: Unit.usd }), ), }, { name: "Greed", title: title("Greed Index"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.unrealized.greedIndex, - name, - color, - unit: Unit.usd, - }), + line({ metric: tree.unrealized.sentiment.greedIndex.usd, name, color, unit: Unit.usd }), ), }, { name: "Pain", title: title("Pain Index"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - line({ - metric: tree.unrealized.painIndex, - name, - color, - unit: Unit.usd, - }), + line({ metric: tree.unrealized.sentiment.painIndex.usd, name, color, unit: Unit.usd }), ), }, ], }; } -/** - * Grouped realized subfolder - * @param {readonly CohortObject[]} list - * @param {CohortAll} all - * @param {(metric: string) => string} title - * @returns {PartialOptionsGroup} - */ -function groupedRealizedSubfolder(list, all, title) { - return { - name: "Realized", - tree: [ - { name: "P&L", tree: groupedRealizedPnlSum(list, all, title) }, - { - name: "Net", - title: title("Net Realized P&L"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.realized.netRealizedPnl.height, - name, - color, - unit: Unit.usd, - }), - ), - }, - { - name: "30d Change", - title: title("Realized P&L 30d Change"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.realized.netRealizedPnlCumulative30dDelta, - name, - color, - unit: Unit.usd, - }), - ), - }, - { - name: "Rolling", - tree: groupedRollingRealizedValueCharts(list, all, title), - }, - { - name: "Cumulative", - tree: [ - { name: "P&L", tree: groupedRealizedPnlCumulative(list, all, title) }, - { - name: "Net", - title: title("Cumulative Net Realized P&L"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.realized.netRealizedPnl.cumulative, - name, - color, - unit: Unit.usd, - }), - ), - }, - ], - }, - ], - }; -} - -/** - * Grouped realized with extras - * @param {readonly (CohortAgeRange | CohortLongTerm | CohortAll | CohortFull)[]} list - * @param {CohortAll} all - * @param {(metric: string) => string} title - * @returns {PartialOptionsGroup} - */ -function groupedRealizedSubfolderWithExtras(list, all, title) { - return { - name: "Realized", - tree: [ - { name: "P&L", tree: groupedRealizedPnlSumWithExtras(list, all, title) }, - { - name: "Net", - title: title("Net Realized P&L"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.realized.netRealizedPnl.height, - name, - color, - unit: Unit.usd, - }), - ), - }, - { - name: "30d Change", - title: title("Realized P&L 30d Change"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.realized.netRealizedPnlCumulative30dDelta, - name, - color, - unit: Unit.usd, - }), - ), - }, - { - name: "Rolling", - tree: groupedRollingRealizedChartsWithExtras(list, all, title), - }, - { - name: "Cumulative", - tree: [ - { name: "P&L", tree: groupedRealizedPnlCumulative(list, all, title) }, - { - name: "Net", - title: title("Cumulative Net Realized P&L"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.realized.netRealizedPnl.cumulative, - name, - color, - unit: Unit.usd, - }), - ), - }, - ], - }, - ], - }; -} - // ============================================================================ // Grouped Section Builders // ============================================================================ /** - * Grouped profitability section (basic) + * Grouped profitability section (basic — NUPL only) * @param {{ list: readonly (UtxoCohortObject | CohortWithoutRelative)[], all: CohortAll, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ @@ -2116,53 +1111,33 @@ export function createGroupedProfitabilitySection({ list, all, title }) { return { name: "Profitability", tree: [ - { name: "Unrealized", tree: groupedPnlCharts(list, all, title) }, + { name: "Unrealized", tree: groupedNuplCharts(list, all, title) }, groupedRealizedSubfolder(list, all, title), - { name: "Volume", tree: groupedSentInPnl(list, all, title) }, - { - name: "Invested Capital", - tree: groupedInvestedCapitalAbsolute(list, all, title), - }, - groupedSentiment(list, all, title), ], }; } /** - * Grouped section with invested capital % (basic cohorts) + * Grouped section with invested capital % (basic cohorts — uses NUPL only) * @param {{ list: readonly CohortBasicWithoutMarketCap[], all: CohortAll, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ -export function createGroupedProfitabilitySectionBasicWithInvestedCapitalPct({ - list, - all, - title, -}) { +export function createGroupedProfitabilitySectionBasicWithInvestedCapitalPct({ list, all, title }) { return { name: "Profitability", tree: [ - { name: "Unrealized", tree: groupedPnlCharts(list, all, title) }, + { name: "Unrealized", tree: groupedNuplCharts(list, all, title) }, groupedRealizedSubfolder(list, all, title), - { name: "Volume", tree: groupedSentInPnl(list, all, title) }, - { - name: "Invested Capital", - tree: groupedInvestedCapital(list, all, title), - }, - groupedSentiment(list, all, title), ], }; } /** - * Grouped section for ageRange cohorts - * @param {{ list: readonly CohortAgeRange[], all: CohortAll, title: (metric: string) => string }} args + * Grouped section for ageRange/maxAge cohorts + * @param {{ list: readonly (CohortAgeRange | CohortWithAdjusted)[], all: CohortAll, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ -export function createGroupedProfitabilitySectionWithInvestedCapitalPct({ - list, - all, - title, -}) { +export function createGroupedProfitabilitySectionWithInvestedCapitalPct({ list, all, title }) { return { name: "Profitability", tree: [ @@ -2170,29 +1145,20 @@ export function createGroupedProfitabilitySectionWithInvestedCapitalPct({ name: "Unrealized", tree: [ ...groupedPnlChartsWithOwnMarketCap(list, all, title), + ...groupedNuplCharts(list, all, title), ], }, - groupedRealizedSubfolderWithExtras(list, all, title), - { name: "Volume", tree: groupedSentInPnl(list, all, title) }, - { - name: "Invested Capital", - tree: groupedInvestedCapital(list, all, title), - }, - groupedSentiment(list, all, title), + groupedRealizedSubfolder(list, all, title), ], }; } /** - * Grouped section with NUPL - * @param {{ list: readonly (CohortFull | CohortBasicWithMarketCap)[], all: CohortAll, title: (metric: string) => string }} args + * Grouped section with NUPL + relToMcap + * @param {{ list: readonly (CohortFull | CohortLongTerm)[], all: CohortAll, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ -export function createGroupedProfitabilitySectionWithNupl({ - list, - all, - title, -}) { +export function createGroupedProfitabilitySectionWithNupl({ list, all, title }) { return { name: "Profitability", tree: [ @@ -2200,27 +1166,10 @@ export function createGroupedProfitabilitySectionWithNupl({ name: "Unrealized", tree: [ ...groupedPnlChartsWithMarketCap(list, all, title), - { - name: "NUPL", - title: title("NUPL"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.relative.nupl, - name, - color, - unit: Unit.ratio, - }), - ), - }, + ...groupedNuplCharts(list, all, title), ], }, groupedRealizedSubfolder(list, all, title), - { name: "Volume", tree: groupedSentInPnl(list, all, title) }, - { - name: "Invested Capital", - tree: groupedInvestedCapital(list, all, title), - }, - groupedSentiment(list, all, title), ], }; } @@ -2230,11 +1179,7 @@ export function createGroupedProfitabilitySectionWithNupl({ * @param {{ list: readonly CohortLongTerm[], all: CohortAll, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ -export function createGroupedProfitabilitySectionLongTerm({ - list, - all, - title, -}) { +export function createGroupedProfitabilitySectionLongTerm({ list, all, title }) { return { name: "Profitability", tree: [ @@ -2242,28 +1187,11 @@ export function createGroupedProfitabilitySectionLongTerm({ name: "Unrealized", tree: [ ...groupedPnlChartsLongTerm(list, all, title), - { - name: "NUPL", - title: title("NUPL"), - bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ - metric: tree.relative.nupl, - name, - color, - unit: Unit.ratio, - }), - ), - }, + ...groupedNuplCharts(list, all, title), ], }, - groupedRealizedSubfolderWithExtras(list, all, title), - { name: "Volume", tree: groupedSentInPnl(list, all, title) }, - { - name: "Invested Capital", - tree: groupedInvestedCapital(list, all, title), - }, + groupedRealizedSubfolderFull(list, all, title), groupedSentiment(list, all, title), ], }; } - diff --git a/website/scripts/options/distribution/valuation.js b/website/scripts/options/distribution/valuation.js index 52c0709c7..ba0223bd3 100644 --- a/website/scripts/options/distribution/valuation.js +++ b/website/scripts/options/distribution/valuation.js @@ -22,7 +22,7 @@ function createSingleRealizedCapSeries(cohort) { const { color, tree } = cohort; return [ line({ - metric: tree.realized.realizedCap, + metric: tree.realized.cap.usd, name: "Realized Cap", color, unit: Unit.usd, @@ -37,7 +37,7 @@ function createSingleRealizedCapSeries(cohort) { function createSingle30dChangeSeries(cohort) { return [ baseline({ - metric: cohort.tree.realized.realizedCap30dDelta, + metric: cohort.tree.realized.cap.delta.change._1m.usd, name: "30d Change", unit: Unit.usd, }), @@ -47,7 +47,7 @@ function createSingle30dChangeSeries(cohort) { /** * Create valuation section for cohorts with full ratio patterns * (CohortAll, CohortFull, CohortWithPercentiles) - * @param {{ cohort: CohortAll | CohortFull | CohortWithPercentiles, title: (metric: string) => string }} args + * @param {{ cohort: CohortAll | CohortFull | CohortLongTerm, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ export function createValuationSectionFull({ cohort, title }) { @@ -61,7 +61,7 @@ export function createValuationSectionFull({ cohort, title }) { bottom: [ ...createSingleRealizedCapSeries(cohort), baseline({ - metric: tree.realized.realizedCapRelToOwnMarketCap, + metric: tree.realized.cap.relToOwnMcap.percent, name: "Rel. to Own M.Cap", color, unit: Unit.pctOwnMcap, @@ -75,8 +75,8 @@ export function createValuationSectionFull({ cohort, title }) { }, createRatioChart({ title, - pricePattern: tree.realized.realizedPrice, - ratio: { ...tree.realized.realizedPriceExtra, ...tree.realized.realizedPriceRatioExt }, + pricePattern: tree.realized.price, + ratio: tree.realized.price, color, name: "MVRV", }), @@ -110,7 +110,7 @@ export function createValuationSection({ cohort, title }) { title: title("MVRV"), bottom: [ baseline({ - metric: tree.realized.realizedPriceExtra.ratio, + metric: tree.realized.mvrv, name: "MVRV", unit: Unit.ratio, base: 1, @@ -134,7 +134,7 @@ export function createGroupedValuationSection({ list, all, title }) { title: title("Realized Cap"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ - metric: tree.realized.realizedCap, + metric: tree.realized.cap.usd, name, color, unit: Unit.usd, @@ -146,7 +146,7 @@ export function createGroupedValuationSection({ list, all, title }) { title: title("Realized Cap 30d Change"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ - metric: tree.realized.realizedCap30dDelta, + metric: tree.realized.cap.delta.change._1m.usd, name, color, unit: Unit.usd, @@ -158,7 +158,7 @@ export function createGroupedValuationSection({ list, all, title }) { title: title("MVRV"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ - metric: tree.realized.realizedPriceExtra.ratio, + metric: tree.realized.mvrv, name, color, unit: Unit.ratio, @@ -171,7 +171,7 @@ export function createGroupedValuationSection({ list, all, title }) { } /** - * @param {{ list: readonly (CohortAll | CohortFull | CohortWithPercentiles)[], all: CohortAll, title: (metric: string) => string }} args + * @param {{ list: readonly (CohortAll | CohortFull | CohortLongTerm)[], all: CohortAll, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ export function createGroupedValuationSectionWithOwnMarketCap({ @@ -188,7 +188,7 @@ export function createGroupedValuationSectionWithOwnMarketCap({ bottom: [ ...mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ - metric: tree.realized.realizedCap, + metric: tree.realized.cap.usd, name, color, unit: Unit.usd, @@ -196,7 +196,7 @@ export function createGroupedValuationSectionWithOwnMarketCap({ ), ...mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ - metric: tree.realized.realizedCapRelToOwnMarketCap, + metric: tree.realized.cap.relToOwnMcap.percent, name, color, unit: Unit.pctOwnMcap, @@ -209,7 +209,7 @@ export function createGroupedValuationSectionWithOwnMarketCap({ title: title("Realized Cap 30d Change"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ - metric: tree.realized.realizedCap30dDelta, + metric: tree.realized.cap.delta.change._1m.usd, name, color, unit: Unit.usd, @@ -221,7 +221,7 @@ export function createGroupedValuationSectionWithOwnMarketCap({ title: title("MVRV"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ - metric: tree.realized.realizedPriceExtra.ratio, + metric: tree.realized.mvrv, name, color, unit: Unit.ratio, diff --git a/website/scripts/options/investing.js b/website/scripts/options/investing.js index 1677c3ea1..ad7a066e6 100644 --- a/website/scripts/options/investing.js +++ b/website/scripts/options/investing.js @@ -3,7 +3,7 @@ import { colors } from "../utils/colors.js"; import { brk } from "../client.js"; import { Unit } from "../utils/units.js"; -import { line, baseline, price, dotted } from "./series.js"; +import { baseline, price } from "./series.js"; import { satsBtcUsd } from "./shared.js"; import { periodIdToName } from "./utils.js"; @@ -42,7 +42,7 @@ const YEARS_2020S = /** @type {const} */ ([ const YEARS_2010S = /** @type {const} */ ([2019, 2018, 2017, 2016, 2015]); /** @typedef {typeof YEARS_2020S[number] | typeof YEARS_2010S[number]} DcaYear */ -/** @typedef {`_${DcaYear}`} DcaYearKey */ +/** @typedef {`from${DcaYear}`} DcaYearKey */ /** @param {AllPeriodKey} key */ const periodName = (key) => periodIdToName(key.slice(1), true); @@ -54,10 +54,6 @@ const periodName = (key) => periodIdToName(key.slice(1), true); * @property {Color} color - Item color * @property {AnyPricePattern} costBasis - Cost basis metric * @property {AnyMetricPattern} returns - Returns metric - * @property {AnyMetricPattern} minReturn - Min return metric - * @property {AnyMetricPattern} maxReturn - Max return metric - * @property {AnyMetricPattern} daysInProfit - Days in profit metric - * @property {AnyMetricPattern} daysInLoss - Days in loss metric * @property {AnyValuePattern} stack - Stack pattern */ @@ -76,17 +72,13 @@ const ALL_YEARS = /** @type {const} */ ([...YEARS_2020S, ...YEARS_2010S]); * @returns {BaseEntryItem} */ function buildYearEntry(dca, year, i) { - const key = /** @type {DcaYearKey} */ (`_${year}`); + const key = /** @type {DcaYearKey} */ (`from${year}`); return { name: `${year}`, color: colors.at(i, ALL_YEARS.length), - costBasis: dca.classAveragePrice[key], - returns: dca.classReturns[key], - minReturn: dca.classMinReturn[key], - maxReturn: dca.classMaxReturn[key], - daysInProfit: dca.classDaysInProfit[key], - daysInLoss: dca.classDaysInLoss[key], - stack: dca.classStack[key], + costBasis: dca.class.costBasis[key], + returns: dca.class.return[key].ratio, + stack: dca.class.stack[key], }; } @@ -109,42 +101,10 @@ export function createInvestingSection() { }; } -/** - * Create profitability folder for compare charts - * @param {string} context - * @param {Pick[]} items - */ -function createProfitabilityFolder(context, items) { - const top = items.map(({ name, color, costBasis }) => - price({ metric: costBasis, name, color }), - ); - return { - name: "Profitability", - tree: [ - { - name: "Days in Profit", - title: `Days in Profit: ${context}`, - top, - bottom: items.map(({ name, color, daysInProfit }) => - line({ metric: daysInProfit, name, color, unit: Unit.days }), - ), - }, - { - name: "Days in Loss", - title: `Days in Loss: ${context}`, - top, - bottom: items.map(({ name, color, daysInLoss }) => - line({ metric: daysInLoss, name, color, unit: Unit.days }), - ), - }, - ], - }; -} - /** * Create compare folder from items * @param {string} context - * @param {Pick[]} items + * @param {Pick[]} items */ function createCompareFolder(context, items) { const topPane = items.map(({ name, color, costBasis }) => @@ -171,7 +131,6 @@ function createCompareFolder(context, items) { }), ), }, - createProfitabilityFolder(context, items), { name: "Accumulated", title: `Accumulated Value: ${context}`, @@ -190,15 +149,7 @@ function createCompareFolder(context, items) { * @param {object[]} returnsBottom - Bottom pane items for returns chart */ function createSingleEntryTree(item, returnsBottom) { - const { - name, - titlePrefix = name, - color, - costBasis, - daysInProfit, - daysInLoss, - stack, - } = item; + const { name, titlePrefix = name, color, costBasis, stack } = item; const top = [price({ metric: costBasis, name: "Cost Basis", color })]; return { name, @@ -210,25 +161,6 @@ function createSingleEntryTree(item, returnsBottom) { top, bottom: returnsBottom, }, - { - name: "Profitability", - title: `Profitability: ${titlePrefix}`, - top, - bottom: [ - line({ - metric: daysInProfit, - name: "Days in Profit", - color: colors.profit, - unit: Unit.days, - }), - line({ - metric: daysInLoss, - name: "Days in Loss", - color: colors.loss, - unit: Unit.days, - }), - ], - }, { name: "Accumulated", title: `Accumulated Value: ${titlePrefix}`, @@ -244,23 +176,8 @@ function createSingleEntryTree(item, returnsBottom) { * @param {BaseEntryItem & { titlePrefix?: string }} item */ function createShortSingleEntry(item) { - const { returns, minReturn, maxReturn } = item; return createSingleEntryTree(item, [ - baseline({ metric: returns, name: "Current", unit: Unit.percentage }), - dotted({ - metric: maxReturn, - name: "Max", - color: colors.profit, - unit: Unit.percentage, - defaultActive: false, - }), - dotted({ - metric: minReturn, - name: "Min", - color: colors.loss, - unit: Unit.percentage, - defaultActive: false, - }), + baseline({ metric: item.returns, name: "Current", unit: Unit.percentage }), ]); } @@ -269,24 +186,9 @@ function createShortSingleEntry(item) { * @param {LongEntryItem & { titlePrefix?: string }} item */ function createLongSingleEntry(item) { - const { returns, minReturn, maxReturn, cagr } = item; return createSingleEntryTree(item, [ - baseline({ metric: returns, name: "Current", unit: Unit.percentage }), - baseline({ metric: cagr, name: "CAGR", unit: Unit.cagr }), - dotted({ - metric: maxReturn, - name: "Max", - color: colors.profit, - unit: Unit.percentage, - defaultActive: false, - }), - dotted({ - metric: minReturn, - name: "Min", - color: colors.loss, - unit: Unit.percentage, - defaultActive: false, - }), + baseline({ metric: item.returns, name: "Current", unit: Unit.percentage }), + baseline({ metric: item.cagr, name: "CAGR", unit: Unit.cagr }), ]); } @@ -301,7 +203,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) { /** @param {AllPeriodKey} key */ const topPane = (key) => [ price({ - metric: dca.periodAveragePrice[key], + metric: dca.period.costBasis[key], name: "DCA", color: colors.profit, }), @@ -315,151 +217,54 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) { top: topPane(key), }); - /** @param {string} name @param {AllPeriodKey} key */ - const returnsMinMax = (name, key) => [ - { - name: "Max", - title: `Max Return: ${name} DCA vs Lump Sum`, - top: topPane(key), - bottom: [ - baseline({ - metric: dca.periodMaxReturn[key], - name: "DCA", - unit: Unit.percentage, - }), - baseline({ - metric: dca.periodLumpSumMaxReturn[key], - name: "Lump Sum", - color: colors.bi.p2, - unit: Unit.percentage, - }), - ], - }, - { - name: "Min", - title: `Min Return: ${name} DCA vs Lump Sum`, - top: topPane(key), - bottom: [ - baseline({ - metric: dca.periodMinReturn[key], - name: "DCA", - unit: Unit.percentage, - }), - baseline({ - metric: dca.periodLumpSumMinReturn[key], - name: "Lump Sum", - color: colors.bi.p2, - unit: Unit.percentage, - }), - ], - }, - ]; - /** @param {string} name @param {ShortPeriodKey} key */ - const shortReturnsFolder = (name, key) => ({ + const shortReturnsChart = (name, key) => ({ name: "Returns", - tree: [ - { - name: "Current", - title: `Returns: ${name} DCA vs Lump Sum`, - top: topPane(key), - bottom: [ - baseline({ - metric: dca.periodReturns[key], - name: "DCA", - unit: Unit.percentage, - }), - baseline({ - metric: dca.periodLumpSumReturns[key], - name: "Lump Sum", - color: colors.bi.p2, - unit: Unit.percentage, - }), - ], - }, - ...returnsMinMax(name, key), + title: `Returns: ${name} DCA vs Lump Sum`, + top: topPane(key), + bottom: [ + baseline({ + metric: dca.period.return[key].ratio, + name: "DCA", + unit: Unit.percentage, + }), + baseline({ + metric: dca.period.lumpSumReturn[key].ratio, + name: "Lump Sum", + color: colors.bi.p2, + unit: Unit.percentage, + }), ], }); /** @param {string} name @param {LongPeriodKey} key */ - const longReturnsFolder = (name, key) => ({ + const longReturnsChart = (name, key) => ({ name: "Returns", - tree: [ - { - name: "Current", - title: `Returns: ${name} DCA vs Lump Sum`, - top: topPane(key), - bottom: [ - baseline({ - metric: dca.periodReturns[key], - name: "DCA", - unit: Unit.percentage, - }), - baseline({ - metric: dca.periodLumpSumReturns[key], - name: "Lump Sum", - color: colors.bi.p2, - unit: Unit.percentage, - }), - baseline({ - metric: dca.periodCagr[key], - name: "DCA", - unit: Unit.cagr, - }), - baseline({ - metric: returns.cagr[key], - name: "Lump Sum", - color: colors.bi.p2, - unit: Unit.cagr, - }), - ], - }, - ...returnsMinMax(name, key), - ], - }); - - /** @param {string} name @param {AllPeriodKey} key */ - const profitabilityFolder = (name, key) => ({ - name: "Profitability", - tree: [ - { - name: "Days in Profit", - title: `Days in Profit: ${name} DCA vs Lump Sum`, - top: topPane(key), - bottom: [ - line({ - metric: dca.periodDaysInProfit[key], - name: "DCA", - color: colors.profit, - unit: Unit.days, - }), - line({ - metric: dca.periodLumpSumDaysInProfit[key], - name: "Lump Sum", - color: colors.bitcoin, - unit: Unit.days, - }), - ], - }, - { - name: "Days in Loss", - title: `Days in Loss: ${name} DCA vs Lump Sum`, - top: topPane(key), - bottom: [ - line({ - metric: dca.periodDaysInLoss[key], - name: "DCA", - color: colors.profit, - unit: Unit.days, - }), - line({ - metric: dca.periodLumpSumDaysInLoss[key], - name: "Lump Sum", - color: colors.bitcoin, - unit: Unit.days, - }), - ], - }, + title: `Returns: ${name} DCA vs Lump Sum`, + top: topPane(key), + bottom: [ + baseline({ + metric: dca.period.return[key].ratio, + name: "DCA", + unit: Unit.percentage, + }), + baseline({ + metric: dca.period.lumpSumReturn[key].ratio, + name: "Lump Sum", + color: colors.bi.p2, + unit: Unit.percentage, + }), + baseline({ + metric: dca.period.cagr[key].ratio, + name: "DCA CAGR", + unit: Unit.cagr, + }), + baseline({ + metric: returns.cagr[key].ratio, + name: "Lump Sum CAGR", + color: colors.bi.p2, + unit: Unit.cagr, + }), ], }); @@ -470,12 +275,12 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) { top: topPane(key), bottom: [ ...satsBtcUsd({ - pattern: dca.periodStack[key], + pattern: dca.period.stack[key], name: "DCA", color: colors.profit, }), ...satsBtcUsd({ - pattern: dca.periodLumpSumStack[key], + pattern: dca.period.lumpSumStack[key], name: "Lump Sum", color: colors.bitcoin, }), @@ -489,8 +294,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) { name, tree: [ costBasisChart(name, key), - shortReturnsFolder(name, key), - profitabilityFolder(name, key), + shortReturnsChart(name, key), stackChart(name, key), ], }; @@ -503,8 +307,7 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) { name, tree: [ costBasisChart(name, key), - longReturnsFolder(name, key), - profitabilityFolder(name, key), + longReturnsChart(name, key), stackChart(name, key), ], }; @@ -545,28 +348,20 @@ function createPeriodSection({ dca, lookback, returns }) { const buildBaseEntry = (key, i) => ({ name: periodName(key), color: colors.at(i, allPeriods.length), - costBasis: isLumpSum ? lookback[key] : dca.periodAveragePrice[key], - returns: isLumpSum ? dca.periodLumpSumReturns[key] : dca.periodReturns[key], - minReturn: isLumpSum - ? dca.periodLumpSumMinReturn[key] - : dca.periodMinReturn[key], - maxReturn: isLumpSum - ? dca.periodLumpSumMaxReturn[key] - : dca.periodMaxReturn[key], - daysInProfit: isLumpSum - ? dca.periodLumpSumDaysInProfit[key] - : dca.periodDaysInProfit[key], - daysInLoss: isLumpSum - ? dca.periodLumpSumDaysInLoss[key] - : dca.periodDaysInLoss[key], - stack: isLumpSum ? dca.periodLumpSumStack[key] : dca.periodStack[key], + costBasis: isLumpSum ? lookback[key] : dca.period.costBasis[key], + returns: isLumpSum + ? dca.period.lumpSumReturn[key].ratio + : dca.period.return[key].ratio, + stack: isLumpSum + ? dca.period.lumpSumStack[key] + : dca.period.stack[key], }); /** @param {LongPeriodKey} key @param {number} i @returns {LongEntryItem} */ const buildLongEntry = (key, i) => withCagr( buildBaseEntry(key, i), - isLumpSum ? returns.cagr[key] : dca.periodCagr[key], + isLumpSum ? returns.cagr[key].ratio : dca.period.cagr[key].ratio, ); /** @param {BaseEntryItem} entry */ diff --git a/website/scripts/options/market.js b/website/scripts/options/market.js index d4fa09005..ad5202f64 100644 --- a/website/scripts/options/market.js +++ b/website/scripts/options/market.js @@ -6,7 +6,6 @@ import { includes } from "../utils/array.js"; import { Unit } from "../utils/units.js"; import { priceLine, priceLines } from "./constants.js"; import { baseline, histogram, line, price } from "./series.js"; -import { createPriceRatioCharts } from "./shared.js"; import { periodIdToName } from "./utils.js"; /** @@ -26,7 +25,7 @@ import { periodIdToName } from "./utils.js"; * @typedef {Object} MaPeriod * @property {string} id * @property {Color} color - * @property {ActivePriceRatioPattern} ratio + * @property {Brk.BpsCentsRatioSatsUsdPattern} ratio */ const commonMaIds = /** @type {const} */ ([ @@ -49,13 +48,26 @@ function createMaSubSection(label, averages) { /** @param {MaPeriod} a */ const toFolder = (a) => ({ name: periodIdToName(a.id, true), - tree: createPriceRatioCharts({ - context: `${periodIdToName(a.id, true)} ${label}`, - legend: "average", - pricePattern: a.ratio.price, - ratio: a.ratio, - color: a.color, - }), + tree: [ + { + name: "Price", + title: `${periodIdToName(a.id, true)} ${label}`, + top: [price({ metric: a.ratio, name: "average", color: a.color })], + }, + { + name: "Ratio", + title: `${periodIdToName(a.id, true)} ${label} Ratio`, + top: [price({ metric: a.ratio, name: "average", color: a.color })], + bottom: [ + baseline({ + metric: a.ratio.ratio, + name: "Ratio", + color: a.color, + unit: Unit.ratio, + }), + ], + }, + ], }); return { @@ -65,7 +77,7 @@ function createMaSubSection(label, averages) { name: "Compare", title: `Price ${label}s`, top: averages.map((a) => - price({ metric: a.ratio.price, name: a.id, color: a.color }), + price({ metric: a.ratio, name: a.id, color: a.color }), ), }, ...common.map(toFolder), @@ -184,83 +196,83 @@ function historicalSubSection(name, periods) { * @returns {PartialOptionsGroup} */ export function createMarketSection() { - const { market, supply, distribution, prices } = brk.metrics; + const { market, supply, cohorts, prices, indicators } = brk.metrics; const { movingAverage: ma, ath, returns, volatility, range, - indicators, + technical, lookback, dca, } = market; const shortPeriodsBase = [ - { id: "24h", returns: returns.priceReturns._24h, lookback: lookback._24h }, - { id: "1w", returns: returns.priceReturns._1w, lookback: lookback._1w }, - { id: "1m", returns: returns.priceReturns._1m, lookback: lookback._1m }, + { id: "24h", returns: returns.periods._24h.ratio, lookback: lookback._24h }, + { id: "1w", returns: returns.periods._1w.ratio, lookback: lookback._1w }, + { id: "1m", returns: returns.periods._1m.ratio, lookback: lookback._1m }, { id: "3m", - returns: returns.priceReturns._3m, + returns: returns.periods._3m.ratio, lookback: lookback._3m, defaultActive: false, }, { id: "6m", - returns: returns.priceReturns._6m, + returns: returns.periods._6m.ratio, lookback: lookback._6m, defaultActive: false, }, - { id: "1y", returns: returns.priceReturns._1y, lookback: lookback._1y }, + { id: "1y", returns: returns.periods._1y.ratio, lookback: lookback._1y }, ]; const longPeriodsBase = [ { id: "2y", - returns: returns.priceReturns._2y, - cagr: returns.cagr._2y, + returns: returns.periods._2y.ratio, + cagr: returns.cagr._2y.ratio, lookback: lookback._2y, defaultActive: false, }, { id: "3y", - returns: returns.priceReturns._3y, - cagr: returns.cagr._3y, + returns: returns.periods._3y.ratio, + cagr: returns.cagr._3y.ratio, lookback: lookback._3y, defaultActive: false, }, { id: "4y", - returns: returns.priceReturns._4y, - cagr: returns.cagr._4y, + returns: returns.periods._4y.ratio, + cagr: returns.cagr._4y.ratio, lookback: lookback._4y, }, { id: "5y", - returns: returns.priceReturns._5y, - cagr: returns.cagr._5y, + returns: returns.periods._5y.ratio, + cagr: returns.cagr._5y.ratio, lookback: lookback._5y, defaultActive: false, }, { id: "6y", - returns: returns.priceReturns._6y, - cagr: returns.cagr._6y, + returns: returns.periods._6y.ratio, + cagr: returns.cagr._6y.ratio, lookback: lookback._6y, defaultActive: false, }, { id: "8y", - returns: returns.priceReturns._8y, - cagr: returns.cagr._8y, + returns: returns.periods._8y.ratio, + cagr: returns.cagr._8y.ratio, lookback: lookback._8y, defaultActive: false, }, { id: "10y", - returns: returns.priceReturns._10y, - cagr: returns.cagr._10y, + returns: returns.periods._10y.ratio, + cagr: returns.cagr._10y.ratio, lookback: lookback._10y, defaultActive: false, }, @@ -282,42 +294,42 @@ export function createMarketSection() { /** @type {MaPeriod[]} */ const sma = [ - { id: "1w", ratio: ma.price1wSma }, - { id: "8d", ratio: ma.price8dSma }, - { id: "13d", ratio: ma.price13dSma }, - { id: "21d", ratio: ma.price21dSma }, - { id: "1m", ratio: ma.price1mSma }, - { id: "34d", ratio: ma.price34dSma }, - { id: "55d", ratio: ma.price55dSma }, - { id: "89d", ratio: ma.price89dSma }, - { id: "111d", ratio: ma.price111dSma }, - { id: "144d", ratio: ma.price144dSma }, - { id: "200d", ratio: ma.price200dSma }, - { id: "350d", ratio: ma.price350dSma }, - { id: "1y", ratio: ma.price1ySma }, - { id: "2y", ratio: ma.price2ySma }, - { id: "200w", ratio: ma.price200wSma }, - { id: "4y", ratio: ma.price4ySma }, + { id: "1w", ratio: ma.sma._1w }, + { id: "8d", ratio: ma.sma._8d }, + { id: "13d", ratio: ma.sma._13d }, + { id: "21d", ratio: ma.sma._21d }, + { id: "1m", ratio: ma.sma._1m }, + { id: "34d", ratio: ma.sma._34d }, + { id: "55d", ratio: ma.sma._55d }, + { id: "89d", ratio: ma.sma._89d }, + { id: "111d", ratio: ma.sma._111d }, + { id: "144d", ratio: ma.sma._144d }, + { id: "200d", ratio: ma.sma._200d }, + { id: "350d", ratio: ma.sma._350d }, + { id: "1y", ratio: ma.sma._1y }, + { id: "2y", ratio: ma.sma._2y }, + { id: "200w", ratio: ma.sma._200w }, + { id: "4y", ratio: ma.sma._4y }, ].map((p, i, arr) => ({ ...p, color: colors.at(i, arr.length) })); /** @type {MaPeriod[]} */ const ema = [ - { id: "1w", ratio: ma.price1wEma }, - { id: "8d", ratio: ma.price8dEma }, - { id: "12d", ratio: ma.price12dEma }, - { id: "13d", ratio: ma.price13dEma }, - { id: "21d", ratio: ma.price21dEma }, - { id: "26d", ratio: ma.price26dEma }, - { id: "1m", ratio: ma.price1mEma }, - { id: "34d", ratio: ma.price34dEma }, - { id: "55d", ratio: ma.price55dEma }, - { id: "89d", ratio: ma.price89dEma }, - { id: "144d", ratio: ma.price144dEma }, - { id: "200d", ratio: ma.price200dEma }, - { id: "1y", ratio: ma.price1yEma }, - { id: "2y", ratio: ma.price2yEma }, - { id: "200w", ratio: ma.price200wEma }, - { id: "4y", ratio: ma.price4yEma }, + { id: "1w", ratio: ma.ema._1w }, + { id: "8d", ratio: ma.ema._8d }, + { id: "12d", ratio: ma.ema._12d }, + { id: "13d", ratio: ma.ema._13d }, + { id: "21d", ratio: ma.ema._21d }, + { id: "26d", ratio: ma.ema._26d }, + { id: "1m", ratio: ma.ema._1m }, + { id: "34d", ratio: ma.ema._34d }, + { id: "55d", ratio: ma.ema._55d }, + { id: "89d", ratio: ma.ema._89d }, + { id: "144d", ratio: ma.ema._144d }, + { id: "200d", ratio: ma.ema._200d }, + { id: "1y", ratio: ma.ema._1y }, + { id: "2y", ratio: ma.ema._2y }, + { id: "200w", ratio: ma.ema._200w }, + { id: "4y", ratio: ma.ema._4y }, ].map((p, i, arr) => ({ ...p, color: colors.at(i, arr.length) })); // SMA vs EMA comparison periods (common periods only) @@ -325,38 +337,38 @@ export function createMarketSection() { { id: "1w", name: "1 Week", - sma: ma.price1wSma, - ema: ma.price1wEma, + sma: ma.sma._1w, + ema: ma.ema._1w, }, { id: "1m", name: "1 Month", - sma: ma.price1mSma, - ema: ma.price1mEma, + sma: ma.sma._1m, + ema: ma.ema._1m, }, { id: "200d", name: "200 Day", - sma: ma.price200dSma, - ema: ma.price200dEma, + sma: ma.sma._200d, + ema: ma.ema._200d, }, { id: "1y", name: "1 Year", - sma: ma.price1ySma, - ema: ma.price1yEma, + sma: ma.sma._1y, + ema: ma.ema._1y, }, { id: "200w", name: "200 Week", - sma: ma.price200wSma, - ema: ma.price200wEma, + sma: ma.sma._200w, + ema: ma.ema._200w, }, { id: "4y", name: "4 Year", - sma: ma.price4ySma, - ema: ma.price4yEma, + sma: ma.sma._4y, + ema: ma.ema._4y, }, ].map((p, i, arr) => ({ ...p, color: colors.at(i, arr.length) })); @@ -370,7 +382,7 @@ export function createMarketSection() { title: "Sats per Dollar", bottom: [ line({ - metric: prices.price.sats, + metric: prices.spot.sats, name: "Sats/$", unit: Unit.sats, }), @@ -385,7 +397,7 @@ export function createMarketSection() { title: "Market Capitalization", bottom: [ line({ - metric: supply.marketCap, + metric: supply.marketCap.usd, name: "Market Cap", unit: Unit.usd, }), @@ -396,7 +408,7 @@ export function createMarketSection() { title: "Realized Capitalization", bottom: [ line({ - metric: distribution.utxoCohorts.all.realized.realizedCap, + metric: cohorts.utxo.all.realized.cap.usd, name: "Realized Cap", color: colors.realized, unit: Unit.usd, @@ -408,20 +420,14 @@ export function createMarketSection() { title: "Capitalization Growth Rate", bottom: [ line({ - metric: supply.marketCapGrowthRate, - name: "Market Cap", + metric: supply.marketCap.delta.rate._24h.percent, + name: "Market Cap (24h)", color: colors.bitcoin, unit: Unit.percentage, }), - line({ - metric: supply.realizedCapGrowthRate, - name: "Realized Cap", - color: colors.usd, - unit: Unit.percentage, - }), baseline({ - metric: supply.capGrowthRateDiff, - name: "Difference", + metric: supply.marketMinusRealizedCapGrowthRate._24h, + name: "Market - Realized", unit: Unit.percentage, }), ], @@ -435,10 +441,10 @@ export function createMarketSection() { { name: "Drawdown", title: "ATH Drawdown", - top: [price({ metric: ath.priceAth, name: "ATH" })], + top: [price({ metric: ath.high, name: "ATH" })], bottom: [ line({ - metric: ath.priceDrawdown, + metric: ath.drawdown.percent, name: "Drawdown", color: colors.loss, unit: Unit.percentage, @@ -448,26 +454,26 @@ export function createMarketSection() { { name: "Time Since", title: "Time Since ATH", - top: [price({ metric: ath.priceAth, name: "ATH" })], + top: [price({ metric: ath.high, name: "ATH" })], bottom: [ line({ - metric: ath.daysSincePriceAth, + metric: ath.daysSince, name: "Since", unit: Unit.days, }), line({ - metric: ath.yearsSincePriceAth, + metric: ath.yearsSince, name: "Since", unit: Unit.years, }), line({ - metric: ath.maxDaysBetweenPriceAths, + metric: ath.maxDaysBetween, name: "Max", color: colors.loss, unit: Unit.days, }), line({ - metric: ath.maxYearsBetweenPriceAths, + metric: ath.maxYearsBetween, name: "Max", color: colors.loss, unit: Unit.years, @@ -502,22 +508,22 @@ export function createMarketSection() { name: "Volatility", tree: [ volatilityChart("Index", "Volatility Index", Unit.percentage, { - _1w: volatility.price1wVolatility, - _1m: volatility.price1mVolatility, - _1y: volatility.price1yVolatility, + _1w: volatility._1w, + _1m: volatility._1m, + _1y: volatility._1y, }), { name: "True Range", title: "True Range", bottom: [ line({ - metric: range.priceTrueRange, + metric: range.trueRange, name: "Daily", color: colors.time._24h, unit: Unit.usd, }), line({ - metric: range.priceTrueRange2wSum, + metric: range.trueRangeSum2w, name: "2w Sum", color: colors.time._1w, unit: Unit.usd, @@ -530,7 +536,7 @@ export function createMarketSection() { title: "Choppiness Index", bottom: [ line({ - metric: range.price2wChoppinessIndex, + metric: range.choppinessIndex2w.percent, name: "2w", color: colors.indicator.main, unit: Unit.index, @@ -552,12 +558,12 @@ export function createMarketSection() { title: "SMA vs EMA Comparison", top: smaVsEma.flatMap((p) => [ price({ - metric: p.sma.price, + metric: p.sma, name: `${p.id} SMA`, color: p.color, }), price({ - metric: p.ema.price, + metric: p.ema, name: `${p.id} EMA`, color: p.color, style: 1, @@ -568,9 +574,9 @@ export function createMarketSection() { name: p.name, title: `${p.name} SMA vs EMA`, top: [ - price({ metric: p.sma.price, name: "SMA", color: p.color }), + price({ metric: p.sma, name: "SMA", color: p.color }), price({ - metric: p.ema.price, + metric: p.ema, name: "EMA", color: p.color, style: 1, @@ -593,26 +599,26 @@ export function createMarketSection() { { id: "1w", name: "1 Week", - min: range.price1wMin, - max: range.price1wMax, + min: range.min._1w, + max: range.max._1w, }, { id: "2w", name: "2 Week", - min: range.price2wMin, - max: range.price2wMax, + min: range.min._2w, + max: range.max._2w, }, { id: "1m", name: "1 Month", - min: range.price1mMin, - max: range.price1mMax, + min: range.min._1m, + max: range.max._1m, }, { id: "1y", name: "1 Year", - min: range.price1yMin, - max: range.price1yMax, + min: range.min._1y, + max: range.max._1y, }, ].map((p) => ({ name: p.id, @@ -638,17 +644,17 @@ export function createMarketSection() { title: "Mayer Multiple", top: [ price({ - metric: ma.price200dSma.price, + metric: ma.sma._200d, name: "200d SMA", color: colors.indicator.main, }), price({ - metric: ma.price200dSmaX24, + metric: ma.sma._200d.x24, name: "200d SMA x2.4", color: colors.indicator.upper, }), price({ - metric: ma.price200dSmaX08, + metric: ma.sma._200d.x08, name: "200d SMA x0.8", color: colors.indicator.lower, }), @@ -668,25 +674,25 @@ export function createMarketSection() { title: "RSI Comparison", bottom: [ line({ - metric: indicators.rsi._1d.rsi, + metric: technical.rsi._24h.rsi.percent, name: "1d", color: colors.time._24h, unit: Unit.index, }), line({ - metric: indicators.rsi._1w.rsi, + metric: technical.rsi._1w.rsi.percent, name: "1w", color: colors.time._1w, unit: Unit.index, }), line({ - metric: indicators.rsi._1m.rsi, + metric: technical.rsi._1m.rsi.percent, name: "1m", color: colors.time._1m, unit: Unit.index, }), line({ - metric: indicators.rsi._1y.rsi, + metric: technical.rsi._1y.rsi.percent, name: "1y", color: colors.time._1y, unit: Unit.index, @@ -700,20 +706,20 @@ export function createMarketSection() { title: "RSI (1d)", bottom: [ line({ - metric: indicators.rsi._1d.rsi, + metric: technical.rsi._24h.rsi.percent, name: "RSI", color: colors.indicator.main, unit: Unit.index, }), line({ - metric: indicators.rsi._1d.rsiMax, + metric: technical.rsi._24h.rsiMax.percent, name: "Max", color: colors.stat.max, defaultActive: false, unit: Unit.index, }), line({ - metric: indicators.rsi._1d.rsiMin, + metric: technical.rsi._24h.rsiMin.percent, name: "Min", color: colors.stat.min, defaultActive: false, @@ -733,20 +739,20 @@ export function createMarketSection() { title: "RSI (1w)", bottom: [ line({ - metric: indicators.rsi._1w.rsi, + metric: technical.rsi._1w.rsi.percent, name: "RSI", color: colors.indicator.main, unit: Unit.index, }), line({ - metric: indicators.rsi._1w.rsiMax, + metric: technical.rsi._1w.rsiMax.percent, name: "Max", color: colors.stat.max, defaultActive: false, unit: Unit.index, }), line({ - metric: indicators.rsi._1w.rsiMin, + metric: technical.rsi._1w.rsiMin.percent, name: "Min", color: colors.stat.min, defaultActive: false, @@ -766,20 +772,20 @@ export function createMarketSection() { title: "RSI (1m)", bottom: [ line({ - metric: indicators.rsi._1m.rsi, + metric: technical.rsi._1m.rsi.percent, name: "RSI", color: colors.indicator.main, unit: Unit.index, }), line({ - metric: indicators.rsi._1m.rsiMax, + metric: technical.rsi._1m.rsiMax.percent, name: "Max", color: colors.stat.max, defaultActive: false, unit: Unit.index, }), line({ - metric: indicators.rsi._1m.rsiMin, + metric: technical.rsi._1m.rsiMin.percent, name: "Min", color: colors.stat.min, defaultActive: false, @@ -799,20 +805,20 @@ export function createMarketSection() { title: "RSI (1y)", bottom: [ line({ - metric: indicators.rsi._1y.rsi, + metric: technical.rsi._1y.rsi.percent, name: "RSI", color: colors.indicator.main, unit: Unit.index, }), line({ - metric: indicators.rsi._1y.rsiMax, + metric: technical.rsi._1y.rsiMax.percent, name: "Max", color: colors.stat.max, defaultActive: false, unit: Unit.index, }), line({ - metric: indicators.rsi._1y.rsiMin, + metric: technical.rsi._1y.rsiMin.percent, name: "Min", color: colors.stat.min, defaultActive: false, @@ -837,25 +843,25 @@ export function createMarketSection() { title: "Stochastic RSI Comparison", bottom: [ line({ - metric: indicators.rsi._1d.stochRsiK, + metric: technical.rsi._24h.stochRsiK.percent, name: "1d K", color: colors.time._24h, unit: Unit.index, }), line({ - metric: indicators.rsi._1w.stochRsiK, + metric: technical.rsi._1w.stochRsiK.percent, name: "1w K", color: colors.time._1w, unit: Unit.index, }), line({ - metric: indicators.rsi._1m.stochRsiK, + metric: technical.rsi._1m.stochRsiK.percent, name: "1m K", color: colors.time._1m, unit: Unit.index, }), line({ - metric: indicators.rsi._1y.stochRsiK, + metric: technical.rsi._1y.stochRsiK.percent, name: "1y K", color: colors.time._1y, unit: Unit.index, @@ -868,13 +874,13 @@ export function createMarketSection() { title: "Stochastic RSI (1d)", bottom: [ line({ - metric: indicators.rsi._1d.stochRsiK, + metric: technical.rsi._24h.stochRsiK.percent, name: "K", color: colors.indicator.fast, unit: Unit.index, }), line({ - metric: indicators.rsi._1d.stochRsiD, + metric: technical.rsi._24h.stochRsiD.percent, name: "D", color: colors.indicator.slow, unit: Unit.index, @@ -887,13 +893,13 @@ export function createMarketSection() { title: "Stochastic RSI (1w)", bottom: [ line({ - metric: indicators.rsi._1w.stochRsiK, + metric: technical.rsi._1w.stochRsiK.percent, name: "K", color: colors.indicator.fast, unit: Unit.index, }), line({ - metric: indicators.rsi._1w.stochRsiD, + metric: technical.rsi._1w.stochRsiD.percent, name: "D", color: colors.indicator.slow, unit: Unit.index, @@ -906,13 +912,13 @@ export function createMarketSection() { title: "Stochastic RSI (1m)", bottom: [ line({ - metric: indicators.rsi._1m.stochRsiK, + metric: technical.rsi._1m.stochRsiK.percent, name: "K", color: colors.indicator.fast, unit: Unit.index, }), line({ - metric: indicators.rsi._1m.stochRsiD, + metric: technical.rsi._1m.stochRsiD.percent, name: "D", color: colors.indicator.slow, unit: Unit.index, @@ -925,13 +931,13 @@ export function createMarketSection() { title: "Stochastic RSI (1y)", bottom: [ line({ - metric: indicators.rsi._1y.stochRsiK, + metric: technical.rsi._1y.stochRsiK.percent, name: "K", color: colors.indicator.fast, unit: Unit.index, }), line({ - metric: indicators.rsi._1y.stochRsiD, + metric: technical.rsi._1y.stochRsiD.percent, name: "D", color: colors.indicator.slow, unit: Unit.index, @@ -946,13 +952,13 @@ export function createMarketSection() { title: "Stochastic Oscillator", bottom: [ line({ - metric: indicators.stochK, + metric: technical.stochK.percent, name: "K", color: colors.indicator.fast, unit: Unit.index, }), line({ - metric: indicators.stochD, + metric: technical.stochD.percent, name: "D", color: colors.indicator.slow, unit: Unit.index, @@ -968,25 +974,25 @@ export function createMarketSection() { title: "MACD Comparison", bottom: [ line({ - metric: indicators.macd._1d.line, + metric: technical.macd._24h.line, name: "1d", color: colors.time._24h, unit: Unit.usd, }), line({ - metric: indicators.macd._1w.line, + metric: technical.macd._1w.line, name: "1w", color: colors.time._1w, unit: Unit.usd, }), line({ - metric: indicators.macd._1m.line, + metric: technical.macd._1m.line, name: "1m", color: colors.time._1m, unit: Unit.usd, }), line({ - metric: indicators.macd._1y.line, + metric: technical.macd._1y.line, name: "1y", color: colors.time._1y, unit: Unit.usd, @@ -998,19 +1004,19 @@ export function createMarketSection() { title: "MACD (1d)", bottom: [ line({ - metric: indicators.macd._1d.line, + metric: technical.macd._24h.line, name: "MACD", color: colors.indicator.fast, unit: Unit.usd, }), line({ - metric: indicators.macd._1d.signal, + metric: technical.macd._24h.signal, name: "Signal", color: colors.indicator.slow, unit: Unit.usd, }), histogram({ - metric: indicators.macd._1d.histogram, + metric: technical.macd._24h.histogram, name: "Histogram", unit: Unit.usd, }), @@ -1021,19 +1027,19 @@ export function createMarketSection() { title: "MACD (1w)", bottom: [ line({ - metric: indicators.macd._1w.line, + metric: technical.macd._1w.line, name: "MACD", color: colors.indicator.fast, unit: Unit.usd, }), line({ - metric: indicators.macd._1w.signal, + metric: technical.macd._1w.signal, name: "Signal", color: colors.indicator.slow, unit: Unit.usd, }), histogram({ - metric: indicators.macd._1w.histogram, + metric: technical.macd._1w.histogram, name: "Histogram", unit: Unit.usd, }), @@ -1044,19 +1050,19 @@ export function createMarketSection() { title: "MACD (1m)", bottom: [ line({ - metric: indicators.macd._1m.line, + metric: technical.macd._1m.line, name: "MACD", color: colors.indicator.fast, unit: Unit.usd, }), line({ - metric: indicators.macd._1m.signal, + metric: technical.macd._1m.signal, name: "Signal", color: colors.indicator.slow, unit: Unit.usd, }), histogram({ - metric: indicators.macd._1m.histogram, + metric: technical.macd._1m.histogram, name: "Histogram", unit: Unit.usd, }), @@ -1067,19 +1073,19 @@ export function createMarketSection() { title: "MACD (1y)", bottom: [ line({ - metric: indicators.macd._1y.line, + metric: technical.macd._1y.line, name: "MACD", color: colors.indicator.fast, unit: Unit.usd, }), line({ - metric: indicators.macd._1y.signal, + metric: technical.macd._1y.signal, name: "Signal", color: colors.indicator.slow, unit: Unit.usd, }), histogram({ - metric: indicators.macd._1y.histogram, + metric: technical.macd._1y.histogram, name: "Histogram", unit: Unit.usd, }), @@ -1115,7 +1121,7 @@ export function createMarketSection() { title: "Dollar Cost Average Sats/Day", bottom: [ line({ - metric: dca.dcaSatsPerDay, + metric: dca.satsPerDay, name: "Sats/Day", unit: Unit.sats, }), @@ -1130,19 +1136,19 @@ export function createMarketSection() { title: "Pi Cycle", top: [ price({ - metric: ma.price111dSma.price, + metric: ma.sma._111d, name: "111d SMA", color: colors.indicator.upper, }), price({ - metric: ma.price350dSmaX2, + metric: ma.sma._350d.x2, name: "350d SMA x2", color: colors.indicator.lower, }), ], bottom: [ baseline({ - metric: indicators.piCycle, + metric: technical.piCycle.ratio, name: "Pi Cycle", unit: Unit.ratio, base: 1, @@ -1154,7 +1160,7 @@ export function createMarketSection() { title: "Puell Multiple", bottom: [ line({ - metric: indicators.puellMultiple, + metric: indicators.puellMultiple.ratio, name: "Puell", color: colors.usd, unit: Unit.ratio, @@ -1166,7 +1172,7 @@ export function createMarketSection() { title: "NVT Ratio", bottom: [ line({ - metric: indicators.nvt, + metric: indicators.nvt.ratio, name: "NVT", color: colors.bitcoin, unit: Unit.ratio, @@ -1178,13 +1184,104 @@ export function createMarketSection() { title: "Gini Coefficient", bottom: [ line({ - metric: indicators.gini, + metric: indicators.gini.percent, name: "Gini", color: colors.loss, unit: Unit.ratio, }), ], }, + { + name: "RHODL Ratio", + title: "RHODL Ratio", + bottom: [ + line({ + metric: indicators.rhodlRatio.ratio, + name: "RHODL", + color: colors.bitcoin, + unit: Unit.ratio, + }), + ], + }, + { + name: "Thermocap Multiple", + title: "Thermocap Multiple", + bottom: [ + line({ + metric: indicators.thermocapMultiple.ratio, + name: "Thermocap", + color: colors.bitcoin, + unit: Unit.ratio, + }), + ], + }, + { + name: "Stock-to-Flow", + title: "Stock-to-Flow", + bottom: [ + line({ + metric: indicators.stockToFlow, + name: "S2F", + color: colors.bitcoin, + unit: Unit.ratio, + }), + ], + }, + { + name: "Dormancy", + title: "Dormancy", + bottom: [ + line({ + metric: indicators.dormancy.supplyAdjusted, + name: "Supply Adjusted", + color: colors.bitcoin, + unit: Unit.ratio, + }), + line({ + metric: indicators.dormancy.flow, + name: "Flow", + color: colors.usd, + unit: Unit.ratio, + defaultActive: false, + }), + ], + }, + { + name: "Seller Exhaustion", + title: "Seller Exhaustion Constant", + bottom: [ + line({ + metric: indicators.sellerExhaustionConstant, + name: "SEC", + color: colors.bitcoin, + unit: Unit.ratio, + }), + ], + }, + { + name: "CDD Supply Adjusted", + title: "Coindays Destroyed (Supply Adjusted)", + bottom: [ + line({ + metric: indicators.coindaysDestroyedSupplyAdjusted, + name: "CDD SA", + color: colors.bitcoin, + unit: Unit.ratio, + }), + ], + }, + { + name: "CYD Supply Adjusted", + title: "Coinyears Destroyed (Supply Adjusted)", + bottom: [ + line({ + metric: indicators.coinyearsDestroyedSupplyAdjusted, + name: "CYD SA", + color: colors.bitcoin, + unit: Unit.ratio, + }), + ], + }, ], }, ], diff --git a/website/scripts/options/mining.js b/website/scripts/options/mining.js index 93767c575..18fcf9f3d 100644 --- a/website/scripts/options/mining.js +++ b/website/scripts/options/mining.js @@ -56,20 +56,27 @@ export function createMiningSection() { const { blocks, pools, mining } = brk.metrics; // Pre-compute pool entries with resolved names - const poolData = entries(pools.vecs).map(([id, pool]) => ({ + const majorPoolData = entries(pools.major).map(([id, pool]) => ({ + id, + name: brk.POOL_ID_TO_POOL_NAME[id], + pool, + })); + const minorPoolData = entries(pools.minor).map(([id, pool]) => ({ id, name: brk.POOL_ID_TO_POOL_NAME[id], pool, })); - // Filtered pool groups for comparisons - const majorPools = poolData.filter((p) => includes(MAJOR_POOL_IDS, p.id)); - const antpoolFriends = poolData.filter((p) => + // Filtered pool groups for comparisons (major pools only have windowed dominance) + const featuredPools = majorPoolData.filter((p) => + includes(MAJOR_POOL_IDS, p.id), + ); + const antpoolFriends = majorPoolData.filter((p) => includes(ANTPOOL_AND_FRIENDS_IDS, p.id), ); // Build individual pool trees - const poolsTree = poolData.map(({ name, pool }) => ({ + const majorPoolsTree = majorPoolData.map(({ name, pool }) => ({ name, tree: [ { @@ -77,34 +84,34 @@ export function createMiningSection() { title: `Dominance: ${name}`, bottom: [ dots({ - metric: pool.dominance24h, + metric: pool.dominance._24h.percent, name: "24h", color: colors.time._24h, unit: Unit.percentage, defaultActive: false, }), line({ - metric: pool.dominance1w, + metric: pool.dominance._1w.percent, name: "1w", color: colors.time._1w, unit: Unit.percentage, defaultActive: false, }), line({ - metric: pool.dominance1m, + metric: pool.dominance._1m.percent, name: "1m", color: colors.time._1m, unit: Unit.percentage, }), line({ - metric: pool.dominance1y, + metric: pool.dominance._1y.percent, name: "1y", color: colors.time._1y, unit: Unit.percentage, defaultActive: false, }), line({ - metric: pool.dominance, + metric: pool.dominance.percent, name: "All Time", color: colors.time.all, unit: Unit.percentage, @@ -120,7 +127,7 @@ export function createMiningSection() { title: `Blocks Mined: ${name}`, bottom: [ line({ - metric: pool.blocksMined.height, + metric: pool.blocksMined.base, name: "base", unit: Unit.count, }), @@ -146,41 +153,69 @@ export function createMiningSection() { { name: "Sum", title: `Rewards: ${name}`, - bottom: revenueBtcSatsUsd({ - coinbase: pool.coinbase, - subsidy: pool.subsidy, - fee: pool.fee, + bottom: satsBtcUsdFrom({ + source: pool.rewards, key: "base", + name: "sum", }), }, { name: "Cumulative", title: `Rewards: ${name} (Total)`, - bottom: revenueBtcSatsUsd({ - coinbase: pool.coinbase, - subsidy: pool.subsidy, - fee: pool.fee, + bottom: satsBtcUsdFrom({ + source: pool.rewards, key: "cumulative", + name: "all-time", }), }, ], }, + ], + })); + + const minorPoolsTree = minorPoolData.map(({ name, pool }) => ({ + name, + tree: [ { - name: "Since Last Block", - title: `Since Last Block: ${name}`, + name: "Dominance", + title: `Dominance: ${name}`, bottom: [ line({ - metric: pool.blocksSinceBlock, - name: "Elapsed", - unit: Unit.blocks, - }), - line({ - metric: pool.daysSinceBlock, - name: "Elapsed", - unit: Unit.days, + metric: pool.dominance.percent, + name: "All Time", + color: colors.time.all, + unit: Unit.percentage, }), ], }, + { + name: "Blocks Mined", + tree: [ + { + name: "Base", + title: `Blocks Mined: ${name}`, + bottom: [ + line({ + metric: pool.blocksMined.base, + name: "base", + unit: Unit.count, + }), + ], + }, + rollingWindowsTree({ windows: pool.blocksMined.sum, title: `Blocks Mined: ${name}`, unit: Unit.count }), + { + name: "Cumulative", + title: `Blocks Mined: ${name} (Total)`, + bottom: [ + line({ + metric: pool.blocksMined.cumulative, + name: "all-time", + unit: Unit.count, + }), + ], + }, + ], + }, ], })); @@ -196,33 +231,33 @@ export function createMiningSection() { title: "Network Hashrate", bottom: [ dots({ - metric: mining.hashrate.hashRate, + metric: mining.hashrate.rate.base, name: "Hashrate", unit: Unit.hashRate, }), line({ - metric: mining.hashrate.hashRate1wSma, + metric: mining.hashrate.rate.sma._1w, name: "1w SMA", color: colors.time._1w, unit: Unit.hashRate, defaultActive: false, }), line({ - metric: mining.hashrate.hashRate1mSma, + metric: mining.hashrate.rate.sma._1m, name: "1m SMA", color: colors.time._1m, unit: Unit.hashRate, defaultActive: false, }), line({ - metric: mining.hashrate.hashRate2mSma, + metric: mining.hashrate.rate.sma._2m, name: "2m SMA", color: colors.indicator.main, unit: Unit.hashRate, defaultActive: false, }), line({ - metric: mining.hashrate.hashRate1ySma, + metric: mining.hashrate.rate.sma._1y, name: "1y SMA", color: colors.time._1y, unit: Unit.hashRate, @@ -235,7 +270,7 @@ export function createMiningSection() { unit: Unit.hashRate, }), line({ - metric: mining.hashrate.hashRateAth, + metric: mining.hashrate.rate.ath, name: "ATH", color: colors.loss, unit: Unit.hashRate, @@ -248,13 +283,13 @@ export function createMiningSection() { title: "Network Hashrate ATH", bottom: [ line({ - metric: mining.hashrate.hashRateAth, + metric: mining.hashrate.rate.ath, name: "ATH", color: colors.loss, unit: Unit.hashRate, }), dots({ - metric: mining.hashrate.hashRate, + metric: mining.hashrate.rate.base, name: "Hashrate", color: colors.bitcoin, unit: Unit.hashRate, @@ -266,7 +301,7 @@ export function createMiningSection() { title: "Network Hashrate Drawdown", bottom: [ line({ - metric: mining.hashrate.hashRateDrawdown, + metric: mining.hashrate.rate.drawdown.percent, name: "Drawdown", unit: Unit.percentage, color: colors.loss, @@ -307,7 +342,7 @@ export function createMiningSection() { title: "Difficulty Adjustment", bottom: [ baseline({ - metric: blocks.difficulty.adjustment, + metric: blocks.difficulty.adjustment.percent, name: "Change", unit: Unit.percentage, }), @@ -318,12 +353,12 @@ export function createMiningSection() { title: "Next Difficulty Adjustment", bottom: [ line({ - metric: blocks.difficulty.blocksBeforeNextAdjustment, + metric: blocks.difficulty.blocksBeforeNext, name: "Remaining", unit: Unit.blocks, }), line({ - metric: blocks.difficulty.daysBeforeNextAdjustment, + metric: blocks.difficulty.daysBeforeNext, name: "Remaining", unit: Unit.days, }), @@ -381,22 +416,22 @@ export function createMiningSection() { title: "Coinbase Rolling Sum", bottom: [ ...satsBtcUsd({ - pattern: mining.rewards.coinbase._24h.sum, + pattern: mining.rewards.coinbase.sum._24h, name: "24h", color: colors.time._24h, }), ...satsBtcUsd({ - pattern: mining.rewards.coinbase._7d.sum, + pattern: mining.rewards.coinbase.sum._1w, name: "7d", color: colors.time._1w, }), ...satsBtcUsd({ - pattern: mining.rewards.coinbase._30d.sum, + pattern: mining.rewards.coinbase.sum._1m, name: "30d", color: colors.time._1m, }), ...satsBtcUsd({ - pattern: mining.rewards.coinbase._1y.sum, + pattern: mining.rewards.coinbase.sum._1y, name: "1y", color: colors.time._1y, }), @@ -406,7 +441,7 @@ export function createMiningSection() { name: "24h", title: "Coinbase 24h Rolling Sum", bottom: satsBtcUsd({ - pattern: mining.rewards.coinbase._24h.sum, + pattern: mining.rewards.coinbase.sum._24h, name: "24h", color: colors.time._24h, }), @@ -415,7 +450,7 @@ export function createMiningSection() { name: "7d", title: "Coinbase 7d Rolling Sum", bottom: satsBtcUsd({ - pattern: mining.rewards.coinbase._7d.sum, + pattern: mining.rewards.coinbase.sum._1w, name: "7d", color: colors.time._1w, }), @@ -424,7 +459,7 @@ export function createMiningSection() { name: "30d", title: "Coinbase 30d Rolling Sum", bottom: satsBtcUsd({ - pattern: mining.rewards.coinbase._30d.sum, + pattern: mining.rewards.coinbase.sum._1m, name: "30d", color: colors.time._1m, }), @@ -433,18 +468,13 @@ export function createMiningSection() { name: "1y", title: "Coinbase 1y Rolling Sum", bottom: satsBtcUsd({ - pattern: mining.rewards.coinbase._1y.sum, + pattern: mining.rewards.coinbase.sum._1y, name: "1y", color: colors.time._1y, }), }, ], }, - { - name: "Distribution", - title: "Coinbase Rewards per Block Distribution", - bottom: distributionBtcSatsUsd(mining.rewards.coinbase._24h), - }, { name: "Cumulative", title: "Coinbase Rewards (Total)", @@ -469,7 +499,7 @@ export function createMiningSection() { name: "sum", }), line({ - metric: mining.rewards.subsidyUsd1ySma, + metric: mining.rewards.subsidy.sma1y.usd, name: "1y SMA", color: colors.time._1y, unit: Unit.usd, @@ -477,78 +507,6 @@ export function createMiningSection() { }), ], }, - { - name: "Rolling", - tree: [ - { - name: "Compare", - title: "Subsidy Rolling Sum", - bottom: [ - ...satsBtcUsd({ - pattern: mining.rewards.subsidy._24h.sum, - name: "24h", - color: colors.time._24h, - }), - ...satsBtcUsd({ - pattern: mining.rewards.subsidy._7d.sum, - name: "7d", - color: colors.time._1w, - }), - ...satsBtcUsd({ - pattern: mining.rewards.subsidy._30d.sum, - name: "30d", - color: colors.time._1m, - }), - ...satsBtcUsd({ - pattern: mining.rewards.subsidy._1y.sum, - name: "1y", - color: colors.time._1y, - }), - ], - }, - { - name: "24h", - title: "Subsidy 24h Rolling Sum", - bottom: satsBtcUsd({ - pattern: mining.rewards.subsidy._24h.sum, - name: "24h", - color: colors.time._24h, - }), - }, - { - name: "7d", - title: "Subsidy 7d Rolling Sum", - bottom: satsBtcUsd({ - pattern: mining.rewards.subsidy._7d.sum, - name: "7d", - color: colors.time._1w, - }), - }, - { - name: "30d", - title: "Subsidy 30d Rolling Sum", - bottom: satsBtcUsd({ - pattern: mining.rewards.subsidy._30d.sum, - name: "30d", - color: colors.time._1m, - }), - }, - { - name: "1y", - title: "Subsidy 1y Rolling Sum", - bottom: satsBtcUsd({ - pattern: mining.rewards.subsidy._1y.sum, - name: "1y", - color: colors.time._1y, - }), - }, - ], - }, - { - name: "Distribution", - title: "Block Subsidy Distribution", - bottom: distributionBtcSatsUsd(mining.rewards.subsidy._24h), - }, { name: "Cumulative", title: "Block Subsidy (Total)", @@ -580,22 +538,22 @@ export function createMiningSection() { title: "Fee Rolling Sum", bottom: [ ...satsBtcUsd({ - pattern: mining.rewards.fees._24h.sum, + pattern: mining.rewards.fees.sum._24h, name: "24h", color: colors.time._24h, }), ...satsBtcUsd({ - pattern: mining.rewards.fees._7d.sum, + pattern: mining.rewards.fees.sum._1w, name: "7d", color: colors.time._1w, }), ...satsBtcUsd({ - pattern: mining.rewards.fees._30d.sum, + pattern: mining.rewards.fees.sum._1m, name: "30d", color: colors.time._1m, }), ...satsBtcUsd({ - pattern: mining.rewards.fees._1y.sum, + pattern: mining.rewards.fees.sum._1y, name: "1y", color: colors.time._1y, }), @@ -605,7 +563,7 @@ export function createMiningSection() { name: "24h", title: "Fee 24h Rolling Sum", bottom: satsBtcUsd({ - pattern: mining.rewards.fees._24h.sum, + pattern: mining.rewards.fees.sum._24h, name: "24h", color: colors.time._24h, }), @@ -614,7 +572,7 @@ export function createMiningSection() { name: "7d", title: "Fee 7d Rolling Sum", bottom: satsBtcUsd({ - pattern: mining.rewards.fees._7d.sum, + pattern: mining.rewards.fees.sum._1w, name: "7d", color: colors.time._1w, }), @@ -623,7 +581,7 @@ export function createMiningSection() { name: "30d", title: "Fee 30d Rolling Sum", bottom: satsBtcUsd({ - pattern: mining.rewards.fees._30d.sum, + pattern: mining.rewards.fees.sum._1m, name: "30d", color: colors.time._1m, }), @@ -632,7 +590,7 @@ export function createMiningSection() { name: "1y", title: "Fee 1y Rolling Sum", bottom: satsBtcUsd({ - pattern: mining.rewards.fees._1y.sum, + pattern: mining.rewards.fees.sum._1y, name: "1y", color: colors.time._1y, }), @@ -666,31 +624,31 @@ export function createMiningSection() { title: "Subsidy Dominance", bottom: [ line({ - metric: mining.rewards.subsidyDominance, + metric: mining.rewards.subsidy.dominance.percent, name: "All-time", color: colors.time.all, unit: Unit.percentage, }), line({ - metric: mining.rewards.subsidyDominance24h, + metric: mining.rewards.subsidy.dominance._24h.percent, name: "24h", color: colors.time._24h, unit: Unit.percentage, }), line({ - metric: mining.rewards.subsidyDominance7d, + metric: mining.rewards.subsidy.dominance._1w.percent, name: "7d", color: colors.time._1w, unit: Unit.percentage, }), line({ - metric: mining.rewards.subsidyDominance30d, + metric: mining.rewards.subsidy.dominance._1m.percent, name: "30d", color: colors.time._1m, unit: Unit.percentage, }), line({ - metric: mining.rewards.subsidyDominance1y, + metric: mining.rewards.subsidy.dominance._1y.percent, name: "1y", color: colors.time._1y, unit: Unit.percentage, @@ -702,31 +660,31 @@ export function createMiningSection() { title: "Fee Dominance", bottom: [ line({ - metric: mining.rewards.feeDominance, + metric: mining.rewards.fees.dominance.percent, name: "All-time", color: colors.time.all, unit: Unit.percentage, }), line({ - metric: mining.rewards.feeDominance24h, + metric: mining.rewards.fees.dominance._24h.percent, name: "24h", color: colors.time._24h, unit: Unit.percentage, }), line({ - metric: mining.rewards.feeDominance7d, + metric: mining.rewards.fees.dominance._1w.percent, name: "7d", color: colors.time._1w, unit: Unit.percentage, }), line({ - metric: mining.rewards.feeDominance30d, + metric: mining.rewards.fees.dominance._1m.percent, name: "30d", color: colors.time._1m, unit: Unit.percentage, }), line({ - metric: mining.rewards.feeDominance1y, + metric: mining.rewards.fees.dominance._1y.percent, name: "1y", color: colors.time._1y, unit: Unit.percentage, @@ -740,13 +698,13 @@ export function createMiningSection() { title: "Revenue Dominance (All-time)", bottom: [ line({ - metric: mining.rewards.subsidyDominance, + metric: mining.rewards.subsidy.dominance.percent, name: "Subsidy", color: colors.mining.subsidy, unit: Unit.percentage, }), line({ - metric: mining.rewards.feeDominance, + metric: mining.rewards.fees.dominance.percent, name: "Fees", color: colors.mining.fee, unit: Unit.percentage, @@ -758,13 +716,13 @@ export function createMiningSection() { title: "Revenue Dominance (24h)", bottom: [ line({ - metric: mining.rewards.subsidyDominance24h, + metric: mining.rewards.subsidy.dominance._24h.percent, name: "Subsidy", color: colors.mining.subsidy, unit: Unit.percentage, }), line({ - metric: mining.rewards.feeDominance24h, + metric: mining.rewards.fees.dominance._24h.percent, name: "Fees", color: colors.mining.fee, unit: Unit.percentage, @@ -776,13 +734,13 @@ export function createMiningSection() { title: "Revenue Dominance (7d)", bottom: [ line({ - metric: mining.rewards.subsidyDominance7d, + metric: mining.rewards.subsidy.dominance._1w.percent, name: "Subsidy", color: colors.mining.subsidy, unit: Unit.percentage, }), line({ - metric: mining.rewards.feeDominance7d, + metric: mining.rewards.fees.dominance._1w.percent, name: "Fees", color: colors.mining.fee, unit: Unit.percentage, @@ -794,13 +752,13 @@ export function createMiningSection() { title: "Revenue Dominance (30d)", bottom: [ line({ - metric: mining.rewards.subsidyDominance30d, + metric: mining.rewards.subsidy.dominance._1m.percent, name: "Subsidy", color: colors.mining.subsidy, unit: Unit.percentage, }), line({ - metric: mining.rewards.feeDominance30d, + metric: mining.rewards.fees.dominance._1m.percent, name: "Fees", color: colors.mining.fee, unit: Unit.percentage, @@ -812,13 +770,13 @@ export function createMiningSection() { title: "Revenue Dominance (1y)", bottom: [ line({ - metric: mining.rewards.subsidyDominance1y, + metric: mining.rewards.subsidy.dominance._1y.percent, name: "Subsidy", color: colors.mining.subsidy, unit: Unit.percentage, }), line({ - metric: mining.rewards.feeDominance1y, + metric: mining.rewards.fees.dominance._1y.percent, name: "Fees", color: colors.mining.fee, unit: Unit.percentage, @@ -834,7 +792,7 @@ export function createMiningSection() { name: "Sum", title: "Unclaimed Rewards", bottom: satsBtcUsdFrom({ - source: mining.rewards.unclaimedRewards, + source: mining.rewards.unclaimed, key: "base", name: "sum", }), @@ -843,7 +801,7 @@ export function createMiningSection() { name: "Cumulative", title: "Unclaimed Rewards (Total)", bottom: satsBtcUsdFrom({ - source: mining.rewards.unclaimedRewards, + source: mining.rewards.unclaimed, key: "cumulative", name: "all-time", }), @@ -862,25 +820,25 @@ export function createMiningSection() { title: "Hash Price", bottom: [ line({ - metric: mining.hashrate.hashPriceThs, + metric: mining.hashrate.price.ths, name: "TH/s", color: colors.usd, unit: Unit.usdPerThsPerDay, }), line({ - metric: mining.hashrate.hashPricePhs, + metric: mining.hashrate.price.phs, name: "PH/s", color: colors.usd, unit: Unit.usdPerPhsPerDay, }), dotted({ - metric: mining.hashrate.hashPriceThsMin, + metric: mining.hashrate.price.thsMin, name: "TH/s Min", color: colors.stat.min, unit: Unit.usdPerThsPerDay, }), dotted({ - metric: mining.hashrate.hashPricePhsMin, + metric: mining.hashrate.price.phsMin, name: "PH/s Min", color: colors.stat.min, unit: Unit.usdPerPhsPerDay, @@ -892,25 +850,25 @@ export function createMiningSection() { title: "Hash Value", bottom: [ line({ - metric: mining.hashrate.hashValueThs, + metric: mining.hashrate.value.ths, name: "TH/s", color: colors.bitcoin, unit: Unit.satsPerThsPerDay, }), line({ - metric: mining.hashrate.hashValuePhs, + metric: mining.hashrate.value.phs, name: "PH/s", color: colors.bitcoin, unit: Unit.satsPerPhsPerDay, }), dotted({ - metric: mining.hashrate.hashValueThsMin, + metric: mining.hashrate.value.thsMin, name: "TH/s Min", color: colors.stat.min, unit: Unit.satsPerThsPerDay, }), dotted({ - metric: mining.hashrate.hashValuePhsMin, + metric: mining.hashrate.value.phsMin, name: "PH/s Min", color: colors.stat.min, unit: Unit.satsPerPhsPerDay, @@ -922,13 +880,13 @@ export function createMiningSection() { title: "Recovery", bottom: [ line({ - metric: mining.hashrate.hashPriceRebound, + metric: mining.hashrate.price.rebound.percent, name: "Hash Price", color: colors.usd, unit: Unit.percentage, }), line({ - metric: mining.hashrate.hashValueRebound, + metric: mining.hashrate.value.rebound.percent, name: "Hash Value", color: colors.bitcoin, unit: Unit.percentage, @@ -947,12 +905,12 @@ export function createMiningSection() { title: "Next Halving", bottom: [ line({ - metric: blocks.halving.blocksBeforeNextHalving, + metric: blocks.halving.blocksBeforeNext, name: "Remaining", unit: Unit.blocks, }), line({ - metric: blocks.halving.daysBeforeNextHalving, + metric: blocks.halving.daysBeforeNext, name: "Remaining", unit: Unit.days, }), @@ -983,11 +941,11 @@ export function createMiningSection() { { name: "Dominance", title: "Dominance: Major Pools (1m)", - bottom: majorPools.map((p, i) => + bottom: featuredPools.map((p, i) => line({ - metric: p.pool.dominance1m, + metric: p.pool.dominance._1m.percent, name: p.name, - color: colors.at(i, majorPools.length), + color: colors.at(i, featuredPools.length), unit: Unit.percentage, }), ), @@ -995,11 +953,11 @@ export function createMiningSection() { { name: "Blocks Mined", title: "Blocks Mined: Major Pools (1m)", - bottom: majorPools.map((p, i) => + bottom: featuredPools.map((p, i) => line({ - metric: p.pool.blocksMined1mSum, + metric: p.pool.blocksMined.sum._1m, name: p.name, - color: colors.at(i, majorPools.length), + color: colors.at(i, featuredPools.length), unit: Unit.count, }), ), @@ -1007,12 +965,12 @@ export function createMiningSection() { { name: "Total Rewards", title: "Total Rewards: Major Pools", - bottom: majorPools.flatMap((p, i) => + bottom: featuredPools.flatMap((p, i) => satsBtcUsdFrom({ - source: p.pool.coinbase, + source: p.pool.rewards, key: "base", name: p.name, - color: colors.at(i, majorPools.length), + color: colors.at(i, featuredPools.length), }), ), }, @@ -1027,7 +985,7 @@ export function createMiningSection() { title: "Dominance: AntPool & Friends (1m)", bottom: antpoolFriends.map((p, i) => line({ - metric: p.pool.dominance1m, + metric: p.pool.dominance._1m.percent, name: p.name, color: colors.at(i, antpoolFriends.length), unit: Unit.percentage, @@ -1039,7 +997,7 @@ export function createMiningSection() { title: "Blocks Mined: AntPool & Friends (1m)", bottom: antpoolFriends.map((p, i) => line({ - metric: p.pool.blocksMined1mSum, + metric: p.pool.blocksMined.sum._1m, name: p.name, color: colors.at(i, antpoolFriends.length), unit: Unit.count, @@ -1051,7 +1009,7 @@ export function createMiningSection() { title: "Total Rewards: AntPool & Friends", bottom: antpoolFriends.flatMap((p, i) => satsBtcUsdFrom({ - source: p.pool.coinbase, + source: p.pool.rewards, key: "base", name: p.name, color: colors.at(i, antpoolFriends.length), @@ -1063,7 +1021,7 @@ export function createMiningSection() { // All pools { name: "All Pools", - tree: poolsTree, + tree: [...majorPoolsTree, ...minorPoolsTree], }, ], }, diff --git a/website/scripts/options/network.js b/website/scripts/options/network.js index e9e4db241..7b6d1ab94 100644 --- a/website/scripts/options/network.js +++ b/website/scripts/options/network.js @@ -10,14 +10,14 @@ import { dots, baseline, fromSupplyPattern, - fromBaseStatsPattern, chartsFromFullPerBlock, chartsFromCount, - chartsFromValueFull, fromStatsPattern, chartsFromSumPerBlock, - statsAtWindow, rollingWindowsTree, + distributionWindowsTree, + mapWindows, + ROLLING_WINDOWS, } from "./series.js"; import { satsBtcUsd, satsBtcUsdFrom } from "./shared.js"; @@ -33,7 +33,8 @@ export function createNetworkSection() { outputs, scripts, supply, - distribution, + addresses, + cohorts, } = brk.metrics; const st = colors.scriptType; @@ -60,13 +61,13 @@ export function createNetworkSection() { defaultActive: false, }, { - key: "emptyoutput", + key: "emptyOutput", name: "Empty", color: st.empty, defaultActive: false, }, { - key: "unknownoutput", + key: "unknownOutput", name: "Unknown", color: st.unknown, defaultActive: false, @@ -114,42 +115,25 @@ export function createNetworkSection() { }, ]); - // Balance change types - const balanceTypes = /** @type {const} */ ([ - { - key: "balanceIncreased", - name: "Accumulating", - title: "Accumulating Addresses per Block", - compareTitle: "Accumulating Addresses per Block by Type", - }, - { - key: "balanceDecreased", - name: "Distributing", - title: "Distributing Addresses per Block", - compareTitle: "Distributing Addresses per Block by Type", - }, - ]); - // Count types for comparison charts - // addrCount and emptyAddrCount have .count, totalAddrCount doesn't const countTypes = /** @type {const} */ ([ { name: "Funded", title: "Address Count by Type", /** @param {AddressableType} t */ - getMetric: (t) => distribution.addrCount[t].count, + getMetric: (t) => addresses.funded[t], }, { name: "Empty", title: "Empty Address Count by Type", /** @param {AddressableType} t */ - getMetric: (t) => distribution.emptyAddrCount[t].count, + getMetric: (t) => addresses.empty[t], }, { name: "Total", title: "Total Address Count by Type", /** @param {AddressableType} t */ - getMetric: (t) => distribution.totalAddrCount[t], + getMetric: (t) => addresses.total[t], }, ]); @@ -164,19 +148,19 @@ export function createNetworkSection() { title: `${titlePrefix}Address Count`, bottom: [ line({ - metric: distribution.addrCount[key].count, + metric: addresses.funded[key], name: "Funded", unit: Unit.count, }), line({ - metric: distribution.emptyAddrCount[key].count, + metric: addresses.empty[key], name: "Empty", color: colors.gray, unit: Unit.count, defaultActive: false, }), line({ - metric: distribution.totalAddrCount[key], + metric: addresses.total[key], name: "Total", color: colors.default, unit: Unit.count, @@ -187,35 +171,23 @@ export function createNetworkSection() { { name: "Trends", tree: [ - { - name: "30d Change", - title: `${titlePrefix}Address Count 30d Change`, - bottom: [ - baseline({ - metric: distribution.addrCount[key]._30dChange, - name: "Funded", - unit: Unit.count, - }), - baseline({ - metric: distribution.emptyAddrCount[key]._30dChange, - name: "Empty", - color: colors.gray, - unit: Unit.count, - defaultActive: false, - }), - ], - }, + rollingWindowsTree({ + windows: addresses.delta[key].change, + title: `${titlePrefix}Address Count Change`, + unit: Unit.count, + series: baseline, + }), { name: "New", tree: (() => { - const p = distribution.newAddrCount[key]; + const p = addresses.new[key]; const t = `${titlePrefix}New Address Count`; return [ { name: "Sum", title: t, bottom: [ - line({ metric: p.height, name: "base", unit: Unit.count }), + line({ metric: p.base, name: "base", unit: Unit.count }), ], }, rollingWindowsTree({ @@ -239,46 +211,66 @@ export function createNetworkSection() { }, { name: "Reactivated", - title: `${titlePrefix}Reactivated Addresses per Block`, - bottom: fromBaseStatsPattern({ - pattern: distribution.addressActivity[key].reactivated, - window: "_24h", - unit: Unit.count, - }), - }, - { - name: "Growth Rate", - title: `${titlePrefix}Address Growth Rate per Block`, - bottom: fromBaseStatsPattern({ - pattern: distribution.growthRate[key], - window: "_24h", - unit: Unit.ratio, - }), + tree: [ + { + name: "Base", + title: `${titlePrefix}Reactivated Addresses per Block`, + bottom: [ + dots({ + metric: addresses.activity[key].reactivated.height, + name: "base", + unit: Unit.count, + }), + line({ + metric: addresses.activity[key].reactivated._24h, + name: "24h avg", + color: colors.stat.avg, + unit: Unit.count, + }), + ], + }, + rollingWindowsTree({ + windows: addresses.activity[key].reactivated, + title: `${titlePrefix}Reactivated Addresses`, + unit: Unit.count, + }), + ], }, + rollingWindowsTree({ + windows: mapWindows(addresses.delta[key].rate, (r) => r.ratio), + title: `${titlePrefix}Address Growth Rate`, + unit: Unit.ratio, + }), ], }, { name: "Transacting", tree: transactingTypes.map((t) => ({ name: t.name, - title: `${titlePrefix}${t.title}`, - bottom: fromBaseStatsPattern({ - pattern: distribution.addressActivity[key][t.key], - window: "_24h", - unit: Unit.count, - }), - })), - }, - { - name: "Balance", - tree: balanceTypes.map((b) => ({ - name: b.name, - title: `${titlePrefix}${b.title}`, - bottom: fromBaseStatsPattern({ - pattern: distribution.addressActivity[key][b.key], - window: "_24h", - unit: Unit.count, - }), + tree: [ + { + name: "Base", + title: `${titlePrefix}${t.title}`, + bottom: [ + dots({ + metric: addresses.activity[key][t.key].height, + name: "base", + unit: Unit.count, + }), + line({ + metric: addresses.activity[key][t.key]._24h, + name: "24h avg", + color: colors.stat.avg, + unit: Unit.count, + }), + ], + }, + rollingWindowsTree({ + windows: addresses.activity[key][t.key], + title: `${titlePrefix}${t.title.replace(" per Block", "")}`, + unit: Unit.count, + }), + ], })), }, ]; @@ -312,13 +304,13 @@ export function createNetworkSection() { title: `${groupName} New Address Count`, bottom: types.flatMap((t) => [ line({ - metric: distribution.newAddrCount[t.key].height, + metric: addresses.new[t.key].base, name: t.name, color: t.color, unit: Unit.count, }), line({ - metric: distribution.newAddrCount[t.key].sum._24h, + metric: addresses.new[t.key].sum._24h, name: t.name, color: t.color, unit: Unit.count, @@ -327,81 +319,78 @@ export function createNetworkSection() { }, { name: "Reactivated", - title: `${groupName} Reactivated Addresses per Block`, - bottom: types.flatMap((t) => [ - line({ - metric: distribution.addressActivity[t.key].reactivated.height, - name: t.name, - color: t.color, - unit: Unit.count, - }), - line({ - metric: - distribution.addressActivity[t.key].reactivated.average._24h, - name: t.name, - color: t.color, - unit: Unit.count, - }), - ]), + tree: [ + { + name: "Base", + title: `${groupName} Reactivated Addresses per Block`, + bottom: types.map((t) => + line({ + metric: addresses.activity[t.key].reactivated.height, + name: t.name, + color: t.color, + unit: Unit.count, + }), + ), + }, + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: `${groupName} Reactivated Addresses (${w.name})`, + bottom: types.map((t) => + line({ + metric: addresses.activity[t.key].reactivated[w.key], + name: t.name, + color: t.color, + unit: Unit.count, + }), + ), + })), + ], }, { name: "Growth Rate", - title: `${groupName} Address Growth Rate per Block`, - bottom: types.flatMap((t) => [ - dots({ - metric: distribution.growthRate[t.key].height, - name: t.name, - color: t.color, - unit: Unit.ratio, - }), - dots({ - metric: distribution.growthRate[t.key].average._24h, - name: t.name, - color: t.color, - unit: Unit.ratio, - }), - ]), + tree: ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: `${groupName} Address Growth Rate (${w.name})`, + bottom: types.map((t) => + line({ + metric: addresses.delta[t.key].rate[w.key].ratio, + name: t.name, + color: t.color, + unit: Unit.ratio, + }), + ), + })), }, { name: "Transacting", tree: transactingTypes.map((tr) => ({ name: tr.name, - title: `${groupName} ${tr.compareTitle}`, - bottom: types.flatMap((t) => [ - line({ - metric: distribution.addressActivity[t.key][tr.key].height, - name: t.name, - color: t.color, - unit: Unit.count, - }), - line({ - metric: distribution.addressActivity[t.key][tr.key].average._24h, - name: t.name, - color: t.color, - unit: Unit.count, - }), - ]), - })), - }, - { - name: "Balance", - tree: balanceTypes.map((b) => ({ - name: b.name, - title: `${groupName} ${b.compareTitle}`, - bottom: types.flatMap((t) => [ - line({ - metric: distribution.addressActivity[t.key][b.key].height, - name: t.name, - color: t.color, - unit: Unit.count, - }), - line({ - metric: distribution.addressActivity[t.key][b.key].average._24h, - name: t.name, - color: t.color, - unit: Unit.count, - }), - ]), + tree: [ + { + name: "Base", + title: `${groupName} ${tr.compareTitle}`, + bottom: types.map((t) => + line({ + metric: addresses.activity[t.key][tr.key].height, + name: t.name, + color: t.color, + unit: Unit.count, + }), + ), + }, + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: `${groupName} ${tr.compareTitle} (${w.name})`, + bottom: types.map((t) => + line({ + metric: addresses.activity[t.key][tr.key][w.key], + name: t.name, + color: t.color, + unit: Unit.count, + }), + ), + })), + ], })), }, ], @@ -478,7 +467,7 @@ export function createNetworkSection() { title: "Inflation Rate", bottom: [ dots({ - metric: supply.inflation, + metric: supply.inflationRate.percent, name: "Rate", unit: Unit.percentage, }), @@ -509,10 +498,18 @@ export function createNetworkSection() { }, { name: "OP_RETURN", - tree: chartsFromValueFull({ - pattern: scripts.value.opreturn, - title: "OP_RETURN Burned", - }), + tree: [ + { + name: "Sum", + title: "OP_RETURN Burned", + bottom: satsBtcUsd({ pattern: scripts.value.opreturn.base, name: "sum" }), + }, + { + name: "Cumulative", + title: "OP_RETURN Burned (Total)", + bottom: satsBtcUsd({ pattern: scripts.value.opreturn.cumulative, name: "all-time" }), + }, + ], }, ], }, @@ -524,7 +521,7 @@ export function createNetworkSection() { { name: "Count", tree: chartsFromFullPerBlock({ - pattern: transactions.count.txCount, + pattern: transactions.count.total, title: "Transaction Count", unit: Unit.count, }), @@ -545,11 +542,11 @@ export function createNetworkSection() { title: "Transaction Volume", bottom: [ ...satsBtcUsd({ - pattern: transactions.volume.sentSum, + pattern: transactions.volume.sentSum.base, name: "Sent", }), ...satsBtcUsd({ - pattern: transactions.volume.receivedSum, + pattern: transactions.volume.receivedSum.base, name: "Received", color: colors.entity.output, }), @@ -559,7 +556,7 @@ export function createNetworkSection() { name: "Annualized", title: "Annualized Transaction Volume", bottom: satsBtcUsd({ - pattern: transactions.volume.annualizedVolume, + pattern: transactions.volume.sentSum.sum._1y, name: "Annualized", }), }, @@ -588,7 +585,7 @@ export function createNetworkSection() { bottom: entries(transactions.versions).map( ([v, data], i, arr) => line({ - metric: data.height, + metric: data.base, name: v, color: colors.at(i, arr.length), unit: Unit.count, @@ -642,12 +639,12 @@ export function createNetworkSection() { title: "Block Count", bottom: [ line({ - metric: blocks.count.blockCount.height, + metric: blocks.count.total.base, name: "base", unit: Unit.count, }), line({ - metric: blocks.count.blockCountTarget, + metric: blocks.count.target, name: "Target", color: colors.gray, unit: Unit.count, @@ -656,7 +653,7 @@ export function createNetworkSection() { ], }, rollingWindowsTree({ - windows: blocks.count.blockCountSum, + windows: blocks.count.total.sum, title: "Block Count", unit: Unit.count, }), @@ -665,7 +662,7 @@ export function createNetworkSection() { title: "Block Count (Total)", bottom: [ line({ - metric: blocks.count.blockCount.cumulative, + metric: blocks.count.total.cumulative, name: "all-time", unit: Unit.count, }), @@ -675,14 +672,30 @@ export function createNetworkSection() { }, { name: "Interval", - title: "Block Interval", - bottom: [ - ...fromBaseStatsPattern({ - pattern: blocks.interval, - window: "_24h", + tree: [ + { + name: "Base", + title: "Block Interval", + bottom: [ + dots({ + metric: blocks.interval.height, + name: "base", + unit: Unit.secs, + }), + line({ + metric: blocks.interval._24h, + name: "24h avg", + color: colors.stat.avg, + unit: Unit.secs, + }), + priceLine({ unit: Unit.secs, name: "Target", number: 600 }), + ], + }, + rollingWindowsTree({ + windows: blocks.interval, + title: "Block Interval", unit: Unit.secs, }), - priceLine({ unit: Unit.secs, name: "Target", number: 600 }), ], }, { @@ -693,7 +706,7 @@ export function createNetworkSection() { title: "Block Size", bottom: [ line({ - metric: blocks.totalSize, + metric: blocks.size.total, name: "base", unit: Unit.bytes, }), @@ -704,21 +717,12 @@ export function createNetworkSection() { title: "Block Size", unit: Unit.bytes, }), - { - name: "Distribution", - title: "Block Size Distribution", - bottom: [ - line({ - metric: blocks.totalSize, - name: "base", - unit: Unit.bytes, - }), - ...fromStatsPattern({ - pattern: statsAtWindow(blocks.size, "_24h"), - unit: Unit.bytes, - }), - ], - }, + distributionWindowsTree({ + pattern: blocks.size, + base: blocks.size.total, + title: "Block Size", + unit: Unit.bytes, + }), { name: "Cumulative", title: "Block Size (Total)", @@ -740,7 +744,7 @@ export function createNetworkSection() { title: "Block Weight", bottom: [ line({ - metric: blocks.weight.base, + metric: blocks.weight.raw, name: "base", unit: Unit.wu, }), @@ -751,21 +755,12 @@ export function createNetworkSection() { title: "Block Weight", unit: Unit.wu, }), - { - name: "Distribution", - title: "Block Weight Distribution", - bottom: [ - line({ - metric: blocks.weight.base, - name: "base", - unit: Unit.wu, - }), - ...fromStatsPattern({ - pattern: statsAtWindow(blocks.weight, "_24h"), - unit: Unit.wu, - }), - ], - }, + distributionWindowsTree({ + pattern: blocks.weight, + base: blocks.weight.raw, + title: "Block Weight", + unit: Unit.wu, + }), { name: "Cumulative", title: "Block Weight (Total)", @@ -787,7 +782,7 @@ export function createNetworkSection() { title: "Block vBytes", bottom: [ line({ - metric: blocks.vbytes.height, + metric: blocks.vbytes.base, name: "base", unit: Unit.vb, }), @@ -798,21 +793,12 @@ export function createNetworkSection() { title: "Block vBytes", unit: Unit.vb, }), - { - name: "Distribution", - title: "Block vBytes Distribution", - bottom: [ - line({ - metric: blocks.vbytes.height, - name: "base", - unit: Unit.vb, - }), - ...fromStatsPattern({ - pattern: statsAtWindow(blocks.vbytes, "_24h"), - unit: Unit.vb, - }), - ], - }, + distributionWindowsTree({ + pattern: blocks.vbytes, + base: blocks.vbytes.base, + title: "Block vBytes", + unit: Unit.vb, + }), { name: "Cumulative", title: "Block vBytes (Total)", @@ -829,11 +815,13 @@ export function createNetworkSection() { { name: "Fullness", title: "Block Fullness", - bottom: fromBaseStatsPattern({ - pattern: blocks.fullness, - window: "_24h", - unit: Unit.percentage, - }), + bottom: [ + dots({ + metric: blocks.fullness.percent, + name: "base", + unit: Unit.percentage, + }), + ], }, ], }, @@ -847,7 +835,7 @@ export function createNetworkSection() { title: "UTXO Count", bottom: [ line({ - metric: outputs.count.utxoCount, + metric: outputs.count.unspent, name: "Count", unit: Unit.count, }), @@ -858,7 +846,7 @@ export function createNetworkSection() { title: "UTXO Count 30d Change", bottom: [ baseline({ - metric: distribution.utxoCohorts.all.outputs.utxoCount30dChange, + metric: cohorts.utxo.all.outputs.unspentCount.delta.change._1m, name: "30d Change", unit: Unit.count, }), @@ -869,7 +857,7 @@ export function createNetworkSection() { title: "UTXO Flow", bottom: [ line({ - metric: outputs.count.totalCount.sum, + metric: outputs.count.total.sum, name: "Created", color: colors.entity.output, unit: Unit.count, @@ -895,7 +883,7 @@ export function createNetworkSection() { { name: "Outputs", tree: chartsFromSumPerBlock({ - pattern: outputs.count.totalCount, + pattern: outputs.count.total, title: "Output Count", unit: Unit.count, }), @@ -957,14 +945,14 @@ export function createNetworkSection() { title: "New Address Count by Type", bottom: addressTypes.flatMap((t) => [ line({ - metric: distribution.newAddrCount[t.key].height, + metric: addresses.new[t.key].base, name: t.name, color: t.color, unit: Unit.count, defaultActive: t.defaultActive, }), line({ - metric: distribution.newAddrCount[t.key].sum._24h, + metric: addresses.new[t.key].sum._24h, name: t.name, color: t.color, unit: Unit.count, @@ -974,95 +962,83 @@ export function createNetworkSection() { }, { name: "Reactivated", - title: "Reactivated Addresses per Block by Type", - bottom: addressTypes.flatMap((t) => [ - line({ - metric: - distribution.addressActivity[t.key].reactivated.height, - name: t.name, - color: t.color, - unit: Unit.count, - defaultActive: t.defaultActive, - }), - line({ - metric: - distribution.addressActivity[t.key].reactivated.average - ._24h, - name: t.name, - color: t.color, - unit: Unit.count, - defaultActive: t.defaultActive, - }), - ]), + tree: [ + { + name: "Base", + title: "Reactivated Addresses per Block by Type", + bottom: addressTypes.map((t) => + line({ + metric: addresses.activity[t.key].reactivated.height, + name: t.name, + color: t.color, + unit: Unit.count, + defaultActive: t.defaultActive, + }), + ), + }, + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: `Reactivated Addresses by Type (${w.name})`, + bottom: addressTypes.map((t) => + line({ + metric: addresses.activity[t.key].reactivated[w.key], + name: t.name, + color: t.color, + unit: Unit.count, + defaultActive: t.defaultActive, + }), + ), + })), + ], }, { name: "Growth Rate", - title: "Address Growth Rate per Block by Type", - bottom: addressTypes.flatMap((t) => [ - dots({ - metric: distribution.growthRate[t.key].height, - name: t.name, - color: t.color, - unit: Unit.ratio, - defaultActive: t.defaultActive, - }), - dots({ - metric: distribution.growthRate[t.key].average._24h, - name: t.name, - color: t.color, - unit: Unit.ratio, - defaultActive: t.defaultActive, - }), - ]), + tree: ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: `Address Growth Rate by Type (${w.name})`, + bottom: addressTypes.map((t) => + line({ + metric: addresses.delta[t.key].rate[w.key].ratio, + name: t.name, + color: t.color, + unit: Unit.ratio, + defaultActive: t.defaultActive, + }), + ), + })), }, { name: "Transacting", tree: transactingTypes.map((tr) => ({ name: tr.name, - title: tr.compareTitle, - bottom: addressTypes.flatMap((t) => [ - line({ - metric: - distribution.addressActivity[t.key][tr.key].height, - name: t.name, - color: t.color, - unit: Unit.count, - defaultActive: t.defaultActive, - }), - line({ - metric: - distribution.addressActivity[t.key][tr.key].average - ._24h, - name: t.name, - color: t.color, - unit: Unit.count, - defaultActive: t.defaultActive, - }), - ]), - })), - }, - { - name: "Balance", - tree: balanceTypes.map((b) => ({ - name: b.name, - title: b.compareTitle, - bottom: addressTypes.flatMap((t) => [ - line({ - metric: distribution.addressActivity[t.key][b.key].height, - name: t.name, - color: t.color, - unit: Unit.count, - defaultActive: t.defaultActive, - }), - line({ - metric: - distribution.addressActivity[t.key][b.key].average._24h, - name: t.name, - color: t.color, - unit: Unit.count, - defaultActive: t.defaultActive, - }), - ]), + tree: [ + { + name: "Base", + title: tr.compareTitle, + bottom: addressTypes.map((t) => + line({ + metric: addresses.activity[t.key][tr.key].height, + name: t.name, + color: t.color, + unit: Unit.count, + defaultActive: t.defaultActive, + }), + ), + }, + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: `${tr.compareTitle} (${w.name})`, + bottom: addressTypes.map((t) => + line({ + metric: addresses.activity[t.key][tr.key][w.key], + name: t.name, + color: t.color, + unit: Unit.count, + defaultActive: t.defaultActive, + }), + ), + })), + ], })), }, ], @@ -1237,13 +1213,13 @@ export function createNetworkSection() { title: "Script Adoption", bottom: [ line({ - metric: scripts.adoption.segwit, + metric: scripts.adoption.segwit.percent, name: "SegWit", color: colors.segwit, unit: Unit.percentage, }), line({ - metric: scripts.adoption.taproot, + metric: scripts.adoption.taproot.percent, name: "Taproot", color: taprootAddresses[1].color, unit: Unit.percentage, @@ -1255,7 +1231,7 @@ export function createNetworkSection() { title: "SegWit Adoption", bottom: [ line({ - metric: scripts.adoption.segwit, + metric: scripts.adoption.segwit.percent, name: "Adoption", unit: Unit.percentage, }), @@ -1266,7 +1242,7 @@ export function createNetworkSection() { title: "Taproot Adoption", bottom: [ line({ - metric: scripts.adoption.taproot, + metric: scripts.adoption.taproot.percent, name: "Adoption", unit: Unit.percentage, }), diff --git a/website/scripts/options/series.js b/website/scripts/options/series.js index 42a5f1716..69f2b48c3 100644 --- a/website/scripts/options/series.js +++ b/website/scripts/options/series.js @@ -3,6 +3,36 @@ import { colors } from "../utils/colors.js"; import { Unit } from "../utils/units.js"; +// ============================================================================ +// Rolling window constants +// ============================================================================ + +/** @typedef {'_24h' | '_1w' | '_1m' | '_1y'} RollingWindowKey */ + +/** @type {ReadonlyArray<{key: RollingWindowKey, name: string, color: Color}>} */ +export const ROLLING_WINDOWS = [ + { key: "_24h", name: "24h", color: colors.time._24h }, + { key: "_1w", name: "1w", color: colors.time._1w }, + { key: "_1m", name: "1m", color: colors.time._1m }, + { key: "_1y", name: "1y", color: colors.time._1y }, +]; + +/** + * Extract a metric from each rolling window via a mapping function + * @template T + * @param {{ _24h: T, _1w: T, _1m: T, _1y: T }} windows + * @param {(v: T) => AnyMetricPattern} extract + * @returns {{ _24h: AnyMetricPattern, _1w: AnyMetricPattern, _1m: AnyMetricPattern, _1y: AnyMetricPattern }} + */ +export function mapWindows(windows, extract) { + return { + _24h: extract(windows._24h), + _1w: extract(windows._1w), + _1m: extract(windows._1m), + _1y: extract(windows._1y), + }; +} + // ============================================================================ // Price helper for top pane (auto-expands to USD + sats) // ============================================================================ @@ -439,46 +469,67 @@ export function statsAtWindow(pattern, window) { * @param {{ _24h: AnyMetricPattern, _1w: AnyMetricPattern, _1m: AnyMetricPattern, _1y: AnyMetricPattern }} args.windows * @param {string} args.title * @param {Unit} args.unit + * @param {(args: {metric: AnyMetricPattern, name: string, color: Color, unit: Unit}) => AnyFetchedSeriesBlueprint} [args.series] * @returns {PartialOptionsGroup} */ -export function rollingWindowsTree({ windows, title, unit }) { +export function rollingWindowsTree({ windows, title, unit, series = line }) { return { name: "Rolling", tree: [ { name: "Compare", title: `${title} Rolling`, + bottom: ROLLING_WINDOWS.map((w) => + series({ + metric: windows[w.key], + name: w.name, + color: w.color, + unit, + }), + ), + }, + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: `${title} ${w.name}`, bottom: [ - line({ metric: windows._24h, name: "24h", color: colors.time._24h, unit }), - line({ metric: windows._1w, name: "7d", color: colors.time._1w, unit }), - line({ metric: windows._1m, name: "30d", color: colors.time._1m, unit }), - line({ metric: windows._1y, name: "1y", color: colors.time._1y, unit }), + series({ + metric: windows[w.key], + name: w.name, + color: w.color, + unit, + }), ], - }, - { - name: "24h", - title: `${title} 24h`, - bottom: [line({ metric: windows._24h, name: "24h", color: colors.time._24h, unit })], - }, - { - name: "7d", - title: `${title} 7d`, - bottom: [line({ metric: windows._1w, name: "7d", color: colors.time._1w, unit })], - }, - { - name: "30d", - title: `${title} 30d`, - bottom: [line({ metric: windows._1m, name: "30d", color: colors.time._1m, unit })], - }, - { - name: "1y", - title: `${title} 1y`, - bottom: [line({ metric: windows._1y, name: "1y", color: colors.time._1y, unit })], - }, + })), ], }; } +/** + * Create a Distribution folder tree with stats at each rolling window (24h/7d/30d/1y) + * @param {Object} args + * @param {Record} args.pattern - Pattern with pct10/pct25/... and average/median/... as _1y24h30d7dPattern + * @param {AnyMetricPattern} [args.base] - Optional base metric to show as dots on each chart + * @param {string} args.title + * @param {Unit} args.unit + * @returns {PartialOptionsGroup} + */ +export function distributionWindowsTree({ pattern, base, title, unit }) { + return { + name: "Distribution", + tree: ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: `${title} Distribution (${w.name})`, + bottom: [ + ...(base ? [line({ metric: base, name: "base", unit })] : []), + ...fromStatsPattern({ + pattern: statsAtWindow(pattern, w.key), + unit, + }), + ], + })), + }; +} + /** * Map a rolling window slot's stats to a specific unit, producing a stats-compatible pattern * @param {RollingWindowSlot} slot - Rolling window slot (e.g., pattern.rolling._24h) @@ -503,9 +554,18 @@ function rollingSlotForUnit(slot, unitKey) { * @returns {AnyFetchedSeriesBlueprint[]} */ export const distributionBtcSatsUsd = (slot) => [ - ...fromStatsPattern({ pattern: rollingSlotForUnit(slot, "btc"), unit: Unit.btc }), - ...fromStatsPattern({ pattern: rollingSlotForUnit(slot, "sats"), unit: Unit.sats }), - ...fromStatsPattern({ pattern: rollingSlotForUnit(slot, "usd"), unit: Unit.usd }), + ...fromStatsPattern({ + pattern: rollingSlotForUnit(slot, "btc"), + unit: Unit.btc, + }), + ...fromStatsPattern({ + pattern: rollingSlotForUnit(slot, "sats"), + unit: Unit.sats, + }), + ...fromStatsPattern({ + pattern: rollingSlotForUnit(slot, "usd"), + unit: Unit.usd, + }), ]; /** @@ -658,20 +718,16 @@ export function chartsFromFull({ distributionSuffix = "", }) { const distTitle = distributionSuffix - ? `${title} ${distributionSuffix} Distribution` - : `${title} Distribution`; + ? `${title} ${distributionSuffix}` + : title; return [ { name: "Sum", title, - bottom: [{ metric: pattern.height, title: "base", unit }], + bottom: [{ metric: pattern.base, title: "base", unit }], }, rollingWindowsTree({ windows: pattern.sum, title, unit }), - { - name: "Distribution", - title: distTitle, - bottom: distributionSeries(statsAtWindow(pattern, "_24h"), unit), - }, + distributionWindowsTree({ pattern, title: distTitle, unit }), { name: "Cumulative", title: `${title} (Total)`, @@ -708,17 +764,19 @@ export function chartsFromSum({ }) { const { stat } = colors; const distTitle = distributionSuffix - ? `${title} ${distributionSuffix} Distribution` - : `${title} Distribution`; + ? `${title} ${distributionSuffix}` + : title; return [ { name: "Sum", title, bottom: [{ metric: pattern.sum, title: "sum", color: stat.sum, unit }], }, + rollingWindowsTree({ windows: pattern.rolling.sum, title, unit }), + distributionWindowsTree({ pattern: pattern.rolling, title: distTitle, unit }), { name: "Distribution", - title: distTitle, + title: `${distTitle} Distribution`, bottom: distributionSeries(pattern, unit), }, { @@ -775,21 +833,19 @@ export function chartsFromValueFull({ pattern, title }) { bottom: [ ...btcSatsUsdSeries({ metrics: pattern.base, name: "sum" }), ...btcSatsUsdSeries({ - metrics: pattern._24h.sum, + metrics: pattern.sum._24h, name: "24h sum", defaultActive: false, }), ], }, - { - name: "Distribution", - title: `${title} Distribution`, - bottom: distributionBtcSatsUsd(pattern._24h), - }, { name: "Cumulative", title: `${title} (Total)`, - bottom: btcSatsUsdSeries({ metrics: pattern.cumulative, name: "all-time" }), + bottom: btcSatsUsdSeries({ + metrics: pattern.cumulative, + name: "all-time", + }), }, ]; } diff --git a/website/scripts/options/shared.js b/website/scripts/options/shared.js index 11e786c58..73bab42aa 100644 --- a/website/scripts/options/shared.js +++ b/website/scripts/options/shared.js @@ -148,7 +148,7 @@ export function satsBtcUsdBaseline({ pattern, name, color, defaultActive }) { /** * Create sats/btc/usd series from any value pattern using base or cumulative key * @param {Object} args - * @param {AnyValuePatternType} args.source + * @param {{ base: AnyValuePattern, cumulative: AnyValuePattern }} args.source * @param {'base' | 'cumulative'} args.key * @param {string} args.name * @param {Color} [args.color] @@ -167,7 +167,7 @@ export function satsBtcUsdFrom({ source, key, name, color, defaultActive }) { /** * Create sats/btc/usd series from a full value pattern using base or cumulative key * @param {Object} args - * @param {FullValuePattern} args.source + * @param {{ base: AnyValuePattern, cumulative: AnyValuePattern }} args.source * @param {'base' | 'cumulative'} args.key * @param {string} args.name * @param {Color} [args.color] @@ -192,9 +192,9 @@ export function satsBtcUsdFromFull({ /** * Create coinbase/subsidy/fee series from separate sources * @param {Object} args - * @param {AnyValuePatternType} args.coinbase - * @param {AnyValuePatternType} args.subsidy - * @param {AnyValuePatternType} args.fee + * @param {{ base: AnyValuePattern, cumulative: AnyValuePattern }} args.coinbase + * @param {{ base: AnyValuePattern, cumulative: AnyValuePattern }} args.subsidy + * @param {{ base: AnyValuePattern, cumulative: AnyValuePattern }} args.fee * @param {'base' | 'cumulative'} args.key * @returns {FetchedLineSeriesBlueprint[]} */ @@ -267,13 +267,14 @@ export function revenueRollingBtcSatsUsd({ coinbase, subsidy, fee }) { * @param {AnyRatioPattern} ratio */ export function percentileUsdMap(ratio) { + const p = ratio.percentiles; return /** @type {const} */ ([ - { name: "pct95", prop: ratio.ratioPct95Usd, color: colors.ratioPct._95 }, - { name: "pct5", prop: ratio.ratioPct5Usd, color: colors.ratioPct._5 }, - { name: "pct98", prop: ratio.ratioPct98Usd, color: colors.ratioPct._98 }, - { name: "pct2", prop: ratio.ratioPct2Usd, color: colors.ratioPct._2 }, - { name: "pct99", prop: ratio.ratioPct99Usd, color: colors.ratioPct._99 }, - { name: "pct1", prop: ratio.ratioPct1Usd, color: colors.ratioPct._1 }, + { name: "pct95", prop: p.pct95.price, color: colors.ratioPct._95 }, + { name: "pct5", prop: p.pct5.price, color: colors.ratioPct._5 }, + { name: "pct98", prop: p.pct98.price, color: colors.ratioPct._98 }, + { name: "pct2", prop: p.pct2.price, color: colors.ratioPct._2 }, + { name: "pct99", prop: p.pct99.price, color: colors.ratioPct._99 }, + { name: "pct1", prop: p.pct1.price, color: colors.ratioPct._1 }, ]); } @@ -282,13 +283,14 @@ export function percentileUsdMap(ratio) { * @param {AnyRatioPattern} ratio */ export function percentileMap(ratio) { + const p = ratio.percentiles; return /** @type {const} */ ([ - { name: "pct95", prop: ratio.ratioPct95, color: colors.ratioPct._95 }, - { name: "pct5", prop: ratio.ratioPct5, color: colors.ratioPct._5 }, - { name: "pct98", prop: ratio.ratioPct98, color: colors.ratioPct._98 }, - { name: "pct2", prop: ratio.ratioPct2, color: colors.ratioPct._2 }, - { name: "pct99", prop: ratio.ratioPct99, color: colors.ratioPct._99 }, - { name: "pct1", prop: ratio.ratioPct1, color: colors.ratioPct._1 }, + { name: "pct95", prop: p.pct95.ratio, color: colors.ratioPct._95 }, + { name: "pct5", prop: p.pct5.ratio, color: colors.ratioPct._5 }, + { name: "pct98", prop: p.pct98.ratio, color: colors.ratioPct._98 }, + { name: "pct2", prop: p.pct2.ratio, color: colors.ratioPct._2 }, + { name: "pct99", prop: p.pct99.ratio, color: colors.ratioPct._99 }, + { name: "pct1", prop: p.pct1.ratio, color: colors.ratioPct._1 }, ]); } @@ -298,10 +300,10 @@ export function percentileMap(ratio) { */ export function sdPatterns(ratio) { return /** @type {const} */ ([ - { nameAddon: "All Time", titleAddon: "", sd: ratio.ratioSd }, - { nameAddon: "4y", titleAddon: "4y", sd: ratio.ratio4ySd }, - { nameAddon: "2y", titleAddon: "2y", sd: ratio.ratio2ySd }, - { nameAddon: "1y", titleAddon: "1y", sd: ratio.ratio1ySd }, + { nameAddon: "All Time", titleAddon: "", sd: ratio.stdDev.all, smaRatio: ratio.sma.all.ratio }, + { nameAddon: "4y", titleAddon: "4y", sd: ratio.stdDev._4y, smaRatio: ratio.sma._4y.ratio }, + { nameAddon: "2y", titleAddon: "2y", sd: ratio.stdDev._2y, smaRatio: ratio.sma._2y.ratio }, + { nameAddon: "1y", titleAddon: "1y", sd: ratio.stdDev._1y, smaRatio: ratio.sma._1y.ratio }, ]); } @@ -311,41 +313,42 @@ export function sdPatterns(ratio) { */ export function sdBandsUsd(sd) { return /** @type {const} */ ([ - { name: "0σ", prop: sd._0sdUsd, color: colors.sd._0 }, - { name: "+0.5σ", prop: sd.p05sdUsd, color: colors.sd.p05 }, - { name: "−0.5σ", prop: sd.m05sdUsd, color: colors.sd.m05 }, - { name: "+1σ", prop: sd.p1sdUsd, color: colors.sd.p1 }, - { name: "−1σ", prop: sd.m1sdUsd, color: colors.sd.m1 }, - { name: "+1.5σ", prop: sd.p15sdUsd, color: colors.sd.p15 }, - { name: "−1.5σ", prop: sd.m15sdUsd, color: colors.sd.m15 }, - { name: "+2σ", prop: sd.p2sdUsd, color: colors.sd.p2 }, - { name: "−2σ", prop: sd.m2sdUsd, color: colors.sd.m2 }, - { name: "+2.5σ", prop: sd.p25sdUsd, color: colors.sd.p25 }, - { name: "−2.5σ", prop: sd.m25sdUsd, color: colors.sd.m25 }, - { name: "+3σ", prop: sd.p3sdUsd, color: colors.sd.p3 }, - { name: "−3σ", prop: sd.m3sdUsd, color: colors.sd.m3 }, + { name: "0σ", prop: sd._0sd, color: colors.sd._0 }, + { name: "+0.5σ", prop: sd.p05sd.price, color: colors.sd.p05 }, + { name: "−0.5σ", prop: sd.m05sd.price, color: colors.sd.m05 }, + { name: "+1σ", prop: sd.p1sd.price, color: colors.sd.p1 }, + { name: "−1σ", prop: sd.m1sd.price, color: colors.sd.m1 }, + { name: "+1.5σ", prop: sd.p15sd.price, color: colors.sd.p15 }, + { name: "−1.5σ", prop: sd.m15sd.price, color: colors.sd.m15 }, + { name: "+2σ", prop: sd.p2sd.price, color: colors.sd.p2 }, + { name: "−2σ", prop: sd.m2sd.price, color: colors.sd.m2 }, + { name: "+2.5σ", prop: sd.p25sd.price, color: colors.sd.p25 }, + { name: "−2.5σ", prop: sd.m25sd.price, color: colors.sd.m25 }, + { name: "+3σ", prop: sd.p3sd.price, color: colors.sd.p3 }, + { name: "−3σ", prop: sd.m3sd.price, color: colors.sd.m3 }, ]); } /** * Build SD band mappings (ratio) from an SD pattern * @param {Ratio1ySdPattern} sd + * @param {AnyMetricPattern} smaRatio */ -export function sdBandsRatio(sd) { +export function sdBandsRatio(sd, smaRatio) { return /** @type {const} */ ([ - { name: "0σ", prop: sd.sma, color: colors.sd._0 }, - { name: "+0.5σ", prop: sd.p05sd, color: colors.sd.p05 }, - { name: "−0.5σ", prop: sd.m05sd, color: colors.sd.m05 }, - { name: "+1σ", prop: sd.p1sd, color: colors.sd.p1 }, - { name: "−1σ", prop: sd.m1sd, color: colors.sd.m1 }, - { name: "+1.5σ", prop: sd.p15sd, color: colors.sd.p15 }, - { name: "−1.5σ", prop: sd.m15sd, color: colors.sd.m15 }, - { name: "+2σ", prop: sd.p2sd, color: colors.sd.p2 }, - { name: "−2σ", prop: sd.m2sd, color: colors.sd.m2 }, - { name: "+2.5σ", prop: sd.p25sd, color: colors.sd.p25 }, - { name: "−2.5σ", prop: sd.m25sd, color: colors.sd.m25 }, - { name: "+3σ", prop: sd.p3sd, color: colors.sd.p3 }, - { name: "−3σ", prop: sd.m3sd, color: colors.sd.m3 }, + { name: "0σ", prop: smaRatio, color: colors.sd._0 }, + { name: "+0.5σ", prop: sd.p05sd.value, color: colors.sd.p05 }, + { name: "−0.5σ", prop: sd.m05sd.value, color: colors.sd.m05 }, + { name: "+1σ", prop: sd.p1sd.value, color: colors.sd.p1 }, + { name: "−1σ", prop: sd.m1sd.value, color: colors.sd.m1 }, + { name: "+1.5σ", prop: sd.p15sd.value, color: colors.sd.p15 }, + { name: "−1.5σ", prop: sd.m15sd.value, color: colors.sd.m15 }, + { name: "+2σ", prop: sd.p2sd.value, color: colors.sd.p2 }, + { name: "−2σ", prop: sd.m2sd.value, color: colors.sd.m2 }, + { name: "+2.5σ", prop: sd.p25sd.value, color: colors.sd.p25 }, + { name: "−2.5σ", prop: sd.m25sd.value, color: colors.sd.m25 }, + { name: "+3σ", prop: sd.p3sd.value, color: colors.sd.p3 }, + { name: "−3σ", prop: sd.m3sd.value, color: colors.sd.m3 }, ]); } @@ -355,12 +358,12 @@ export function sdBandsRatio(sd) { */ export function ratioSmas(ratio) { return [ - { name: "1w SMA", metric: ratio.ratio1wSma }, - { name: "1m SMA", metric: ratio.ratio1mSma }, - { name: "1y SMA", metric: ratio.ratio1ySd.sma }, - { name: "2y SMA", metric: ratio.ratio2ySd.sma }, - { name: "4y SMA", metric: ratio.ratio4ySd.sma }, - { name: "All SMA", metric: ratio.ratioSd.sma, color: colors.time.all }, + { name: "1w SMA", metric: ratio.sma._1w.ratio }, + { name: "1m SMA", metric: ratio.sma._1m.ratio }, + { name: "1y SMA", metric: ratio.sma._1y.ratio }, + { name: "2y SMA", metric: ratio.sma._2y.ratio }, + { name: "4y SMA", metric: ratio.sma._4y.ratio }, + { name: "All SMA", metric: ratio.sma.all.ratio, color: colors.time.all }, ].map((s, i, arr) => ({ color: colors.at(i, arr.length), ...s })); } @@ -434,10 +437,10 @@ export function createZScoresFolder({ const sdPats = sdPatterns(ratio); const zscorePeriods = [ - { name: "1y", sd: ratio.ratio1ySd }, - { name: "2y", sd: ratio.ratio2ySd }, - { name: "4y", sd: ratio.ratio4ySd }, - { name: "all", sd: ratio.ratioSd, color: colors.time.all }, + { name: "1y", sd: ratio.stdDev._1y }, + { name: "2y", sd: ratio.stdDev._2y }, + { name: "4y", sd: ratio.stdDev._4y }, + { name: "all", sd: ratio.stdDev.all, color: colors.time.all }, ].map((s, i, arr) => ({ color: colors.at(i, arr.length), ...s })); return { @@ -450,7 +453,7 @@ export function createZScoresFolder({ price({ metric: pricePattern, name: legend, color }), ...zscorePeriods.map((p) => price({ - metric: p.sd._0sdUsd, + metric: p.sd._0sd, name: `${p.name} 0σ`, color: p.color, defaultActive: false, @@ -473,7 +476,7 @@ export function createZScoresFolder({ }), ], }, - ...sdPats.map(({ nameAddon, titleAddon, sd }) => { + ...sdPats.map(({ nameAddon, titleAddon, sd, smaRatio }) => { const prefix = titleAddon ? `${titleAddon} ` : ""; const topPrice = price({ metric: pricePattern, name: legend, color }); return { @@ -521,7 +524,7 @@ export function createZScoresFolder({ unit: Unit.ratio, base: 1, }), - ...sdBandsRatio(sd).map( + ...sdBandsRatio(sd, smaRatio).map( ({ name: bandName, prop, color: bandColor }) => line({ metric: prop, diff --git a/website/scripts/options/types.js b/website/scripts/options/types.js index bb3af339c..4f1daf769 100644 --- a/website/scripts/options/types.js +++ b/website/scripts/options/types.js @@ -173,9 +173,11 @@ * Patterns with RelToMarketCap in relative (geAmount.*, ltAmount.*): * @typedef {UtxoAmountPattern | AddressAmountPattern} PatternBasicWithMarketCap * - * Patterns without RelToMarketCap in relative (RelativePattern4): - * - EpochPattern (epoch.*, amountRange.*, year.*, type.*) - * @typedef {EpochPattern} PatternBasicWithoutMarketCap + * Patterns without RelToMarketCap in relative: + * - EpochPattern (epoch.*, year.*) + * - UtxoAmountPattern (amountRange.*) + * - OutputsRealizedSupplyUnrealizedPattern2 (addressable type.*) + * @typedef {EpochPattern | UtxoAmountPattern | Brk.OutputsRealizedSupplyUnrealizedPattern2} PatternBasicWithoutMarketCap * * Patterns without relative section entirely (edge case output types): * - EmptyPattern (type.empty, type.p2ms, type.unknown) @@ -259,8 +261,8 @@ * Extended Cohort Types (with address count) * ============================================================================ * - * Addressable cohort with address count (for "type" cohorts - no RelToMarketCap) - * @typedef {CohortBasicWithoutMarketCap & { addressCount: Brk.DeltaInnerPattern }} CohortAddress + * Addressable cohort with address count (for "type" cohorts - uses OutputsRealizedSupplyUnrealizedPattern2) + * @typedef {{ name: string, title: string, color: Color, tree: Brk.OutputsRealizedSupplyUnrealizedPattern2, addressCount: Brk.DeltaInnerPattern }} CohortAddress * * ============================================================================ * Cohort Group Types (by capability) diff --git a/website/scripts/options/unused.js b/website/scripts/options/unused.js index e8937a275..b1c66cc23 100644 --- a/website/scripts/options/unused.js +++ b/website/scripts/options/unused.js @@ -30,6 +30,8 @@ function walkMetrics(node, map, path) { key.endsWith("Cents") || key.endsWith("State") || key.endsWith("Start") || + kn === "cents" || + kn === "bps" || kn === "mvrv" || kn === "constants" || kn === "blockhash" || @@ -144,8 +146,10 @@ export function extractTreeStructure(options) { /** @type {Record} */ const grouped = {}; for (const s of series) { - const metric = /** @type {any} */ (s.metric); - if (isTop && metric?.usd && metric?.sats) { + const metric = /** @type {AnyMetricPattern | AnyPricePattern} */ ( + s.metric + ); + if (isTop && "usd" in metric && "sats" in metric) { const title = s.title || s.key || "unnamed"; (grouped["USD"] ??= []).push(title); (grouped["sats"] ??= []).push(title); diff --git a/website/scripts/types.js b/website/scripts/types.js index ba30bf667..1fc684808 100644 --- a/website/scripts/types.js +++ b/website/scripts/types.js @@ -30,63 +30,63 @@ * @typedef {SeriesMarker