mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-24 06:39:58 -07:00
global: snapshot part 13
This commit is contained in:
@@ -1063,10 +1063,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_empty_fills_loss_from_shortest_leaf() {
|
||||
// Integration test: "loss" child returns same base as parent (because
|
||||
// its children like neg_realized_loss break the prefix). The mixed-empty
|
||||
// fix should fill it from shortest leaf "utxos_realized_loss".
|
||||
fn test_loss_with_neg_suffix_has_correct_field_parts() {
|
||||
// Integration test: "loss" child has suffix-named children (realized_loss,
|
||||
// realized_loss_neg) so it returns a proper base that differs from parent.
|
||||
use brk_types::{SeriesLeaf, SeriesLeafWithSchema, TreeNode};
|
||||
|
||||
fn leaf(name: &str) -> TreeNode {
|
||||
@@ -1084,7 +1083,7 @@ mod tests {
|
||||
TreeNode::Branch(
|
||||
[
|
||||
("base".into(), leaf("utxos_realized_loss")),
|
||||
("negative".into(), leaf("utxos_neg_realized_loss")),
|
||||
("negative".into(), leaf("utxos_realized_loss_neg")),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
@@ -1115,9 +1114,7 @@ mod tests {
|
||||
assert!(!result.has_outlier);
|
||||
assert_eq!(result.field_parts.get("cap"), Some(&"realized_cap".to_string()));
|
||||
assert_eq!(result.field_parts.get("mvrv"), Some(&"mvrv".to_string()));
|
||||
// loss stays empty after first pass (child returned same base as parent).
|
||||
// The second pass (fill_mixed_empty_field_parts) fills it for templated
|
||||
// children, but that requires pattern_lookup which this test doesn't set up.
|
||||
assert_eq!(result.field_parts.get("loss"), Some(&"".to_string()));
|
||||
// loss branch returns base "utxos_realized_loss" which yields field_part "realized_loss"
|
||||
assert_eq!(result.field_parts.get("loss"), Some(&"realized_loss".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -52,7 +52,7 @@ impl RealizedCore {
|
||||
let minimal = RealizedMinimal::forced_import(cfg)?;
|
||||
|
||||
let neg_loss_base = LazyPerBlock::from_height_source::<NegCentsUnsignedToDollars>(
|
||||
&cfg.name("neg_realized_loss"),
|
||||
&cfg.name("realized_loss_neg"),
|
||||
cfg.version + Version::ONE,
|
||||
minimal.loss.block.cents.read_only_boxed_clone(),
|
||||
cfg.indexes,
|
||||
@@ -60,7 +60,7 @@ impl RealizedCore {
|
||||
|
||||
let neg_loss_sum = minimal.loss.sum.0.map_with_suffix(|suffix, slot| {
|
||||
LazyPerBlock::from_height_source::<NegCentsUnsignedToDollars>(
|
||||
&cfg.name(&format!("neg_realized_loss_sum_{suffix}")),
|
||||
&cfg.name(&format!("realized_loss_neg_sum_{suffix}")),
|
||||
cfg.version + Version::ONE,
|
||||
slot.cents.height.read_only_boxed_clone(),
|
||||
cfg.indexes,
|
||||
|
||||
@@ -6,8 +6,7 @@ use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
|
||||
use crate::{distribution::state::{CohortState, CostBasisOps, RealizedOps}, prices};
|
||||
|
||||
use crate::internal::{
|
||||
AmountPerBlock, HalveCents, HalveDollars, HalveSats, HalveSatsToBitcoin,
|
||||
LazyAmountPerBlock, LazyRollingDeltasFromHeight,
|
||||
AmountPerBlock, LazyRollingDeltasFromHeight,
|
||||
};
|
||||
|
||||
use crate::distribution::metrics::ImportConfig;
|
||||
@@ -16,20 +15,12 @@ use crate::distribution::metrics::ImportConfig;
|
||||
#[derive(Traversable)]
|
||||
pub struct SupplyBase<M: StorageMode = Rw> {
|
||||
pub total: AmountPerBlock<M>,
|
||||
pub half: LazyAmountPerBlock,
|
||||
pub delta: LazyRollingDeltasFromHeight<Sats, SatsSigned, BasisPointsSigned32>,
|
||||
}
|
||||
|
||||
impl SupplyBase {
|
||||
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
|
||||
let supply = cfg.import("supply", Version::ZERO)?;
|
||||
|
||||
let supply_half = LazyAmountPerBlock::from_block_source::<
|
||||
HalveSats,
|
||||
HalveSatsToBitcoin,
|
||||
HalveCents,
|
||||
HalveDollars,
|
||||
>(&cfg.name("supply_half"), &supply, cfg.version);
|
||||
let supply: AmountPerBlock = cfg.import("supply", Version::ZERO)?;
|
||||
|
||||
let delta = LazyRollingDeltasFromHeight::new(
|
||||
&cfg.name("supply_delta"),
|
||||
@@ -41,7 +32,6 @@ impl SupplyBase {
|
||||
|
||||
Ok(Self {
|
||||
total: supply,
|
||||
half: supply_half,
|
||||
delta,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
|
||||
|
||||
use crate::{distribution::state::UnrealizedState, prices};
|
||||
|
||||
use crate::internal::AmountPerBlock;
|
||||
use crate::internal::{
|
||||
AmountPerBlock, HalveCents, HalveDollars, HalveSats, HalveSatsToBitcoin, LazyAmountPerBlock,
|
||||
};
|
||||
|
||||
use crate::distribution::metrics::ImportConfig;
|
||||
|
||||
@@ -20,6 +22,7 @@ pub struct SupplyCore<M: StorageMode = Rw> {
|
||||
#[traversable(flatten)]
|
||||
pub base: SupplyBase<M>,
|
||||
|
||||
pub half: LazyAmountPerBlock,
|
||||
pub in_profit: AmountPerBlock<M>,
|
||||
pub in_loss: AmountPerBlock<M>,
|
||||
}
|
||||
@@ -29,8 +32,16 @@ impl SupplyCore {
|
||||
let v0 = Version::ZERO;
|
||||
let base = SupplyBase::forced_import(cfg)?;
|
||||
|
||||
let half = LazyAmountPerBlock::from_block_source::<
|
||||
HalveSats,
|
||||
HalveSatsToBitcoin,
|
||||
HalveCents,
|
||||
HalveDollars,
|
||||
>(&cfg.name("supply_half"), &base.total, cfg.version);
|
||||
|
||||
Ok(Self {
|
||||
base,
|
||||
half,
|
||||
in_profit: cfg.import("supply_in_profit", v0)?,
|
||||
in_loss: cfg.import("supply_in_loss", v0)?,
|
||||
})
|
||||
|
||||
@@ -30,7 +30,7 @@ impl UnrealizedBasic {
|
||||
let loss: FiatPerBlock<Cents> = cfg.import("unrealized_loss", v1)?;
|
||||
|
||||
let neg_loss = LazyPerBlock::from_computed::<NegCentsUnsignedToDollars>(
|
||||
&cfg.name("neg_unrealized_loss"),
|
||||
&cfg.name("unrealized_loss_neg"),
|
||||
cfg.version,
|
||||
loss.cents.height.read_only_boxed_clone(),
|
||||
&loss.cents,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -71,35 +71,6 @@ function volumeFolderWithProfitability(activity, color, title) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Full activity items: volume (with profitability), coindays, dormancy
|
||||
* @param {FullActivityPattern} activity
|
||||
* @param {Color} color
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function fullVolumeTree(activity, color, title) {
|
||||
return [
|
||||
volumeFolderWithProfitability(activity, color, title),
|
||||
{
|
||||
name: "Coindays Destroyed",
|
||||
tree: chartsFromCount({
|
||||
pattern: activity.coindaysDestroyed,
|
||||
title: title("Coindays Destroyed"),
|
||||
unit: Unit.coindays,
|
||||
color,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Dormancy",
|
||||
tree: averagesArray({
|
||||
windows: activity.dormancy,
|
||||
title: title("Dormancy"),
|
||||
unit: Unit.days,
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Shared SOPR Helpers
|
||||
@@ -180,64 +151,46 @@ function singleSellSideRiskTree(sellSideRisk, title) {
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Full activity with adjusted SOPR (All/STH)
|
||||
* @param {{ cohort: CohortAll | CohortFull, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
* Single activity tree items shared between WithAdjusted and basic
|
||||
* @param {CohortAll | CohortFull | CohortLongTerm} cohort
|
||||
* @param {(name: string) => string} title
|
||||
* @param {PartialOptionsGroup} soprFolder
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function createActivitySectionWithAdjusted({ cohort, title }) {
|
||||
function singleFullActivityTree(cohort, title, soprFolder) {
|
||||
const { tree, color } = cohort;
|
||||
const r = tree.realized;
|
||||
const sopr = r.sopr;
|
||||
return [
|
||||
volumeFolderWithProfitability(tree.activity, color, title),
|
||||
soprFolder,
|
||||
{ name: "Coindays Destroyed", tree: chartsFromCount({ pattern: tree.activity.coindaysDestroyed, title: title("Coindays Destroyed"), unit: Unit.coindays, color }) },
|
||||
{ name: "Dormancy", tree: averagesArray({ windows: tree.activity.dormancy, title: title("Dormancy"), unit: Unit.days }) },
|
||||
{ name: "Sell Side Risk", tree: singleSellSideRiskTree(tree.realized.sellSideRiskRatio, title) },
|
||||
];
|
||||
}
|
||||
|
||||
/** @param {{ cohort: CohortAll | CohortFull, title: (name: string) => string }} args */
|
||||
export function createActivitySectionWithAdjusted({ cohort, title }) {
|
||||
const sopr = cohort.tree.realized.sopr;
|
||||
return {
|
||||
name: "Activity",
|
||||
tree: [
|
||||
...fullVolumeTree(tree.activity, color, title),
|
||||
{
|
||||
name: "SOPR",
|
||||
tree: [
|
||||
...singleRollingSoprTree(sopr.ratio, title),
|
||||
{
|
||||
name: "Adjusted",
|
||||
tree: singleRollingSoprTree(
|
||||
sopr.adjusted.ratio,
|
||||
title,
|
||||
"Adjusted ",
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Sell Side Risk",
|
||||
tree: singleSellSideRiskTree(r.sellSideRiskRatio, title),
|
||||
},
|
||||
],
|
||||
tree: singleFullActivityTree(cohort, title, {
|
||||
name: "SOPR",
|
||||
tree: [
|
||||
...singleRollingSoprTree(sopr.ratio, title),
|
||||
{ name: "Adjusted", tree: singleRollingSoprTree(sopr.adjusted.ratio, title, "Adjusted ") },
|
||||
],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Activity section for cohorts with rolling SOPR + sell side risk (LTH, also CohortFull | CohortLongTerm)
|
||||
* @param {{ cohort: CohortFull | CohortLongTerm, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
/** @param {{ cohort: CohortFull | CohortLongTerm, title: (name: string) => string }} args */
|
||||
export function createActivitySection({ cohort, title }) {
|
||||
const { tree, color } = cohort;
|
||||
const r = tree.realized;
|
||||
const sopr = r.sopr;
|
||||
|
||||
return {
|
||||
name: "Activity",
|
||||
tree: [
|
||||
...fullVolumeTree(tree.activity, color, title),
|
||||
{
|
||||
name: "SOPR",
|
||||
tree: singleRollingSoprTree(sopr.ratio, title),
|
||||
},
|
||||
{
|
||||
name: "Sell Side Risk",
|
||||
tree: singleSellSideRiskTree(r.sellSideRiskRatio, title),
|
||||
},
|
||||
],
|
||||
tree: singleFullActivityTree(cohort, title, {
|
||||
name: "SOPR",
|
||||
tree: singleRollingSoprTree(cohort.tree.realized.sopr.ratio, title),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -316,6 +269,27 @@ export function createGroupedActivitySectionMinimal({ list, all, title }) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Grouped volume folder with In Profit/Loss subfolders
|
||||
* @template {{ name: string, color: Color }} T
|
||||
* @template {{ name: string, color: Color }} A
|
||||
* @param {readonly T[]} list
|
||||
* @param {A} all
|
||||
* @param {(name: string) => string} title
|
||||
* @param {(c: T | A) => { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern, inProfit: { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern }, inLoss: { sum: Record<string, AnyValuePattern>, cumulative: AnyValuePattern } }} getTransferVolume
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
function groupedVolumeFolder(list, all, title, getTransferVolume) {
|
||||
return {
|
||||
name: "Volume",
|
||||
tree: [
|
||||
...groupedWindowsCumulativeSatsBtcUsd({ list, all, title, metricTitle: "Sent Volume", getMetric: (c) => getTransferVolume(c) }),
|
||||
{ name: "In Profit", tree: groupedWindowsCumulativeSatsBtcUsd({ list, all, title, metricTitle: "Sent In Profit", getMetric: (c) => getTransferVolume(c).inProfit }) },
|
||||
{ name: "In Loss", tree: groupedWindowsCumulativeSatsBtcUsd({ list, all, title, metricTitle: "Sent In Loss", getMetric: (c) => getTransferVolume(c).inLoss }) },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Grouped SOPR Helpers
|
||||
// ============================================================================
|
||||
@@ -366,214 +340,88 @@ function groupedSoprCharts(list, all, getRatio, title, prefix = "") {
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @param {{ list: readonly CohortFull[], all: CohortAll, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
* Grouped activity tree items shared between WithAdjusted and basic
|
||||
* @param {readonly (CohortFull | CohortLongTerm)[]} list
|
||||
* @param {CohortAll} all
|
||||
* @param {(name: string) => string} title
|
||||
* @param {PartialOptionsGroup} soprFolder
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function groupedFullActivityTree(list, all, title, soprFolder) {
|
||||
return [
|
||||
groupedVolumeFolder(list, all, title, (c) => c.tree.activity.transferVolume),
|
||||
soprFolder,
|
||||
...groupedActivitySharedItems(list, all, title),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param {{ list: readonly CohortFull[], all: CohortAll, title: (name: string) => string }} args */
|
||||
export function createGroupedActivitySectionWithAdjusted({ list, all, title }) {
|
||||
return {
|
||||
name: "Activity",
|
||||
tree: [
|
||||
{
|
||||
name: "Volume",
|
||||
tree: [
|
||||
...groupedWindowsCumulativeSatsBtcUsd({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
metricTitle: "Sent Volume",
|
||||
getMetric: (c) => c.tree.activity.transferVolume,
|
||||
}),
|
||||
{
|
||||
name: "In Profit",
|
||||
tree: groupedWindowsCumulativeSatsBtcUsd({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
metricTitle: "Sent In Profit",
|
||||
getMetric: (c) => c.tree.activity.transferVolume.inProfit,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "In Loss",
|
||||
tree: groupedWindowsCumulativeSatsBtcUsd({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
metricTitle: "Sent In Loss",
|
||||
getMetric: (c) => c.tree.activity.transferVolume.inLoss,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Coindays Destroyed",
|
||||
tree: groupedWindowsCumulative({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
metricTitle: "Coindays Destroyed",
|
||||
getWindowSeries: (c, key) =>
|
||||
c.tree.activity.coindaysDestroyed.sum[key],
|
||||
getCumulativeSeries: (c) =>
|
||||
c.tree.activity.coindaysDestroyed.cumulative,
|
||||
seriesFn: line,
|
||||
unit: Unit.coindays,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Dormancy",
|
||||
tree: ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`Dormancy (${w.title})`),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
series: tree.activity.dormancy[w.key],
|
||||
name,
|
||||
color,
|
||||
unit: Unit.days,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: "SOPR",
|
||||
tree: [
|
||||
...groupedSoprCharts(
|
||||
list,
|
||||
all,
|
||||
(c) => c.tree.realized.sopr.ratio,
|
||||
title,
|
||||
),
|
||||
{
|
||||
name: "Adjusted",
|
||||
tree: groupedSoprCharts(
|
||||
list,
|
||||
all,
|
||||
(c) => c.tree.realized.sopr.adjusted.ratio,
|
||||
title,
|
||||
"Adjusted ",
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Sell Side Risk",
|
||||
tree: ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`Sell Side Risk (${w.title})`),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
series: tree.realized.sellSideRiskRatio[w.key].ratio,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
},
|
||||
],
|
||||
tree: groupedFullActivityTree(list, all, title, {
|
||||
name: "SOPR",
|
||||
tree: [
|
||||
...groupedSoprCharts(list, all, (c) => c.tree.realized.sopr.ratio, title),
|
||||
{ name: "Adjusted", tree: groupedSoprCharts(list, all, (c) => c.tree.realized.sopr.adjusted.ratio, title, "Adjusted ") },
|
||||
],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {{ list: readonly (CohortFull | CohortLongTerm)[], all: CohortAll, title: (name: string) => string }} args */
|
||||
export function createGroupedActivitySection({ list, all, title }) {
|
||||
return {
|
||||
name: "Activity",
|
||||
tree: groupedFullActivityTree(list, all, title, {
|
||||
name: "SOPR",
|
||||
tree: groupedSoprCharts(list, all, (c) => c.tree.realized.sopr.ratio, title),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Grouped activity for cohorts with rolling SOPR + sell side risk (LTH-like)
|
||||
* @param {{ list: readonly (CohortFull | CohortLongTerm)[], all: CohortAll, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
* Shared grouped activity items: coindays, dormancy, sell side risk
|
||||
* @param {readonly (CohortFull | CohortLongTerm)[]} list
|
||||
* @param {CohortAll} all
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
export function createGroupedActivitySection({ list, all, title }) {
|
||||
return {
|
||||
name: "Activity",
|
||||
tree: [
|
||||
{
|
||||
name: "Volume",
|
||||
tree: [
|
||||
...groupedWindowsCumulativeSatsBtcUsd({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
metricTitle: "Sent Volume",
|
||||
getMetric: (c) => c.tree.activity.transferVolume,
|
||||
}),
|
||||
{
|
||||
name: "In Profit",
|
||||
tree: groupedWindowsCumulativeSatsBtcUsd({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
metricTitle: "Sent In Profit",
|
||||
getMetric: (c) => c.tree.activity.transferVolume.inProfit,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "In Loss",
|
||||
tree: groupedWindowsCumulativeSatsBtcUsd({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
metricTitle: "Sent In Loss",
|
||||
getMetric: (c) => c.tree.activity.transferVolume.inLoss,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Coindays Destroyed",
|
||||
tree: groupedWindowsCumulative({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
metricTitle: "Coindays Destroyed",
|
||||
getWindowSeries: (c, key) =>
|
||||
c.tree.activity.coindaysDestroyed.sum[key],
|
||||
getCumulativeSeries: (c) =>
|
||||
c.tree.activity.coindaysDestroyed.cumulative,
|
||||
seriesFn: line,
|
||||
unit: Unit.coindays,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Dormancy",
|
||||
tree: ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`Dormancy (${w.title})`),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
series: tree.activity.dormancy[w.key],
|
||||
name,
|
||||
color,
|
||||
unit: Unit.days,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: "SOPR",
|
||||
tree: groupedSoprCharts(
|
||||
list,
|
||||
all,
|
||||
(c) => c.tree.realized.sopr.ratio,
|
||||
title,
|
||||
function groupedActivitySharedItems(list, all, title) {
|
||||
return [
|
||||
{
|
||||
name: "Coindays Destroyed",
|
||||
tree: groupedWindowsCumulative({
|
||||
list, all, title, metricTitle: "Coindays Destroyed",
|
||||
getWindowSeries: (c, key) => c.tree.activity.coindaysDestroyed.sum[key],
|
||||
getCumulativeSeries: (c) => c.tree.activity.coindaysDestroyed.cumulative,
|
||||
seriesFn: line, unit: Unit.coindays,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Dormancy",
|
||||
tree: ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`Dormancy (${w.title})`),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({ series: tree.activity.dormancy[w.key], name, color, unit: Unit.days }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Sell Side Risk",
|
||||
tree: ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`Sell Side Risk (${w.title})`),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
series: tree.realized.sellSideRiskRatio[w.key].ratio,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
},
|
||||
],
|
||||
};
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: "Sell Side Risk",
|
||||
tree: ROLLING_WINDOWS.map((w) => ({
|
||||
name: w.name,
|
||||
title: title(`Sell Side Risk (${w.title})`),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({ series: tree.realized.sellSideRiskRatio[w.key].ratio, name, color, unit: Unit.ratio }),
|
||||
),
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Grouped activity for cohorts with activity but basic realized (AgeRange/MaxAge)
|
||||
* @param {{ list: readonly (CohortAgeRange | CohortWithAdjusted)[], all: CohortAll, title: (name: string) => string }} args
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
baseline,
|
||||
sumsTreeBaseline,
|
||||
rollingPercentRatioTree,
|
||||
percentRatio,
|
||||
percentRatioBaseline,
|
||||
} from "../series.js";
|
||||
import {
|
||||
@@ -29,7 +30,7 @@ import { priceLines } from "../constants.js";
|
||||
|
||||
/**
|
||||
* Simple supply series (total + half only, no profit/loss)
|
||||
* @param {{ total: AnyValuePattern, half: AnyValuePattern }} supply
|
||||
* @param {{ total: AnyValuePattern }} supply
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
function simpleSupplySeries(supply) {
|
||||
@@ -39,68 +40,6 @@ function simpleSupplySeries(supply) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* % of Own Supply series (profit/loss relative to own supply)
|
||||
* @param {{ inProfit: { toOwn: { percent: AnySeriesPattern, ratio: AnySeriesPattern } }, inLoss: { toOwn: { percent: AnySeriesPattern, ratio: AnySeriesPattern } } }} supply
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
function ownSupplyPctSeries(supply) {
|
||||
return [
|
||||
line({
|
||||
series: supply.inProfit.toOwn.percent,
|
||||
name: "In Profit",
|
||||
color: colors.profit,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
line({
|
||||
series: supply.inLoss.toOwn.percent,
|
||||
name: "In Loss",
|
||||
color: colors.loss,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
line({
|
||||
series: supply.inProfit.toOwn.ratio,
|
||||
name: "In Profit",
|
||||
color: colors.profit,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
line({
|
||||
series: supply.inLoss.toOwn.ratio,
|
||||
name: "In Loss",
|
||||
color: colors.loss,
|
||||
unit: Unit.ratio,
|
||||
}),
|
||||
...priceLines({ numbers: [100, 50, 0], unit: Unit.percentage }),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* % of Circulating Supply series (total, profit, loss)
|
||||
* @param {{ toCirculating: { percent: AnySeriesPattern }, inProfit: { toCirculating: { percent: AnySeriesPattern } }, inLoss: { toCirculating: { percent: AnySeriesPattern } } }} supply
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
function circulatingSupplyPctSeries(supply) {
|
||||
return [
|
||||
line({
|
||||
series: supply.toCirculating.percent,
|
||||
name: "Total",
|
||||
color: colors.default,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
line({
|
||||
series: supply.inProfit.toCirculating.percent,
|
||||
name: "In Profit",
|
||||
color: colors.profit,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
line({
|
||||
series: supply.inLoss.toCirculating.percent,
|
||||
name: "In Loss",
|
||||
color: colors.loss,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {readonly (UtxoCohortObject | CohortWithoutRelative)[]} list
|
||||
@@ -244,7 +183,11 @@ function circulatingChart(supply, title) {
|
||||
return {
|
||||
name: "% of Circulating",
|
||||
title: title("Supply (% of Circulating)"),
|
||||
bottom: circulatingSupplyPctSeries(supply),
|
||||
bottom: [
|
||||
line({ series: supply.toCirculating.percent, name: "Total", color: colors.default, unit: Unit.percentage }),
|
||||
line({ series: supply.inProfit.toCirculating.percent, name: "In Profit", color: colors.profit, unit: Unit.percentage }),
|
||||
line({ series: supply.inLoss.toCirculating.percent, name: "In Loss", color: colors.loss, unit: Unit.percentage }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -257,7 +200,11 @@ function ownSupplyChart(supply, title) {
|
||||
return {
|
||||
name: "% of Own Supply",
|
||||
title: title("Supply (% of Own)"),
|
||||
bottom: ownSupplyPctSeries(supply),
|
||||
bottom: [
|
||||
...percentRatio({ pattern: supply.inProfit.toOwn, name: "In Profit", color: colors.profit }),
|
||||
...percentRatio({ pattern: supply.inLoss.toOwn, name: "In Loss", color: colors.loss }),
|
||||
...priceLines({ numbers: [100, 50, 0], unit: Unit.percentage }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -460,6 +407,35 @@ export function createHoldingsSectionAddressAmount({ cohort, title }) {
|
||||
];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Grouped Cohort Supply Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @template {{ name: string, color: Color, tree: { supply: { total: AnyValuePattern } } }} T
|
||||
* @param {readonly T[]} list
|
||||
* @param {CohortAll} all
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialChartOption}
|
||||
*/
|
||||
function groupedSupplyTotal(list, all, title) {
|
||||
return { name: "Total", title: title("Supply"), bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => satsBtcUsd({ pattern: tree.supply.total, name, color })) };
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {{ name: string, color: Color, tree: { supply: { inProfit: AnyValuePattern, inLoss: AnyValuePattern } } }} T
|
||||
* @param {readonly T[]} list
|
||||
* @param {CohortAll} all
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function groupedSupplyProfitLoss(list, all, title) {
|
||||
return [
|
||||
{ name: "In Profit", title: title("Supply In Profit"), 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.supply.inLoss, name, color })) },
|
||||
];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Grouped Cohort Holdings Sections
|
||||
// ============================================================================
|
||||
@@ -473,27 +449,8 @@ export function createGroupedHoldingsSectionAddress({ list, all, title }) {
|
||||
{
|
||||
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.supply.inProfit, name, color }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "In Loss",
|
||||
title: title("Supply In Loss"),
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
satsBtcUsd({ pattern: tree.supply.inLoss, name, color }),
|
||||
),
|
||||
},
|
||||
groupedSupplyTotal(list, all, title),
|
||||
...groupedSupplyProfitLoss(list, all, title),
|
||||
...groupedDeltaItems(list, all, (c) => c.tree.supply.delta, Unit.sats, title, "Supply"),
|
||||
],
|
||||
},
|
||||
@@ -524,7 +481,7 @@ export function createGroupedHoldingsSectionAddressAmount({ list, all, title })
|
||||
{
|
||||
name: "Supply",
|
||||
tree: [
|
||||
{ name: "Total", title: title("Supply"), bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => satsBtcUsd({ pattern: tree.supply.total, name, color })) },
|
||||
groupedSupplyTotal(list, all, title),
|
||||
...groupedDeltaItems(list, all, (c) => c.tree.supply.delta, Unit.sats, title, "Supply"),
|
||||
],
|
||||
},
|
||||
@@ -551,7 +508,7 @@ export function createGroupedHoldingsSection({ list, all, title }) {
|
||||
{
|
||||
name: "Supply",
|
||||
tree: [
|
||||
{ name: "Total", title: title("Supply"), bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => satsBtcUsd({ pattern: tree.supply.total, name, color })) },
|
||||
groupedSupplyTotal(list, all, title),
|
||||
...groupedDeltaItems(list, all, (c) => c.tree.supply.delta, Unit.sats, title, "Supply"),
|
||||
],
|
||||
},
|
||||
@@ -565,9 +522,8 @@ export function createGroupedHoldingsSectionWithProfitLoss({ list, all, title })
|
||||
{
|
||||
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.supply.inProfit, name, color })) },
|
||||
{ name: "In Loss", title: title("Supply In Loss"), bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => satsBtcUsd({ pattern: tree.supply.inLoss, name, color })) },
|
||||
groupedSupplyTotal(list, all, title),
|
||||
...groupedSupplyProfitLoss(list, all, title),
|
||||
...groupedDeltaItems(list, all, (c) => c.tree.supply.delta, Unit.sats, title, "Supply"),
|
||||
],
|
||||
},
|
||||
@@ -581,9 +537,8 @@ export function createGroupedHoldingsSectionWithOwnSupply({ list, all, title })
|
||||
{
|
||||
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.supply.inProfit, name, color })) },
|
||||
{ name: "In Loss", title: title("Supply In Loss"), bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => satsBtcUsd({ pattern: tree.supply.inLoss, name, color })) },
|
||||
groupedSupplyTotal(list, all, title),
|
||||
...groupedSupplyProfitLoss(list, all, title),
|
||||
{ name: "% of Circulating", title: title("Supply (% of Circulating)"), bottom: mapCohorts(list, ({ name, color, tree }) => line({ series: tree.supply.toCirculating.percent, name, color, unit: Unit.percentage })) },
|
||||
...groupedDeltaItems(list, all, (c) => c.tree.supply.delta, Unit.sats, title, "Supply"),
|
||||
],
|
||||
@@ -603,9 +558,8 @@ export function createGroupedHoldingsSectionWithRelative({ list, all, title }) {
|
||||
{
|
||||
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.supply.inProfit, name, color })) },
|
||||
{ name: "In Loss", title: title("Supply In Loss"), bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => satsBtcUsd({ pattern: tree.supply.inLoss, name, color })) },
|
||||
groupedSupplyTotal(list, all, title),
|
||||
...groupedSupplyProfitLoss(list, all, title),
|
||||
{ name: "% of Circulating", title: title("Supply (% of Circulating)"), bottom: mapCohorts(list, ({ name, color, tree }) => line({ series: tree.supply.toCirculating.percent, name, color, unit: Unit.percentage })) },
|
||||
{ name: "% of Own Supply", title: title("Supply (% of Own)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.supply.inProfit.toOwn.percent, name, color, unit: Unit.percentage })) },
|
||||
...groupedDeltaItems(list, all, (c) => c.tree.supply.delta, Unit.sats, title, "Supply"),
|
||||
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
createHoldingsSectionWithRelative,
|
||||
createHoldingsSectionWithOwnSupply,
|
||||
createGroupedHoldingsSection,
|
||||
createGroupedHoldingsSectionWithProfitLoss,
|
||||
createGroupedHoldingsSectionAddress,
|
||||
createGroupedHoldingsSectionAddressAmount,
|
||||
createGroupedHoldingsSectionWithRelative,
|
||||
@@ -68,18 +67,15 @@ import {
|
||||
createProfitabilitySectionWithProfitLoss,
|
||||
createProfitabilitySectionLongTerm,
|
||||
createGroupedProfitabilitySection,
|
||||
createGroupedProfitabilitySectionWithProfitLoss,
|
||||
createGroupedProfitabilitySectionWithNupl,
|
||||
createGroupedProfitabilitySectionWithInvestedCapitalPct,
|
||||
createGroupedProfitabilitySectionBasicWithInvestedCapitalPct,
|
||||
createGroupedProfitabilitySectionLongTerm,
|
||||
} from "./profitability.js";
|
||||
import {
|
||||
createActivitySection,
|
||||
createActivitySectionWithAdjusted,
|
||||
createActivitySectionWithActivity,
|
||||
createGroupedActivitySection,
|
||||
createGroupedActivitySectionWithAdjusted,
|
||||
createGroupedActivitySectionWithActivity,
|
||||
createActivitySectionMinimal,
|
||||
createGroupedActivitySectionMinimal,
|
||||
@@ -327,30 +323,6 @@ export function createAddressCohortFolder(cohort) {
|
||||
// Grouped Cohort Folder Builders
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @param {CohortGroupFull} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createGroupedCohortFolderFull({
|
||||
name,
|
||||
title: groupTitle,
|
||||
list,
|
||||
all,
|
||||
}) {
|
||||
const title = formatCohortTitle(groupTitle);
|
||||
return {
|
||||
name: name || "all",
|
||||
tree: [
|
||||
...createGroupedHoldingsSectionWithRelative({ list, all, title }),
|
||||
createGroupedValuationSectionWithOwnMarketCap({ list, all, title }),
|
||||
createGroupedPricesSectionFull({ list, all, title }),
|
||||
createGroupedCostBasisSectionWithPercentiles({ list, all, title }),
|
||||
createGroupedProfitabilitySectionWithNupl({ list, all, title }),
|
||||
createGroupedActivitySectionWithAdjusted({ list, all, title }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CohortGroupWithAdjusted} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
@@ -402,30 +374,6 @@ export function createGroupedCohortFolderWithNupl({
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CohortGroupLongTerm} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createGroupedCohortFolderLongTerm({
|
||||
name,
|
||||
title: groupTitle,
|
||||
list,
|
||||
all,
|
||||
}) {
|
||||
const title = formatCohortTitle(groupTitle);
|
||||
return {
|
||||
name: name || "all",
|
||||
tree: [
|
||||
...createGroupedHoldingsSectionWithRelative({ list, all, title }),
|
||||
createGroupedValuationSectionWithOwnMarketCap({ list, all, title }),
|
||||
createGroupedPricesSectionFull({ list, all, title }),
|
||||
createGroupedCostBasisSectionWithPercentiles({ list, all, title }),
|
||||
createGroupedProfitabilitySectionLongTerm({ list, all, title }),
|
||||
createGroupedActivitySection({ list, all, title }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CohortGroupAgeRange} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
@@ -564,29 +512,6 @@ export function createGroupedCohortFolderAddress({
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CohortGroupWithoutRelative} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createGroupedCohortFolderWithoutRelative({
|
||||
name,
|
||||
title: groupTitle,
|
||||
list,
|
||||
all,
|
||||
}) {
|
||||
const title = formatCohortTitle(groupTitle);
|
||||
return {
|
||||
name: name || "all",
|
||||
tree: [
|
||||
...createGroupedHoldingsSectionWithProfitLoss({ list, all, title }),
|
||||
createGroupedValuationSection({ list, all, title }),
|
||||
createGroupedPricesSection({ list, all, title }),
|
||||
createGroupedProfitabilitySectionWithProfitLoss({ list, all, title }),
|
||||
createGroupedActivitySectionMinimal({ list, all, title }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AddrCohortGroupObject} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
|
||||
@@ -26,7 +26,6 @@ import { colors } from "../../utils/colors.js";
|
||||
import { priceLine } from "../constants.js";
|
||||
import {
|
||||
mapCohortsWithAll,
|
||||
flatMapCohorts,
|
||||
flatMapCohortsWithAll,
|
||||
groupedWindowsCumulativeUsd,
|
||||
} from "../shared.js";
|
||||
@@ -35,14 +34,6 @@ import {
|
||||
// Core Series Builders
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @param {AnySeriesPattern} s
|
||||
* @param {Unit} unit
|
||||
* @returns {AnyFetchedSeriesBlueprint}
|
||||
*/
|
||||
function netBaseline(s, unit) {
|
||||
return baseline({ series: s, name: "Net", unit });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Unrealized P&L Builders
|
||||
@@ -110,18 +101,18 @@ function relPnlChart(profit, loss, name, title) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared unrealized base items
|
||||
* @param {AllRelativePattern | FullRelativePattern} u
|
||||
* Core unrealized items: Overview + Net + NUPL + Profit + Loss
|
||||
* @param {{ profit: { usd: AnySeriesPattern }, loss: { usd: AnySeriesPattern, negative: AnySeriesPattern }, netPnl: { usd: AnySeriesPattern }, nupl: NuplPattern }} u
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function unrealizedBase(u, title) {
|
||||
function unrealizedCore(u, title) {
|
||||
return [
|
||||
unrealizedOverview(u.profit, u.loss, u.netPnl.usd, title),
|
||||
{
|
||||
name: "Net",
|
||||
title: title("Net Unrealized P&L"),
|
||||
bottom: [netBaseline(u.netPnl.usd, Unit.usd)],
|
||||
bottom: [baseline({ series: u.netPnl.usd, name: "Net", unit: Unit.usd })],
|
||||
},
|
||||
{ name: "NUPL", title: title("NUPL"), bottom: nuplSeries(u.nupl) },
|
||||
{
|
||||
@@ -148,35 +139,42 @@ function unrealizedBase(u, title) {
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "% of Own P&L",
|
||||
title: title("Unrealized P&L (% of Own P&L)"),
|
||||
bottom: [
|
||||
...percentRatioBaseline({
|
||||
pattern: u.netPnl.toOwnGrossPnl,
|
||||
name: "Net",
|
||||
}),
|
||||
...percentRatio({
|
||||
pattern: u.profit.toOwnGrossPnl,
|
||||
name: "Profit",
|
||||
color: colors.profit,
|
||||
defaultActive: false,
|
||||
}),
|
||||
...percentRatio({
|
||||
pattern: u.loss.toOwnGrossPnl,
|
||||
name: "Loss",
|
||||
color: colors.loss,
|
||||
defaultActive: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* % of Own P&L chart
|
||||
* @param {AllRelativePattern | FullRelativePattern} u
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialChartOption}
|
||||
*/
|
||||
function ownPnlChart(u, title) {
|
||||
return {
|
||||
name: "% of Own P&L",
|
||||
title: title("Unrealized P&L (% of Own P&L)"),
|
||||
bottom: [
|
||||
...percentRatioBaseline({ pattern: u.netPnl.toOwnGrossPnl, name: "Net" }),
|
||||
...percentRatio({
|
||||
pattern: u.profit.toOwnGrossPnl,
|
||||
name: "Profit",
|
||||
color: colors.profit,
|
||||
defaultActive: false,
|
||||
}),
|
||||
...percentRatio({
|
||||
pattern: u.loss.toOwnGrossPnl,
|
||||
name: "Loss",
|
||||
color: colors.loss,
|
||||
defaultActive: false,
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {AllRelativePattern} u @param {(name: string) => string} title */
|
||||
function unrealizedTreeAll(u, title) {
|
||||
return [
|
||||
...unrealizedBase(u, title),
|
||||
...unrealizedCore(u, title),
|
||||
ownPnlChart(u, title),
|
||||
relPnlChart(u.profit.toMcap, u.loss.toMcap, "% of Market Cap", title),
|
||||
];
|
||||
}
|
||||
@@ -184,7 +182,8 @@ function unrealizedTreeAll(u, title) {
|
||||
/** @param {FullRelativePattern} u @param {(name: string) => string} title */
|
||||
function unrealizedTreeFull(u, title) {
|
||||
return [
|
||||
...unrealizedBase(u, title),
|
||||
...unrealizedCore(u, title),
|
||||
ownPnlChart(u, title),
|
||||
relPnlChart(u.profit.toMcap, u.loss.toMcap, "% of Market Cap", title),
|
||||
relPnlChart(
|
||||
u.profit.toOwnMcap,
|
||||
@@ -198,7 +197,8 @@ function unrealizedTreeFull(u, title) {
|
||||
/** @param {FullRelativePattern} u @param {(name: string) => string} title */
|
||||
function unrealizedTreeLongTerm(u, title) {
|
||||
return [
|
||||
...unrealizedBase(u, title),
|
||||
...unrealizedCore(u, title),
|
||||
ownPnlChart(u, title),
|
||||
{
|
||||
name: "% of Market Cap",
|
||||
title: title("Unrealized Loss (% of Market Cap)"),
|
||||
@@ -217,48 +217,6 @@ function unrealizedTreeLongTerm(u, title) {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Unrealized tree for mid-tier cohorts (AgeRange/MaxAge — profit/loss/net, no relative)
|
||||
* @param {BasicRelativePattern} u
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function unrealizedTreeMid(u, title) {
|
||||
return [
|
||||
unrealizedOverview(u.profit, u.loss, u.netPnl.usd, title),
|
||||
{
|
||||
name: "Net",
|
||||
title: title("Net Unrealized P&L"),
|
||||
bottom: [netBaseline(u.netPnl.usd, Unit.usd)],
|
||||
},
|
||||
{ name: "NUPL", title: title("NUPL"), bottom: nuplSeries(u.nupl) },
|
||||
{
|
||||
name: "Profit",
|
||||
title: title("Unrealized Profit"),
|
||||
bottom: [
|
||||
line({
|
||||
series: u.profit.usd,
|
||||
name: "Profit",
|
||||
color: colors.profit,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Loss",
|
||||
title: title("Unrealized Loss"),
|
||||
bottom: [
|
||||
line({
|
||||
series: u.loss.usd,
|
||||
name: "Loss",
|
||||
color: colors.loss,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Invested Capital, Sentiment, NUPL
|
||||
// ============================================================================
|
||||
@@ -401,11 +359,21 @@ function realizedNetFolder({ netPnl, title, extraChange = [] }) {
|
||||
}),
|
||||
],
|
||||
},
|
||||
{ ...sumsTreeBaseline({ windows: mapWindows(netPnl.delta.absolute, (c) => c.usd), title: title("Net Realized P&L Change"), unit: Unit.usd }), name: "Change" },
|
||||
{
|
||||
...sumsTreeBaseline({
|
||||
windows: mapWindows(netPnl.delta.absolute, (c) => c.usd),
|
||||
title: title("Net Realized P&L Change"),
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
name: "Change",
|
||||
},
|
||||
{
|
||||
name: "Growth Rate",
|
||||
tree: [
|
||||
...rollingPercentRatioTree({ windows: netPnl.delta.rate, title: title("Net Realized P&L Rate") }).tree,
|
||||
...rollingPercentRatioTree({
|
||||
windows: netPnl.delta.rate,
|
||||
title: title("Net Realized P&L Rate"),
|
||||
}).tree,
|
||||
...extraChange,
|
||||
],
|
||||
},
|
||||
@@ -885,7 +853,7 @@ export function createProfitabilitySectionWithInvestedCapitalPct({
|
||||
return {
|
||||
name: "Profitability",
|
||||
tree: [
|
||||
{ name: "Unrealized", tree: unrealizedTreeMid(u, title) },
|
||||
{ name: "Unrealized", tree: unrealizedCore(u, title) },
|
||||
realizedSubfolderMid(r, title),
|
||||
],
|
||||
};
|
||||
@@ -981,10 +949,26 @@ function groupedRealizedSubfolder(list, all, title) {
|
||||
return {
|
||||
name: "Realized",
|
||||
tree: [
|
||||
{ name: "Profit", tree: groupedWindowsCumulativeUsd({ list, all, title, metricTitle: "Realized Profit",
|
||||
getMetric: (c) => c.tree.realized.profit }) },
|
||||
{ name: "Loss", tree: groupedWindowsCumulativeUsd({ list, all, title, metricTitle: "Realized Loss",
|
||||
getMetric: (c) => c.tree.realized.loss }) },
|
||||
{
|
||||
name: "Profit",
|
||||
tree: groupedWindowsCumulativeUsd({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
metricTitle: "Realized Profit",
|
||||
getMetric: (c) => c.tree.realized.profit,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Loss",
|
||||
tree: groupedWindowsCumulativeUsd({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
metricTitle: "Realized Loss",
|
||||
getMetric: (c) => c.tree.realized.loss,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -1004,7 +988,12 @@ function groupedRealizedNetPnlDeltaItems(list, all, title) {
|
||||
name: w.name,
|
||||
title: title(`Net Realized P&L Change (${w.title})`),
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({ series: tree.realized.netPnl.delta.absolute[w.key].usd, name, color, unit: Unit.usd }),
|
||||
baseline({
|
||||
series: tree.realized.netPnl.delta.absolute[w.key].usd,
|
||||
name,
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
},
|
||||
@@ -1014,7 +1003,11 @@ function groupedRealizedNetPnlDeltaItems(list, all, title) {
|
||||
name: w.name,
|
||||
title: title(`Net Realized P&L Rate (${w.title})`),
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
percentRatioBaseline({ pattern: tree.realized.netPnl.delta.rate[w.key], name, color }),
|
||||
percentRatioBaseline({
|
||||
pattern: tree.realized.netPnl.delta.rate[w.key],
|
||||
name,
|
||||
color,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
},
|
||||
@@ -1212,116 +1205,6 @@ function groupedUnrealizedWithMarketCap(list, all, title) {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Grouped unrealized for LongTerm: Net → NUPL → Profit → Loss → Relative(Own P&L, Market Cap, Own Mcap)
|
||||
* @param {readonly CohortLongTerm[]} list
|
||||
* @param {CohortAll} all
|
||||
* @param {(name: string) => string} title
|
||||
* @returns {PartialOptionsTree}
|
||||
*/
|
||||
function groupedUnrealizedLongTerm(list, all, title) {
|
||||
return [
|
||||
...groupedUnrealizedMid(list, all, title),
|
||||
{
|
||||
name: "Relative",
|
||||
tree: [
|
||||
{
|
||||
name: "Own P&L",
|
||||
tree: [
|
||||
{
|
||||
name: "Net",
|
||||
title: title("Net Unrealized P&L (% of Own P&L)"),
|
||||
bottom: flatMapCohortsWithAll(
|
||||
list,
|
||||
all,
|
||||
({ name, color, tree }) =>
|
||||
percentRatioBaseline({
|
||||
pattern: tree.unrealized.netPnl.toOwnGrossPnl,
|
||||
name,
|
||||
color,
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Profit",
|
||||
title: title("Unrealized Profit (% of Own P&L)"),
|
||||
bottom: flatMapCohortsWithAll(
|
||||
list,
|
||||
all,
|
||||
({ name, color, tree }) =>
|
||||
percentRatio({
|
||||
pattern: tree.unrealized.profit.toOwnGrossPnl,
|
||||
name,
|
||||
color,
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Loss",
|
||||
title: title("Unrealized Loss (% of Own P&L)"),
|
||||
bottom: flatMapCohortsWithAll(
|
||||
list,
|
||||
all,
|
||||
({ name, color, tree }) =>
|
||||
percentRatio({
|
||||
pattern: tree.unrealized.loss.toOwnGrossPnl,
|
||||
name,
|
||||
color,
|
||||
}),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Market Cap",
|
||||
title: title("Unrealized Loss (% of Market Cap)"),
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
percentRatio({ pattern: tree.unrealized.loss.toMcap, name, color }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Own Market Cap",
|
||||
tree: [
|
||||
{
|
||||
name: "Net",
|
||||
title: title("Net Unrealized P&L (% of Own Market Cap)"),
|
||||
bottom: flatMapCohorts(list, ({ name, color, tree }) =>
|
||||
percentRatioBaseline({
|
||||
pattern: tree.unrealized.netPnl.toOwnMcap,
|
||||
name,
|
||||
color,
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Profit",
|
||||
title: title("Unrealized Profit (% of Own Market Cap)"),
|
||||
bottom: flatMapCohorts(list, ({ name, color, tree }) =>
|
||||
percentRatio({
|
||||
pattern: tree.unrealized.profit.toOwnMcap,
|
||||
name,
|
||||
color,
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Loss",
|
||||
title: title("Unrealized Loss (% of Own Market Cap)"),
|
||||
bottom: flatMapCohorts(list, ({ name, color, tree }) =>
|
||||
percentRatio({
|
||||
pattern: tree.unrealized.loss.toOwnMcap,
|
||||
name,
|
||||
color,
|
||||
}),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Grouped sentiment (full unrealized only)
|
||||
* @param {readonly (CohortAll | CohortFull | CohortLongTerm)[]} list
|
||||
@@ -1501,23 +1384,3 @@ export function createGroupedProfitabilitySectionWithNupl({
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Grouped section for LongTerm cohorts
|
||||
* @param {{ list: readonly CohortLongTerm[], all: CohortAll, title: (name: string) => string }} args
|
||||
* @returns {PartialOptionsGroup}
|
||||
*/
|
||||
export function createGroupedProfitabilitySectionLongTerm({
|
||||
list,
|
||||
all,
|
||||
title,
|
||||
}) {
|
||||
return {
|
||||
name: "Profitability",
|
||||
tree: [
|
||||
{ name: "Unrealized", tree: groupedUnrealizedLongTerm(list, all, title) },
|
||||
groupedRealizedSubfolderFull(list, all, title),
|
||||
groupedSentiment(list, all, title),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,24 +67,15 @@
|
||||
* @typedef {Brk.AnySeriesEndpoint} AnySeriesEndpoint
|
||||
* @typedef {Brk.AnySeriesData} AnySeriesData
|
||||
* Relative patterns by capability:
|
||||
* - BasicRelativePattern: minimal relative (investedCapitalIn*Pct, supplyIn*RelToOwnSupply only)
|
||||
* - GlobalRelativePattern: has RelToMarketCap series (netUnrealizedPnlRelToMarketCap, etc)
|
||||
* - OwnRelativePattern: has RelToOwnMarketCap series (netUnrealizedPnlRelToOwnMarketCap, etc)
|
||||
* - FullRelativePattern: has BOTH RelToMarketCap AND RelToOwnMarketCap
|
||||
* Unrealized patterns by capability level
|
||||
* @typedef {Brk.LossNetNuplProfitPattern} BasicRelativePattern
|
||||
* @typedef {Brk.LossNetNuplProfitPattern} GlobalRelativePattern
|
||||
* @typedef {Brk.GrossInvestedInvestorLossNetNuplProfitSentimentPattern2} OwnRelativePattern
|
||||
* @typedef {Brk.GrossInvestedInvestorLossNetNuplProfitSentimentPattern2} FullRelativePattern
|
||||
* @typedef {Brk.GrossInvestedInvestorLossNetNuplProfitSentimentPattern2} UnrealizedPattern
|
||||
*
|
||||
* Profitability bucket pattern (supply + realized_cap + nupl)
|
||||
* @typedef {Brk.NuplRealizedSupplyPattern} RealizedSupplyPattern
|
||||
*
|
||||
* Realized patterns
|
||||
* Realized pattern (full: cap + gross + investor + loss + mvrv + net + peak + price + profit + sell + sopr)
|
||||
* @typedef {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern} RealizedPattern
|
||||
* @typedef {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern} RealizedPattern2
|
||||
* @typedef {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern} RealizedPattern3
|
||||
* @typedef {Brk.CapGrossInvestorLossMvrvNetPeakPriceProfitSellSoprPattern} RealizedPattern4
|
||||
*
|
||||
* Transfer volume pattern (block + cumulative + inProfit/inLoss + sum windows)
|
||||
* @typedef {Brk.AverageBlockCumulativeInSumPattern} TransferVolumePattern
|
||||
@@ -103,7 +94,7 @@
|
||||
* @typedef {Brk.SeriesTree_Cohorts_Utxo_Lth_Realized} LthRealizedPattern
|
||||
*
|
||||
* Net PnL pattern with change (base + change + cumulative + delta + rel + sum)
|
||||
* @typedef {Brk.BlockChangeCumulativeDeltaSumToPattern} NetPnlFullPattern
|
||||
* @typedef {Brk.BlockChangeCumulativeDeltaSumPattern} NetPnlFullPattern
|
||||
*
|
||||
* Net PnL basic pattern (base + cumulative + delta + sum)
|
||||
* @typedef {Brk.BlockCumulativeDeltaSumPattern} NetPnlBasicPattern
|
||||
@@ -135,13 +126,9 @@
|
||||
* @typedef {Brk._1m1w1y24hPattern7} SellSideRiskPattern
|
||||
*/
|
||||
/**
|
||||
* Stats pattern: average, min, max, percentiles (height-only indexes, NO base)
|
||||
* Stats pattern: min, max, median, percentiles
|
||||
* @typedef {Brk.MaxMedianMinPct10Pct25Pct75Pct90Pattern<number>} StatsPattern
|
||||
*/
|
||||
/**
|
||||
* Base stats pattern: average, min, max, percentiles
|
||||
* @typedef {Brk.MaxMedianMinPct10Pct25Pct75Pct90Pattern<number>} BaseStatsPattern
|
||||
*/
|
||||
/**
|
||||
* Full stats pattern: cumulative, sum, average, min, max, percentiles + rolling
|
||||
* @typedef {Brk.AverageBlockCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern} FullStatsPattern
|
||||
@@ -150,14 +137,6 @@
|
||||
* Aggregated pattern: cumulative + rolling (with distribution stats) + sum (no base)
|
||||
* @typedef {Brk.CumulativeRollingSumPattern} AggregatedPattern
|
||||
*/
|
||||
/**
|
||||
* Sum stats pattern: cumulative, sum, average, min, max, percentiles + rolling (same as FullStatsPattern)
|
||||
* @typedef {Brk.AverageBlockCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern} SumStatsPattern
|
||||
*/
|
||||
/**
|
||||
* Full stats pattern for Bitcoin (non-generic variant) - same as FullStatsPattern
|
||||
* @typedef {Brk.AverageBlockCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern} BtcFullStatsPattern
|
||||
*/
|
||||
/**
|
||||
* Count pattern: height, cumulative, and rolling sum windows
|
||||
* @template T
|
||||
@@ -170,8 +149,8 @@
|
||||
* @typedef {Omit<Brk.AverageBlockCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern, 'block'>} FullPerBlockPattern
|
||||
*/
|
||||
/**
|
||||
* Any stats pattern union - patterns with sum/cumulative + percentiles
|
||||
* @typedef {FullStatsPattern | BtcFullStatsPattern} AnyStatsPattern
|
||||
* Any stats pattern union
|
||||
* @typedef {FullStatsPattern} AnyStatsPattern
|
||||
*/
|
||||
/**
|
||||
* Distribution stats: min, max, median, pct10/25/75/90
|
||||
@@ -190,8 +169,7 @@
|
||||
* @typedef {Brk.SeriesTree_Market_MovingAverage} MarketMovingAverage
|
||||
* @typedef {Brk.SeriesTree_Market_Dca} MarketDca
|
||||
* @typedef {Brk._10y2y3y4y5y6y8yPattern} PeriodCagrPattern
|
||||
* Full stats pattern union (both generic and non-generic variants)
|
||||
* @typedef {FullStatsPattern | BtcFullStatsPattern} AnyFullStatsPattern
|
||||
* @typedef {FullStatsPattern} AnyFullStatsPattern
|
||||
*
|
||||
* DCA period keys - derived from pattern types
|
||||
* @typedef {keyof Brk._10y2y3y4y5y6y8yPattern} LongPeriodKey
|
||||
@@ -204,18 +182,14 @@
|
||||
* @typedef {UtxoCohortPattern | AddrCohortPattern} CohortPattern
|
||||
*
|
||||
* Relative pattern capability types
|
||||
* @typedef {GlobalRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithMarketCap
|
||||
* @typedef {OwnRelativePattern | FullRelativePattern} RelativeWithOwnMarketCap
|
||||
* @typedef {OwnRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithOwnPnl
|
||||
* @typedef {GlobalRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithNupl
|
||||
* @typedef {BasicRelativePattern | GlobalRelativePattern | OwnRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithInvestedCapitalPct
|
||||
* @typedef {BasicRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithMarketCap
|
||||
* @typedef {FullRelativePattern | AllRelativePattern} RelativeWithOwnMarketCap
|
||||
* @typedef {FullRelativePattern | AllRelativePattern} RelativeWithOwnPnl
|
||||
* @typedef {BasicRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithNupl
|
||||
* @typedef {BasicRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithInvestedCapitalPct
|
||||
*
|
||||
* Realized pattern capability types
|
||||
* RealizedWithExtras: patterns with realizedCapRelToOwnMarketCap + realizedProfitToLossRatio
|
||||
* @typedef {RealizedPattern2 | RealizedPattern3} RealizedWithExtras
|
||||
*
|
||||
* Any realized pattern (all have sellSideRiskRatio, valueCreated, valueDestroyed, etc.)
|
||||
* @typedef {RealizedPattern | RealizedPattern2 | RealizedPattern3 | RealizedPattern4} AnyRealizedPattern
|
||||
* @typedef {RealizedPattern} AnyRealizedPattern
|
||||
*
|
||||
* Capability-based pattern groupings (patterns that have specific properties)
|
||||
* @typedef {AllUtxoPattern | AgeRangePattern | UtxoAmountPattern} PatternWithRealizedPrice
|
||||
|
||||
Reference in New Issue
Block a user