global: metrics -> series rename

This commit is contained in:
nym21
2026-03-16 14:31:50 +01:00
parent bc06567bb0
commit ae2dd43073
95 changed files with 8907 additions and 8415 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
//! Common prefix/suffix detection for metric names.
//! Common prefix/suffix detection for series names.
//!
//! This module provides utilities to find common prefixes and suffixes
//! among metric names, which is used to detect pattern mode (suffix vs prefix).
//! among series names, which is used to detect pattern mode (suffix vs prefix).
/// Find the longest common prefix among all strings.
/// Returns the prefix WITH trailing underscore if found at word boundary.
+11 -11
View File
@@ -179,7 +179,7 @@ fn collect_instance_analyses(
) -> Option<String> {
match node {
TreeNode::Leaf(leaf) => {
// Leaves return their metric name as the base
// Leaves return their series name as the base
Some(leaf.name().to_string())
}
TreeNode::Branch(children) => {
@@ -213,7 +213,7 @@ fn collect_instance_analyses(
if all_empty {
// All-empty case: all children returned the same base.
// Use shortest leaf to derive field_parts for fields whose key
// matches the metric suffix (e.g., pct1 → suffix "pct1").
// matches the series suffix (e.g., pct1 → suffix "pct1").
let prefix = format!("{}_", analysis.base);
let mut any_filled = false;
for (field_name, child_node) in children {
@@ -234,7 +234,7 @@ fn collect_instance_analyses(
// If no fields could be filled and all children are the same type,
// mark as outlier so the tree inlines instead of using identity
// (handles patterns like period windows where field keys differ
// from metric suffixes: all/_4y don't match 0sd/0sd_4y).
// from series suffixes: all/_4y don't match 0sd/0sd_4y).
// When children are different types (like absolute/rate), identity
// is correct — each child handles its own suffixes internally.
if !any_filled {
@@ -462,7 +462,7 @@ fn analyze_instance(child_bases: &BTreeMap<String, String>) -> InstanceAnalysis
// No common prefix or suffix - use empty base so _m(base, relative) returns just the relative.
// No common prefix or suffix — outlier naming (e.g., sopr/asopr/adj_).
// Children have unrelated metric names that can't be parameterized.
// Children have unrelated series names that can't be parameterized.
let field_parts = child_bases
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
@@ -561,7 +561,7 @@ mod tests {
#[test]
fn test_analyze_instance_prefix_mode() {
// Period-prefixed metrics like "1y_lump_sum_stack", "1m_lump_sum_stack"
// Period-prefixed series like "1y_lump_sum_stack", "1m_lump_sum_stack"
// share a common suffix "_lump_sum_stack" with different period prefixes
let mut child_bases = BTreeMap::new();
child_bases.insert("_1y".to_string(), "1y_lump_sum_stack".to_string());
@@ -721,7 +721,7 @@ mod tests {
];
let instance1 = InstanceAnalysis {
base: "metric_a".to_string(),
base: "series_a".to_string(),
field_parts: [
("max".to_string(), "max".to_string()),
("min".to_string(), "min".to_string()),
@@ -732,7 +732,7 @@ mod tests {
has_outlier: false,
};
let instance2 = InstanceAnalysis {
base: "metric_b".to_string(),
base: "series_b".to_string(),
field_parts: [
("max".to_string(), "max".to_string()),
("min".to_string(), "min".to_string()),
@@ -882,7 +882,7 @@ mod tests {
];
// SOPR case: one instance has outlier naming (no common prefix)
let normal = InstanceAnalysis {
base: "metric".into(),
base: "series".into(),
field_parts: [("ratio".into(), "ratio".into()), ("value".into(), "value".into())].into_iter().collect(),
is_suffix_mode: true, has_outlier: false,
};
@@ -1067,11 +1067,11 @@ mod tests {
// 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".
use brk_types::{MetricLeaf, MetricLeafWithSchema, TreeNode};
use brk_types::{SeriesLeaf, SeriesLeafWithSchema, TreeNode};
fn leaf(name: &str) -> TreeNode {
TreeNode::Leaf(MetricLeafWithSchema::new(
MetricLeaf::new(name.into(), "f32".into(), std::collections::BTreeSet::new()),
TreeNode::Leaf(SeriesLeafWithSchema::new(
SeriesLeaf::new(name.into(), "f32".into(), std::collections::BTreeSet::new()),
serde_json::Value::Null,
))
}
+12 -12
View File
@@ -64,7 +64,7 @@ pub fn get_node_fields(
fields
}
/// Detect index patterns (sets of indexes that appear together on metrics).
/// Detect index patterns (sets of indexes that appear together on series).
pub fn detect_index_patterns(tree: &TreeNode) -> Vec<IndexSetPattern> {
let mut unique_index_sets: BTreeSet<BTreeSet<Index>> = BTreeSet::new();
collect_index_sets_from_tree(tree, &mut unique_index_sets);
@@ -85,7 +85,7 @@ pub fn detect_index_patterns(tree: &TreeNode) -> Vec<IndexSetPattern> {
.into_iter()
.enumerate()
.map(|(i, indexes)| IndexSetPattern {
name: format!("MetricPattern{}", i + 1),
name: format!("SeriesPattern{}", i + 1),
indexes,
})
.collect()
@@ -147,7 +147,7 @@ impl PatternBaseResult {
}
}
/// Get the metric base for a pattern instance by analyzing direct children.
/// Get the series base for a pattern instance by analyzing direct children.
///
/// Uses the shortest leaf names from direct children to find common prefix/suffix.
///
@@ -194,7 +194,7 @@ pub fn get_pattern_instance_base(node: &TreeNode) -> PatternBaseResult {
}
// Fallback: no common prefix/suffix found - this is a root-level pattern
// Return empty base so metric names are used directly
// Return empty base so series names are used directly
PatternBaseResult::empty()
}
@@ -346,15 +346,15 @@ pub fn get_fields_with_child_info(
#[cfg(test)]
mod tests {
use super::*;
use brk_types::{MetricLeaf, MetricLeafWithSchema, TreeNode};
use brk_types::{SeriesLeaf, SeriesLeafWithSchema, TreeNode};
fn make_leaf(name: &str) -> TreeNode {
let leaf = MetricLeaf {
let leaf = SeriesLeaf {
name: name.to_string(),
kind: "TestType".to_string(),
indexes: BTreeSet::new(),
};
TreeNode::Leaf(MetricLeafWithSchema::new(leaf, serde_json::json!({})))
TreeNode::Leaf(SeriesLeafWithSchema::new(leaf, serde_json::json!({})))
}
fn make_branch(children: Vec<(&str, TreeNode)>) -> TreeNode {
@@ -390,7 +390,7 @@ mod tests {
#[test]
fn test_get_pattern_instance_base_without_base_field() {
// Simulates weight tree: NO base field, only suffixed metrics
// Simulates weight tree: NO base field, only suffixed series
let tree = make_branch(vec![
(
"average",
@@ -447,7 +447,7 @@ mod tests {
#[test]
fn test_get_pattern_instance_base_with_mismatched_base_name() {
// Simulates the actual bug: indexed tree's "base" field has name "weight"
// but computed tree's derived metrics use "block_weight_*" prefix.
// but computed tree's derived series use "block_weight_*" prefix.
// After tree merge, we get a base field with mismatched naming.
let tree = make_branch(vec![
("base", make_leaf("weight")), // Outlier - doesn't match pattern
@@ -466,11 +466,11 @@ mod tests {
#[test]
fn test_get_pattern_instance_base_root_level_no_common_pattern() {
// Simulates root-level pattern with metrics that have no common prefix/suffix.
// Simulates root-level pattern with series that have no common prefix/suffix.
// These names have no shared prefix or suffix, even when excluding any one.
// In this case, we should return empty base so metric names are used directly.
// In this case, we should return empty base so series names are used directly.
let tree = make_branch(vec![
("alpha", make_leaf("foo_metric")),
("alpha", make_leaf("foo_series")),
("beta", make_leaf("bar_value")),
("gamma", make_leaf("baz_count")),
]);
+10 -10
View File
@@ -6,7 +6,7 @@
use std::fmt::Write;
use brk_types::MetricLeafWithSchema;
use brk_types::SeriesLeafWithSchema;
use crate::{ClientMetadata, LanguageSyntax, PatternBaseResult, PatternField, PatternMode, StructuralPattern};
@@ -56,7 +56,7 @@ fn compute_parameterized_value<S: LanguageSyntax>(
syntax.constructor(&accessor.name, &path_expr)
} else if field.is_leaf() {
panic!(
"Field '{}' has no matching index accessor. All metrics must be indexed.",
"Field '{}' has no matching index accessor. All series must be indexed.",
field.name
)
} else {
@@ -66,7 +66,7 @@ fn compute_parameterized_value<S: LanguageSyntax>(
/// Generate a parameterized field for a pattern factory.
///
/// Used for pattern instances where fields build metric names from an accumulated base.
/// Used for pattern instances where fields build series names from an accumulated base.
pub fn generate_parameterized_field<S: LanguageSyntax>(
output: &mut String,
syntax: &S,
@@ -135,10 +135,10 @@ pub fn generate_tree_node_field<S: LanguageSyntax>(
.unwrap();
}
/// Generate a leaf field using the actual metric name from the TreeNode::Leaf.
/// Generate a leaf field using the actual series name from the TreeNode::Leaf.
///
/// This is the shared implementation for all language backends. It uses
/// `leaf.name()` directly to get the correct metric name, avoiding any
/// `leaf.name()` directly to get the correct series name, avoiding any
/// path concatenation that could produce incorrect names.
///
/// # Arguments
@@ -146,7 +146,7 @@ pub fn generate_tree_node_field<S: LanguageSyntax>(
/// * `syntax` - The language syntax implementation
/// * `client_expr` - The client expression (e.g., "client.clone()", "this", "client")
/// * `tree_field_name` - The field name from the tree structure
/// * `leaf` - The Leaf node containing the actual metric name and indexes
/// * `leaf` - The Leaf node containing the actual series name and indexes
/// * `metadata` - Client metadata for looking up index patterns
/// * `indent` - Indentation string
pub fn generate_leaf_field<S: LanguageSyntax>(
@@ -154,7 +154,7 @@ pub fn generate_leaf_field<S: LanguageSyntax>(
syntax: &S,
client_expr: &str,
tree_field_name: &str,
leaf: &MetricLeafWithSchema,
leaf: &SeriesLeafWithSchema,
metadata: &ClientMetadata,
indent: &str,
) {
@@ -163,18 +163,18 @@ pub fn generate_leaf_field<S: LanguageSyntax>(
.find_index_set_pattern(leaf.indexes())
.unwrap_or_else(|| {
panic!(
"Metric '{}' has no matching index pattern. All metrics must be indexed.",
"Series '{}' has no matching index pattern. All series must be indexed.",
leaf.name()
)
});
let type_ann = metadata.field_type_annotation_from_leaf(leaf, syntax.generic_syntax());
let metric_name = syntax.string_literal(leaf.name());
let series_name = syntax.string_literal(leaf.name());
let value = format!(
"{}({}, {})",
syntax.constructor_name(&accessor.name),
client_expr,
metric_name
series_name
);
writeln!(
@@ -121,15 +121,15 @@ function dateToIndex(index, d) {{
}}
/**
* Wrap raw metric data with helper methods.
* Wrap raw series data with helper methods.
* @template T
* @param {{MetricData<T>}} raw - Raw JSON response
* @returns {{DateMetricData<T>}}
* @param {{SeriesData<T>}} raw - Raw JSON response
* @returns {{DateSeriesData<T>}}
*/
function _wrapMetricData(raw) {{
function _wrapSeriesData(raw) {{
const {{ index, start, end, data }} = raw;
const _dateBased = _DATE_INDEXES.has(index);
return /** @type {{DateMetricData<T>}} */ ({{
return /** @type {{DateSeriesData<T>}} */ ({{
...raw,
isDateBased: _dateBased,
indexes() {{
@@ -156,7 +156,7 @@ function _wrapMetricData(raw) {{
*[Symbol.iterator]() {{
for (let i = 0; i < data.length; i++) yield /** @type {{[number, T]}} */ ([start + i, data[i]]);
}},
// DateMetricData methods (only meaningful for date-based indexes)
// DateSeriesData methods (only meaningful for date-based indexes)
dates() {{
/** @type {{globalThis.Date[]}} */
const result = [];
@@ -180,47 +180,47 @@ function _wrapMetricData(raw) {{
/**
* @template T
* @typedef {{Object}} MetricDataBase
* @property {{number}} version - Version of the metric data
* @typedef {{Object}} SeriesDataBase
* @property {{number}} version - Version of the series data
* @property {{Index}} index - The index type used for this query
* @property {{string}} type - Value type (e.g. "f32", "u64", "Sats")
* @property {{number}} total - Total number of data points
* @property {{number}} start - Start index (inclusive)
* @property {{number}} end - End index (exclusive)
* @property {{string}} stamp - ISO 8601 timestamp of when the response was generated
* @property {{T[]}} data - The metric data
* @property {{boolean}} isDateBased - Whether this metric uses a date-based index
* @property {{T[]}} data - The series data
* @property {{boolean}} isDateBased - Whether this series uses a date-based index
* @property {{() => number[]}} indexes - Get index numbers
* @property {{() => number[]}} keys - Get keys as index numbers (alias for indexes)
* @property {{() => Array<[number, T]>}} entries - Get [index, value] pairs
* @property {{() => Map<number, T>}} toMap - Convert to Map<index, value>
*/
/** @template T @typedef {{MetricDataBase<T> & Iterable<[number, T]>}} MetricData */
/** @template T @typedef {{SeriesDataBase<T> & Iterable<[number, T]>}} SeriesData */
/**
* @template T
* @typedef {{Object}} DateMetricDataExtras
* @typedef {{Object}} DateSeriesDataExtras
* @property {{() => globalThis.Date[]}} dates - Get dates for each data point
* @property {{() => Array<[globalThis.Date, T]>}} dateEntries - Get [date, value] pairs
* @property {{() => Map<globalThis.Date, T>}} toDateMap - Convert to Map<date, value>
*/
/** @template T @typedef {{MetricData<T> & DateMetricDataExtras<T>}} DateMetricData */
/** @typedef {{MetricData<any>}} AnyMetricData */
/** @template T @typedef {{SeriesData<T> & DateSeriesDataExtras<T>}} DateSeriesData */
/** @typedef {{SeriesData<any>}} AnySeriesData */
/** @template T @typedef {{(onfulfilled?: (value: MetricData<T>) => any, onrejected?: (reason: Error) => never) => Promise<MetricData<T>>}} Thenable */
/** @template T @typedef {{(onfulfilled?: (value: DateMetricData<T>) => any, onrejected?: (reason: Error) => never) => Promise<DateMetricData<T>>}} DateThenable */
/** @template T @typedef {{(onfulfilled?: (value: SeriesData<T>) => any, onrejected?: (reason: Error) => never) => Promise<SeriesData<T>>}} Thenable */
/** @template T @typedef {{(onfulfilled?: (value: DateSeriesData<T>) => any, onrejected?: (reason: Error) => never) => Promise<DateSeriesData<T>>}} DateThenable */
/**
* @template T
* @typedef {{Object}} MetricEndpointBuilder
* @typedef {{Object}} SeriesEndpointBuilder
* @property {{(index: number) => SingleItemBuilder<T>}} get - Get single item at index
* @property {{(start?: number, end?: number) => RangeBuilder<T>}} slice - Slice by index
* @property {{(n: number) => RangeBuilder<T>}} first - Get first n items
* @property {{(n: number) => RangeBuilder<T>}} last - Get last n items
* @property {{(n: number) => SkippedBuilder<T>}} skip - Skip first n items, chain with take()
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch all data
* @property {{(onUpdate?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch all data
* @property {{() => Promise<string>}} fetchCsv - Fetch all data as CSV
* @property {{Thenable<T>}} then - Thenable (await endpoint)
* @property {{string}} path - The endpoint path
@@ -228,79 +228,79 @@ function _wrapMetricData(raw) {{
/**
* @template T
* @typedef {{Object}} DateMetricEndpointBuilder
* @typedef {{Object}} DateSeriesEndpointBuilder
* @property {{(index: number | globalThis.Date) => DateSingleItemBuilder<T>}} get - Get single item at index or Date
* @property {{(start?: number | globalThis.Date, end?: number | globalThis.Date) => DateRangeBuilder<T>}} slice - Slice by index or Date
* @property {{(n: number) => DateRangeBuilder<T>}} first - Get first n items
* @property {{(n: number) => DateRangeBuilder<T>}} last - Get last n items
* @property {{(n: number) => DateSkippedBuilder<T>}} skip - Skip first n items, chain with take()
* @property {{(onUpdate?: (value: DateMetricData<T>) => void) => Promise<DateMetricData<T>>}} fetch - Fetch all data
* @property {{(onUpdate?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch all data
* @property {{() => Promise<string>}} fetchCsv - Fetch all data as CSV
* @property {{DateThenable<T>}} then - Thenable (await endpoint)
* @property {{string}} path - The endpoint path
*/
/** @typedef {{MetricEndpointBuilder<any>}} AnyMetricEndpointBuilder */
/** @typedef {{SeriesEndpointBuilder<any>}} AnySeriesEndpointBuilder */
/** @template T @typedef {{Object}} SingleItemBuilder
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch the item
* @property {{(onUpdate?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch the item
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} DateSingleItemBuilder
* @property {{(onUpdate?: (value: DateMetricData<T>) => void) => Promise<DateMetricData<T>>}} fetch - Fetch the item
* @property {{(onUpdate?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch the item
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{DateThenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} SkippedBuilder
* @property {{(n: number) => RangeBuilder<T>}} take - Take n items after skipped position
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch from skipped position to end
* @property {{(onUpdate?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch from skipped position to end
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} DateSkippedBuilder
* @property {{(n: number) => DateRangeBuilder<T>}} take - Take n items after skipped position
* @property {{(onUpdate?: (value: DateMetricData<T>) => void) => Promise<DateMetricData<T>>}} fetch - Fetch from skipped position to end
* @property {{(onUpdate?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch from skipped position to end
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{DateThenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} RangeBuilder
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch the range
* @property {{(onUpdate?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch the range
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} DateRangeBuilder
* @property {{(onUpdate?: (value: DateMetricData<T>) => void) => Promise<DateMetricData<T>>}} fetch - Fetch the range
* @property {{(onUpdate?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch the range
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{DateThenable<T>}} then - Thenable
*/
/**
* @template T
* @typedef {{Object}} MetricPattern
* @property {{string}} name - The metric name
* @property {{Readonly<Partial<Record<Index, MetricEndpointBuilder<T>>>>}} by - Index endpoints as lazy getters
* @typedef {{Object}} SeriesPattern
* @property {{string}} name - The series name
* @property {{Readonly<Partial<Record<Index, SeriesEndpointBuilder<T>>>>}} by - Index endpoints as lazy getters
* @property {{() => readonly Index[]}} indexes - Get the list of available indexes
* @property {{(index: Index) => MetricEndpointBuilder<T>|undefined}} get - Get an endpoint for a specific index
* @property {{(index: Index) => SeriesEndpointBuilder<T>|undefined}} get - Get an endpoint for a specific index
*/
/** @typedef {{MetricPattern<any>}} AnyMetricPattern */
/** @typedef {{SeriesPattern<any>}} AnySeriesPattern */
/**
* Create a metric endpoint builder with typestate pattern.
* Create a series endpoint builder with typestate pattern.
* @template T
* @param {{BrkClientBase}} client
* @param {{string}} name - The metric vec name
* @param {{string}} name - The series vec name
* @param {{Index}} index - The index name
* @returns {{DateMetricEndpointBuilder<T>}}
* @returns {{DateSeriesEndpointBuilder<T>}}
*/
function _endpoint(client, name, index) {{
const p = `/api/metric/${{name}}/${{index}}`;
const p = `/api/series/${{name}}/${{index}}`;
/**
* @param {{number}} [start]
@@ -323,7 +323,7 @@ function _endpoint(client, name, index) {{
* @returns {{DateRangeBuilder<T>}}
*/
const rangeBuilder = (start, end) => ({{
fetch(onUpdate) {{ return client._fetchMetricData(buildPath(start, end), onUpdate); }},
fetch(onUpdate) {{ return client._fetchSeriesData(buildPath(start, end), onUpdate); }},
fetchCsv() {{ return client.getText(buildPath(start, end, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
@@ -333,7 +333,7 @@ function _endpoint(client, name, index) {{
* @returns {{DateSingleItemBuilder<T>}}
*/
const singleItemBuilder = (idx) => ({{
fetch(onUpdate) {{ return client._fetchMetricData(buildPath(idx, idx + 1), onUpdate); }},
fetch(onUpdate) {{ return client._fetchSeriesData(buildPath(idx, idx + 1), onUpdate); }},
fetchCsv() {{ return client.getText(buildPath(idx, idx + 1, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
@@ -344,12 +344,12 @@ function _endpoint(client, name, index) {{
*/
const skippedBuilder = (start) => ({{
take(n) {{ return rangeBuilder(start, start + n); }},
fetch(onUpdate) {{ return client._fetchMetricData(buildPath(start, undefined), onUpdate); }},
fetch(onUpdate) {{ return client._fetchSeriesData(buildPath(start, undefined), onUpdate); }},
fetchCsv() {{ return client.getText(buildPath(start, undefined, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
/** @type {{DateMetricEndpointBuilder<T>}} */
/** @type {{DateSeriesEndpointBuilder<T>}} */
const endpoint = {{
get(idx) {{ if (idx instanceof Date) idx = dateToIndex(index, idx); return singleItemBuilder(idx); }},
slice(start, end) {{
@@ -360,7 +360,7 @@ function _endpoint(client, name, index) {{
first(n) {{ return rangeBuilder(undefined, n); }},
last(n) {{ return n === 0 ? rangeBuilder(undefined, 0) : rangeBuilder(-n, undefined); }},
skip(n) {{ return skippedBuilder(n); }},
fetch(onUpdate) {{ return client._fetchMetricData(buildPath(), onUpdate); }},
fetch(onUpdate) {{ return client._fetchSeriesData(buildPath(), onUpdate); }},
fetchCsv() {{ return client.getText(buildPath(undefined, undefined, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
get path() {{ return p; }},
@@ -466,29 +466,29 @@ class BrkClientBase {{
}}
/**
* Fetch metric data and wrap with helper methods (internal)
* Fetch series data and wrap with helper methods (internal)
* @template T
* @param {{string}} path
* @param {{(value: DateMetricData<T>) => void}} [onUpdate]
* @returns {{Promise<DateMetricData<T>>}}
* @param {{(value: DateSeriesData<T>) => void}} [onUpdate]
* @returns {{Promise<DateSeriesData<T>>}}
*/
async _fetchMetricData(path, onUpdate) {{
const wrappedOnUpdate = onUpdate ? (/** @type {{MetricData<T>}} */ raw) => onUpdate(_wrapMetricData(raw)) : undefined;
async _fetchSeriesData(path, onUpdate) {{
const wrappedOnUpdate = onUpdate ? (/** @type {{SeriesData<T>}} */ raw) => onUpdate(_wrapSeriesData(raw)) : undefined;
const raw = await this.getJson(path, wrappedOnUpdate);
return _wrapMetricData(raw);
return _wrapSeriesData(raw);
}}
}}
/**
* Build metric name with suffix.
* Build series name with suffix.
* @param {{string}} acc - Accumulated prefix
* @param {{string}} s - Metric suffix
* @param {{string}} s - Series suffix
* @returns {{string}}
*/
const _m = (acc, s) => s ? (acc ? `${{acc}}_${{s}}` : s) : acc;
/**
* Build metric name with prefix.
* Build series name with prefix.
* @param {{string}} prefix - Prefix to prepend
* @param {{string}} acc - Accumulated name
* @returns {{string}}
@@ -591,14 +591,14 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
}
writeln!(output).unwrap();
// Generate ONE generic metric pattern factory
// Generate ONE generic series pattern factory
writeln!(
output,
r#"/**
* Generic metric pattern factory.
* Generic series pattern factory.
* @template T
* @param {{BrkClientBase}} client
* @param {{string}} name - The metric vec name
* @param {{string}} name - The series vec name
* @param {{readonly Index[]}} indexes - The supported indexes
*/
function _mp(client, name, indexes) {{
@@ -615,7 +615,7 @@ function _mp(client, name, indexes) {{
by,
/** @returns {{readonly Index[]}} */
indexes() {{ return indexes; }},
/** @param {{Index}} index @returns {{MetricEndpointBuilder<T>|undefined}} */
/** @param {{Index}} index @returns {{SeriesEndpointBuilder<T>|undefined}} */
get(index) {{ return indexes.includes(index) ? _endpoint(client, name, index) : undefined; }}
}};
}}
@@ -631,9 +631,9 @@ function _mp(client, name, indexes) {{
.iter()
.map(|idx| {
let builder = if idx.is_date_based() {
"DateMetricEndpointBuilder"
"DateSeriesEndpointBuilder"
} else {
"MetricEndpointBuilder"
"SeriesEndpointBuilder"
};
format!("readonly {}: {}<T>", idx.name(), builder)
})
@@ -642,7 +642,7 @@ function _mp(client, name, indexes) {{
writeln!(
output,
"/** @template T @typedef {{{{ name: string, by: {}, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }}}} {} */",
"/** @template T @typedef {{{{ name: string, by: {}, indexes: () => readonly Index[], get: (index: Index) => SeriesEndpointBuilder<T>|undefined }}}} {} */",
by_type, pattern.name
)
.unwrap();
@@ -713,7 +713,7 @@ pub fn generate_structural_patterns(
writeln!(output, " * @template T").unwrap();
}
writeln!(output, " * @param {{BrkClientBase}} client").unwrap();
writeln!(output, " * @param {{string}} acc - Accumulated metric name").unwrap();
writeln!(output, " * @param {{string}} acc - Accumulated series name").unwrap();
if pattern.is_templated() {
writeln!(output, " * @param {{string}} disc - Discriminator suffix").unwrap();
}
@@ -13,7 +13,7 @@ use crate::{
use super::api::generate_api_methods;
use super::client::generate_static_constants;
/// Generate JSDoc typedefs for the metrics tree.
/// Generate JSDoc typedefs for the series tree.
pub fn generate_tree_typedefs(output: &mut String, catalog: &TreeNode, metadata: &ClientMetadata) {
writeln!(output, "// Catalog tree typedefs\n").unwrap();
@@ -21,7 +21,7 @@ pub fn generate_tree_typedefs(output: &mut String, catalog: &TreeNode, metadata:
let mut generated = BTreeSet::new();
generate_tree_typedef(
output,
"MetricsTree",
"SeriesTree",
"",
catalog,
pattern_lookup,
@@ -93,7 +93,7 @@ pub fn generate_main_client(
writeln!(output, "/**").unwrap();
writeln!(
output,
" * Main BRK client with metrics tree and API methods"
" * Main BRK client with series tree and API methods"
)
.unwrap();
writeln!(output, " * @extends BrkClientBase").unwrap();
@@ -107,14 +107,14 @@ pub fn generate_main_client(
writeln!(output, " */").unwrap();
writeln!(output, " constructor(options) {{").unwrap();
writeln!(output, " super(options);").unwrap();
writeln!(output, " /** @type {{MetricsTree}} */").unwrap();
writeln!(output, " this.metrics = this._buildTree('');").unwrap();
writeln!(output, " /** @type {{SeriesTree}} */").unwrap();
writeln!(output, " this.series = this._buildTree('');").unwrap();
writeln!(output, " }}\n").unwrap();
writeln!(output, " /**").unwrap();
writeln!(output, " * @private").unwrap();
writeln!(output, " * @param {{string}} basePath").unwrap();
writeln!(output, " * @returns {{MetricsTree}}").unwrap();
writeln!(output, " * @returns {{SeriesTree}}").unwrap();
writeln!(output, " */").unwrap();
writeln!(output, " _buildTree(basePath) {{").unwrap();
writeln!(output, " return {{").unwrap();
@@ -122,7 +122,7 @@ pub fn generate_main_client(
generate_tree_initializer(
output,
catalog,
"MetricsTree",
"SeriesTree",
"",
3,
pattern_lookup,
@@ -135,27 +135,27 @@ pub fn generate_main_client(
writeln!(output, " /**").unwrap();
writeln!(
output,
" * Create a dynamic metric endpoint builder for any metric/index combination."
" * Create a dynamic series endpoint builder for any series/index combination."
)
.unwrap();
writeln!(output, " *").unwrap();
writeln!(
output,
" * Use this for programmatic access when the metric name is determined at runtime."
" * Use this for programmatic access when the series name is determined at runtime."
)
.unwrap();
writeln!(
output,
" * For type-safe access, use the `metrics` tree instead."
" * For type-safe access, use the `series` tree instead."
)
.unwrap();
writeln!(output, " *").unwrap();
writeln!(output, " * @param {{string}} metric - The metric name").unwrap();
writeln!(output, " * @param {{string}} series - The series name").unwrap();
writeln!(output, " * @param {{Index}} index - The index name").unwrap();
writeln!(output, " * @returns {{MetricEndpointBuilder<unknown>}}").unwrap();
writeln!(output, " * @returns {{SeriesEndpointBuilder<unknown>}}").unwrap();
writeln!(output, " */").unwrap();
writeln!(output, " metric(metric, index) {{").unwrap();
writeln!(output, " return _endpoint(this, metric, index);").unwrap();
writeln!(output, " seriesEndpoint(series, index) {{").unwrap();
writeln!(output, " return _endpoint(this, series, index);").unwrap();
writeln!(output, " }}\n").unwrap();
generate_api_methods(output, endpoints);
+1 -1
View File
@@ -18,7 +18,7 @@ pub use python::generate_python_client;
pub use rust::generate_rust_client;
/// Types that are manually defined as generics in client code, not from schema.
pub const MANUAL_GENERIC_TYPES: &[&str] = &["MetricData", "MetricEndpoint"];
pub const MANUAL_GENERIC_TYPES: &[&str] = &["SeriesData", "SeriesEndpoint"];
/// Write a multi-line description with the given prefix for each line.
/// `empty_prefix` is used for blank lines (e.g., " *" without trailing space).
@@ -16,7 +16,7 @@ pub fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
writeln!(output, "class BrkClient(BrkClientBase):").unwrap();
writeln!(
output,
" \"\"\"Main BRK client with metrics tree and API methods.\"\"\""
" \"\"\"Main BRK client with series tree and API methods.\"\"\""
)
.unwrap();
writeln!(output).unwrap();
@@ -30,35 +30,35 @@ pub fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
)
.unwrap();
writeln!(output, " super().__init__(base_url, timeout)").unwrap();
writeln!(output, " self.metrics = MetricsTree(self)").unwrap();
writeln!(output, " self.series = SeriesTree(self)").unwrap();
writeln!(output).unwrap();
// Generate metric() method for dynamic metric access
// Generate series_endpoint() method for dynamic series access
writeln!(
output,
" def metric(self, metric: str, index: Index) -> MetricEndpointBuilder[Any]:"
" def series_endpoint(self, series: str, index: Index) -> SeriesEndpointBuilder[Any]:"
)
.unwrap();
writeln!(
output,
" \"\"\"Create a dynamic metric endpoint builder for any metric/index combination."
" \"\"\"Create a dynamic series endpoint builder for any series/index combination."
)
.unwrap();
writeln!(output).unwrap();
writeln!(
output,
" Use this for programmatic access when the metric name is determined at runtime."
" Use this for programmatic access when the series name is determined at runtime."
)
.unwrap();
writeln!(
output,
" For type-safe access, use the `metrics` tree instead."
" For type-safe access, use the `series` tree instead."
)
.unwrap();
writeln!(output, " \"\"\"").unwrap();
writeln!(
output,
" return MetricEndpointBuilder(self, metric, index)"
" return SeriesEndpointBuilder(self, series, index)"
)
.unwrap();
writeln!(output).unwrap();
@@ -113,13 +113,13 @@ class BrkClientBase:
def _m(acc: str, s: str) -> str:
"""Build metric name with suffix."""
"""Build series name with suffix."""
if not s: return acc
return f"{{acc}}_{{s}}" if acc else s
def _p(prefix: str, acc: str) -> str:
"""Build metric name with prefix."""
"""Build series name with prefix."""
return f"{{prefix}}_{{acc}}" if acc else prefix
"#
@@ -127,7 +127,7 @@ def _p(prefix: str, acc: str) -> str:
.unwrap();
}
/// Generate the MetricData and MetricEndpointBuilder classes
/// Generate the SeriesData and SeriesEndpointBuilder classes
pub fn generate_endpoint_class(output: &mut String) {
writeln!(
output,
@@ -216,8 +216,8 @@ def _date_to_index(index: str, d: Union[date, datetime]) -> int:
@dataclass
class MetricData(Generic[T]):
"""Metric data with range information. Always int-indexed."""
class SeriesData(Generic[T]):
"""Series data with range information. Always int-indexed."""
version: int
index: Index
type: str
@@ -229,7 +229,7 @@ class MetricData(Generic[T]):
@property
def is_date_based(self) -> bool:
"""Whether this metric uses a date-based index."""
"""Whether this series uses a date-based index."""
return self.index in _DATE_INDEXES
def indexes(self) -> List[int]:
@@ -273,8 +273,8 @@ class MetricData(Generic[T]):
@dataclass
class DateMetricData(MetricData[T]):
"""Metric data with date-based index. Extends MetricData with date methods."""
class DateSeriesData(SeriesData[T]):
"""Series data with date-based index. Extends SeriesData with date methods."""
def dates(self) -> List[Union[date, datetime]]:
"""Get dates for the index range. Returns datetime for sub-daily indexes, date for daily+."""
@@ -320,8 +320,8 @@ class DateMetricData(MetricData[T]):
# Type aliases for non-generic usage
AnyMetricData = MetricData[Any]
AnyDateMetricData = DateMetricData[Any]
AnySeriesData = SeriesData[Any]
AnyDateSeriesData = DateSeriesData[Any]
class _EndpointConfig:
@@ -341,7 +341,7 @@ class _EndpointConfig:
self.end = end
def path(self) -> str:
return f"/api/metric/{{self.name}}/{{self.index}}"
return f"/api/series/{{self.name}}/{{self.index}}"
def _build_path(self, format: Optional[str] = None) -> str:
params = []
@@ -358,11 +358,11 @@ class _EndpointConfig:
def _new(self, start: Optional[int] = None, end: Optional[int] = None) -> _EndpointConfig:
return _EndpointConfig(self.client, self.name, self.index, start, end)
def get_metric(self) -> MetricData[Any]:
return MetricData(**self.client.get_json(self._build_path()))
def get_series(self) -> SeriesData[Any]:
return SeriesData(**self.client.get_json(self._build_path()))
def get_date_metric(self) -> DateMetricData[Any]:
return DateMetricData(**self.client.get_json(self._build_path()))
def get_date_series(self) -> DateSeriesData[Any]:
return DateSeriesData(**self.client.get_json(self._build_path()))
def get_csv(self) -> str:
return self.client.get_text(self._build_path(format='csv'))
@@ -374,9 +374,9 @@ class RangeBuilder(Generic[T]):
def __init__(self, config: _EndpointConfig):
self._config = config
def fetch(self) -> MetricData[T]:
def fetch(self) -> SeriesData[T]:
"""Fetch the range as parsed JSON."""
return self._config.get_metric()
return self._config.get_series()
def fetch_csv(self) -> str:
"""Fetch the range as CSV string."""
@@ -389,9 +389,9 @@ class SingleItemBuilder(Generic[T]):
def __init__(self, config: _EndpointConfig):
self._config = config
def fetch(self) -> MetricData[T]:
def fetch(self) -> SeriesData[T]:
"""Fetch the single item."""
return self._config.get_metric()
return self._config.get_series()
def fetch_csv(self) -> str:
"""Fetch as CSV."""
@@ -409,9 +409,9 @@ class SkippedBuilder(Generic[T]):
start = self._config.start or 0
return RangeBuilder(self._config._new(start, start + n))
def fetch(self) -> MetricData[T]:
def fetch(self) -> SeriesData[T]:
"""Fetch from skipped position to end."""
return self._config.get_metric()
return self._config.get_series()
def fetch_csv(self) -> str:
"""Fetch as CSV."""
@@ -419,28 +419,28 @@ class SkippedBuilder(Generic[T]):
class DateRangeBuilder(RangeBuilder[T]):
"""Range builder that returns DateMetricData."""
def fetch(self) -> DateMetricData[T]:
return self._config.get_date_metric()
"""Range builder that returns DateSeriesData."""
def fetch(self) -> DateSeriesData[T]:
return self._config.get_date_series()
class DateSingleItemBuilder(SingleItemBuilder[T]):
"""Single item builder that returns DateMetricData."""
def fetch(self) -> DateMetricData[T]:
return self._config.get_date_metric()
"""Single item builder that returns DateSeriesData."""
def fetch(self) -> DateSeriesData[T]:
return self._config.get_date_series()
class DateSkippedBuilder(SkippedBuilder[T]):
"""Skipped builder that returns DateMetricData."""
"""Skipped builder that returns DateSeriesData."""
def take(self, n: int) -> DateRangeBuilder[T]:
start = self._config.start or 0
return DateRangeBuilder(self._config._new(start, start + n))
def fetch(self) -> DateMetricData[T]:
return self._config.get_date_metric()
def fetch(self) -> DateSeriesData[T]:
return self._config.get_date_series()
class MetricEndpointBuilder(Generic[T]):
"""Builder for metric endpoint queries with int-based indexing.
class SeriesEndpointBuilder(Generic[T]):
"""Builder for series endpoint queries with int-based indexing.
Examples:
data = endpoint.fetch()
@@ -476,9 +476,9 @@ class MetricEndpointBuilder(Generic[T]):
"""Skip the first n items."""
return SkippedBuilder(self._config._new(start=n))
def fetch(self) -> MetricData[T]:
def fetch(self) -> SeriesData[T]:
"""Fetch all data."""
return self._config.get_metric()
return self._config.get_series()
def fetch_csv(self) -> str:
"""Fetch all data as CSV."""
@@ -489,10 +489,10 @@ class MetricEndpointBuilder(Generic[T]):
return self._config.path()
class DateMetricEndpointBuilder(Generic[T]):
"""Builder for metric endpoint queries with date-based indexing.
class DateSeriesEndpointBuilder(Generic[T]):
"""Builder for series endpoint queries with date-based indexing.
Accepts dates in __getitem__ and returns DateMetricData from fetch().
Accepts dates in __getitem__ and returns DateSeriesData from fetch().
Examples:
data = endpoint.fetch()
@@ -539,9 +539,9 @@ class DateMetricEndpointBuilder(Generic[T]):
"""Skip the first n items."""
return DateSkippedBuilder(self._config._new(start=n))
def fetch(self) -> DateMetricData[T]:
def fetch(self) -> DateSeriesData[T]:
"""Fetch all data."""
return self._config.get_date_metric()
return self._config.get_date_series()
def fetch_csv(self) -> str:
"""Fetch all data as CSV."""
@@ -553,23 +553,23 @@ class DateMetricEndpointBuilder(Generic[T]):
# Type aliases for non-generic usage
AnyMetricEndpointBuilder = MetricEndpointBuilder[Any]
AnyDateMetricEndpointBuilder = DateMetricEndpointBuilder[Any]
AnySeriesEndpointBuilder = SeriesEndpointBuilder[Any]
AnyDateSeriesEndpointBuilder = DateSeriesEndpointBuilder[Any]
class MetricPattern(Protocol[T]):
"""Protocol for metric patterns with different index sets."""
class SeriesPattern(Protocol[T]):
"""Protocol for series patterns with different index sets."""
@property
def name(self) -> str:
"""Get the metric name."""
"""Get the series name."""
...
def indexes(self) -> List[str]:
"""Get the list of available indexes for this metric."""
"""Get the list of available indexes for this series."""
...
def get(self, index: Index) -> Optional[MetricEndpointBuilder[T]]:
def get(self, index: Index) -> Optional[SeriesEndpointBuilder[T]]:
"""Get an endpoint builder for a specific index, if supported."""
...
@@ -605,11 +605,11 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
// Generate helper functions
writeln!(
output,
r#"def _ep(c: BrkClientBase, n: str, i: Index) -> MetricEndpointBuilder[Any]:
return MetricEndpointBuilder(c, n, i)
r#"def _ep(c: BrkClientBase, n: str, i: Index) -> SeriesEndpointBuilder[Any]:
return SeriesEndpointBuilder(c, n, i)
def _dep(c: BrkClientBase, n: str, i: Index) -> DateMetricEndpointBuilder[Any]:
return DateMetricEndpointBuilder(c, n, i)
def _dep(c: BrkClientBase, n: str, i: Index) -> DateSeriesEndpointBuilder[Any]:
return DateSeriesEndpointBuilder(c, n, i)
"#
)
.unwrap();
@@ -631,9 +631,9 @@ def _dep(c: BrkClientBase, n: str, i: Index) -> DateMetricEndpointBuilder[Any]:
let method_name = index_to_field_name(index);
let index_name = index.name();
let (builder_type, helper) = if index.is_date_based() {
("DateMetricEndpointBuilder", "_dep")
("DateSeriesEndpointBuilder", "_dep")
} else {
("MetricEndpointBuilder", "_ep")
("SeriesEndpointBuilder", "_ep")
};
writeln!(
output,
@@ -663,7 +663,7 @@ def _dep(c: BrkClientBase, n: str, i: Index) -> DateMetricEndpointBuilder[Any]:
.unwrap();
writeln!(
output,
" def get(self, index: Index) -> Optional[MetricEndpointBuilder[T]]: return _ep(self.by._c, self._n, index) if index in {} else None",
" def get(self, index: Index) -> Optional[SeriesEndpointBuilder[T]]: return _ep(self.by._c, self._n, index) if index in {} else None",
idx_var
)
.unwrap();
@@ -719,7 +719,7 @@ pub fn generate_structural_patterns(
}
writeln!(
output,
" \"\"\"Create pattern node with accumulated metric name.\"\"\""
" \"\"\"Create pattern node with accumulated series name.\"\"\""
)
.unwrap();
@@ -12,13 +12,13 @@ use crate::{
/// Generate tree classes
pub fn generate_tree_classes(output: &mut String, catalog: &TreeNode, metadata: &ClientMetadata) {
writeln!(output, "# Metrics tree classes\n").unwrap();
writeln!(output, "# Series tree classes\n").unwrap();
let pattern_lookup = metadata.pattern_lookup();
let mut generated = BTreeSet::new();
generate_tree_class(
output,
"MetricsTree",
"SeriesTree",
"",
catalog,
pattern_lookup,
@@ -60,7 +60,7 @@ fn generate_tree_class(
// THEN generate the current class (after all children are defined)
writeln!(output, "class {}:", name).unwrap();
writeln!(output, " \"\"\"Metrics tree node.\"\"\"").unwrap();
writeln!(output, " \"\"\"Series tree node.\"\"\"").unwrap();
writeln!(output, " ").unwrap();
writeln!(
output,
+21 -21
View File
@@ -10,10 +10,10 @@ use super::types::js_type_to_rust;
pub fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
writeln!(
output,
r#"/// Main BRK client with metrics tree and API methods.
r#"/// Main BRK client with series tree and API methods.
pub struct BrkClient {{
base: Arc<BrkClientBase>,
metrics: MetricsTree,
series: SeriesTree,
}}
impl BrkClient {{
@@ -23,51 +23,51 @@ impl BrkClient {{
/// Create a new client with the given base URL.
pub fn new(base_url: impl Into<String>) -> Self {{
let base = Arc::new(BrkClientBase::new(base_url));
let metrics = MetricsTree::new(base.clone(), String::new());
Self {{ base, metrics }}
let series = SeriesTree::new(base.clone(), String::new());
Self {{ base, series }}
}}
/// Create a new client with options.
pub fn with_options(options: BrkClientOptions) -> Self {{
let base = Arc::new(BrkClientBase::with_options(options));
let metrics = MetricsTree::new(base.clone(), String::new());
Self {{ base, metrics }}
let series = SeriesTree::new(base.clone(), String::new());
Self {{ base, series }}
}}
/// Get the metrics tree for navigating metrics.
pub fn metrics(&self) -> &MetricsTree {{
&self.metrics
/// Get the series tree for navigating series.
pub fn series(&self) -> &SeriesTree {{
&self.series
}}
/// Create a dynamic metric endpoint builder for any metric/index combination.
/// Create a dynamic series endpoint builder for any series/index combination.
///
/// Use this for programmatic access when the metric name is determined at runtime.
/// For type-safe access, use the `metrics()` tree instead.
/// Use this for programmatic access when the series name is determined at runtime.
/// For type-safe access, use the `series()` tree instead.
///
/// # Example
/// ```ignore
/// let data = client.metric("realized_price", Index::Height)
/// let data = client.series("realized_price", Index::Height)
/// .last(10)
/// .json::<f64>()?;
/// ```
pub fn metric(&self, metric: impl Into<Metric>, index: Index) -> MetricEndpointBuilder<serde_json::Value> {{
MetricEndpointBuilder::new(
pub fn series_endpoint(&self, series: impl Into<Series>, index: Index) -> SeriesEndpointBuilder<serde_json::Value> {{
SeriesEndpointBuilder::new(
self.base.clone(),
Arc::from(metric.into().as_str()),
Arc::from(series.into().as_str()),
index,
)
}}
/// Create a dynamic date-based metric endpoint builder.
/// Create a dynamic date-based series endpoint builder.
///
/// Returns `Err` if the index is not date-based.
pub fn date_metric(&self, metric: impl Into<Metric>, index: Index) -> Result<DateMetricEndpointBuilder<serde_json::Value>> {{
pub fn date_series_endpoint(&self, series: impl Into<Series>, index: Index) -> Result<DateSeriesEndpointBuilder<serde_json::Value>> {{
if !index.is_date_based() {{
return Err(BrkError {{ message: format!("{{}} is not a date-based index", index.name()) }});
}}
Ok(DateMetricEndpointBuilder::new(
Ok(DateSeriesEndpointBuilder::new(
self.base.clone(),
Arc::from(metric.into().as_str()),
Arc::from(series.into().as_str()),
index,
))
}}
@@ -217,7 +217,7 @@ fn param_type_to_rust(param_type: &str) -> String {
"string" | "*" => "&str".to_string(),
"integer" | "number" => "i64".to_string(),
"boolean" => "bool".to_string(),
other => other.to_string(), // Domain types like Index, Metric, Format
other => other.to_string(), // Domain types like Index, Series, Format
}
}
@@ -105,7 +105,7 @@ impl BrkClientBase {{
}}
}}
/// Build metric name with suffix.
/// Build series name with suffix.
#[inline]
fn _m(acc: &str, s: &str) -> String {{
if s.is_empty() {{ acc.to_string() }}
@@ -113,7 +113,7 @@ fn _m(acc: &str, s: &str) -> String {{
else {{ format!("{{acc}}_{{s}}") }}
}}
/// Build metric name with prefix.
/// Build series name with prefix.
#[inline]
fn _p(prefix: &str, acc: &str) -> String {{
if acc.is_empty() {{ prefix.to_string() }} else {{ format!("{{prefix}}_{{acc}}") }}
@@ -124,23 +124,23 @@ fn _p(prefix: &str, acc: &str) -> String {{
.unwrap();
}
/// Generate the MetricPattern trait.
pub fn generate_metric_pattern_trait(output: &mut String) {
/// Generate the SeriesPattern trait.
pub fn generate_series_pattern_trait(output: &mut String) {
writeln!(
output,
r#"/// Non-generic trait for metric patterns (usable in collections).
pub trait AnyMetricPattern {{
/// Get the metric name.
r#"/// Non-generic trait for series patterns (usable in collections).
pub trait AnySeriesPattern {{
/// Get the series name.
fn name(&self) -> &str;
/// Get the list of available indexes for this metric.
/// Get the list of available indexes for this series.
fn indexes(&self) -> &'static [Index];
}}
/// Generic trait for metric patterns with endpoint access.
pub trait MetricPattern<T>: AnyMetricPattern {{
/// Generic trait for series patterns with endpoint access.
pub trait SeriesPattern<T>: AnySeriesPattern {{
/// Get an endpoint builder for a specific index, if supported.
fn get(&self, index: Index) -> Option<MetricEndpointBuilder<T>>;
fn get(&self, index: Index) -> Option<SeriesEndpointBuilder<T>>;
}}
"#
@@ -148,7 +148,7 @@ pub trait MetricPattern<T>: AnyMetricPattern {{
.unwrap();
}
/// Generate the MetricEndpointBuilder structs with typestate pattern.
/// Generate the SeriesEndpointBuilder structs with typestate pattern.
pub fn generate_endpoint(output: &mut String) {
writeln!(
output,
@@ -168,7 +168,7 @@ impl EndpointConfig {{
}}
fn path(&self) -> String {{
format!("/api/metric/{{}}/{{}}", self.name, self.index.name())
format!("/api/series/{{}}/{{}}", self.name, self.index.name())
}}
fn build_path(&self, format: Option<&str>) -> String {{
@@ -189,10 +189,10 @@ impl EndpointConfig {{
}}
}}
/// Builder for metric endpoint queries.
/// Builder for series endpoint queries.
///
/// Parameterized by element type `T` and response type `D` (defaults to `MetricData<T>`).
/// For date-based indexes, use `DateMetricEndpointBuilder<T>` which sets `D = DateMetricData<T>`.
/// Parameterized by element type `T` and response type `D` (defaults to `SeriesData<T>`).
/// For date-based indexes, use `DateSeriesEndpointBuilder<T>` which sets `D = DateSeriesData<T>`.
///
/// # Examples
/// ```ignore
@@ -204,18 +204,18 @@ impl EndpointConfig {{
/// let data = endpoint.last(10).fetch()?; // last 10
/// let data = endpoint.skip(100).take(10).fetch()?; // iterator-style
/// ```
pub struct MetricEndpointBuilder<T, D = MetricData<T>> {{
pub struct SeriesEndpointBuilder<T, D = SeriesData<T>> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<fn() -> (T, D)>,
}}
/// Builder for date-based metric endpoint queries.
/// Builder for date-based series endpoint queries.
///
/// Like `MetricEndpointBuilder` but returns `DateMetricData` and provides
/// Like `SeriesEndpointBuilder` but returns `DateSeriesData` and provides
/// date-based access methods (`get_date`, `date_range`).
pub type DateMetricEndpointBuilder<T> = MetricEndpointBuilder<T, DateMetricData<T>>;
pub type DateSeriesEndpointBuilder<T> = SeriesEndpointBuilder<T, DateSeriesData<T>>;
impl<T: DeserializeOwned, D: DeserializeOwned> MetricEndpointBuilder<T, D> {{
impl<T: DeserializeOwned, D: DeserializeOwned> SeriesEndpointBuilder<T, D> {{
pub fn new(client: Arc<BrkClientBase>, name: Arc<str>, index: Index) -> Self {{
Self {{ config: EndpointConfig::new(client, name, index), _marker: std::marker::PhantomData }}
}}
@@ -286,29 +286,29 @@ impl<T: DeserializeOwned, D: DeserializeOwned> MetricEndpointBuilder<T, D> {{
}}
}}
/// Date-specific methods available only on `DateMetricEndpointBuilder`.
impl<T: DeserializeOwned> MetricEndpointBuilder<T, DateMetricData<T>> {{
/// Date-specific methods available only on `DateSeriesEndpointBuilder`.
impl<T: DeserializeOwned> SeriesEndpointBuilder<T, DateSeriesData<T>> {{
/// Select a specific date position (for day-precision or coarser indexes).
pub fn get_date(self, date: Date) -> SingleItemBuilder<T, DateMetricData<T>> {{
pub fn get_date(self, date: Date) -> SingleItemBuilder<T, DateSeriesData<T>> {{
let index = self.config.index.date_to_index(date).unwrap_or(0);
self.get(index)
}}
/// Select a date range (for day-precision or coarser indexes).
pub fn date_range(self, start: Date, end: Date) -> RangeBuilder<T, DateMetricData<T>> {{
pub fn date_range(self, start: Date, end: Date) -> RangeBuilder<T, DateSeriesData<T>> {{
let s = self.config.index.date_to_index(start).unwrap_or(0);
let e = self.config.index.date_to_index(end).unwrap_or(0);
self.range(s..e)
}}
/// Select a specific timestamp position (works for all date-based indexes including sub-daily).
pub fn get_timestamp(self, ts: Timestamp) -> SingleItemBuilder<T, DateMetricData<T>> {{
pub fn get_timestamp(self, ts: Timestamp) -> SingleItemBuilder<T, DateSeriesData<T>> {{
let index = self.config.index.timestamp_to_index(ts).unwrap_or(0);
self.get(index)
}}
/// Select a timestamp range (works for all date-based indexes including sub-daily).
pub fn timestamp_range(self, start: Timestamp, end: Timestamp) -> RangeBuilder<T, DateMetricData<T>> {{
pub fn timestamp_range(self, start: Timestamp, end: Timestamp) -> RangeBuilder<T, DateSeriesData<T>> {{
let s = self.config.index.timestamp_to_index(start).unwrap_or(0);
let e = self.config.index.timestamp_to_index(end).unwrap_or(0);
self.range(s..e)
@@ -316,13 +316,13 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T, DateMetricData<T>> {{
}}
/// Builder for single item access.
pub struct SingleItemBuilder<T, D = MetricData<T>> {{
pub struct SingleItemBuilder<T, D = SeriesData<T>> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<fn() -> (T, D)>,
}}
/// Date-aware single item builder.
pub type DateSingleItemBuilder<T> = SingleItemBuilder<T, DateMetricData<T>>;
pub type DateSingleItemBuilder<T> = SingleItemBuilder<T, DateSeriesData<T>>;
impl<T: DeserializeOwned, D: DeserializeOwned> SingleItemBuilder<T, D> {{
/// Fetch the single item.
@@ -337,13 +337,13 @@ impl<T: DeserializeOwned, D: DeserializeOwned> SingleItemBuilder<T, D> {{
}}
/// Builder after calling `skip(n)`. Chain with `take(n)` to specify count.
pub struct SkippedBuilder<T, D = MetricData<T>> {{
pub struct SkippedBuilder<T, D = SeriesData<T>> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<fn() -> (T, D)>,
}}
/// Date-aware skipped builder.
pub type DateSkippedBuilder<T> = SkippedBuilder<T, DateMetricData<T>>;
pub type DateSkippedBuilder<T> = SkippedBuilder<T, DateSeriesData<T>>;
impl<T: DeserializeOwned, D: DeserializeOwned> SkippedBuilder<T, D> {{
/// Take n items after the skipped position.
@@ -365,13 +365,13 @@ impl<T: DeserializeOwned, D: DeserializeOwned> SkippedBuilder<T, D> {{
}}
/// Builder with range fully specified.
pub struct RangeBuilder<T, D = MetricData<T>> {{
pub struct RangeBuilder<T, D = SeriesData<T>> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<fn() -> (T, D)>,
}}
/// Date-aware range builder.
pub type DateRangeBuilder<T> = RangeBuilder<T, DateMetricData<T>>;
pub type DateRangeBuilder<T> = RangeBuilder<T, DateSeriesData<T>>;
impl<T: DeserializeOwned, D: DeserializeOwned> RangeBuilder<T, D> {{
/// Fetch the range as parsed JSON.
@@ -414,13 +414,13 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
writeln!(
output,
r#"#[inline]
fn _ep<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) -> MetricEndpointBuilder<T> {{
MetricEndpointBuilder::new(c.clone(), n.clone(), i)
fn _ep<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) -> SeriesEndpointBuilder<T> {{
SeriesEndpointBuilder::new(c.clone(), n.clone(), i)
}}
#[inline]
fn _dep<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) -> DateMetricEndpointBuilder<T> {{
DateMetricEndpointBuilder::new(c.clone(), n.clone(), i)
fn _dep<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) -> DateSeriesEndpointBuilder<T> {{
DateSeriesEndpointBuilder::new(c.clone(), n.clone(), i)
}}
"#
)
@@ -441,14 +441,14 @@ fn _dep<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) ->
if index.is_date_based() {
writeln!(
output,
" pub fn {}(&self) -> DateMetricEndpointBuilder<T> {{ _dep(&self.client, &self.name, Index::{}) }}",
" pub fn {}(&self) -> DateSeriesEndpointBuilder<T> {{ _dep(&self.client, &self.name, Index::{}) }}",
method_name, index
)
.unwrap();
} else {
writeln!(
output,
" pub fn {}(&self) -> MetricEndpointBuilder<T> {{ _ep(&self.client, &self.name, Index::{}) }}",
" pub fn {}(&self) -> SeriesEndpointBuilder<T> {{ _ep(&self.client, &self.name, Index::{}) }}",
method_name, index
)
.unwrap();
@@ -473,18 +473,18 @@ fn _dep<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) ->
writeln!(output, " pub fn name(&self) -> &str {{ &self.name }}").unwrap();
writeln!(output, "}}\n").unwrap();
// Implement AnyMetricPattern trait
// Implement AnySeriesPattern trait
writeln!(
output,
"impl<T> AnyMetricPattern for {}<T> {{ fn name(&self) -> &str {{ &self.name }} fn indexes(&self) -> &'static [Index] {{ {} }} }}",
"impl<T> AnySeriesPattern for {}<T> {{ fn name(&self) -> &str {{ &self.name }} fn indexes(&self) -> &'static [Index] {{ {} }} }}",
pattern.name, idx_const
)
.unwrap();
// Implement MetricPattern<T> trait
// Implement SeriesPattern<T> trait
writeln!(
output,
"impl<T: DeserializeOwned> MetricPattern<T> for {}<T> {{ fn get(&self, index: Index) -> Option<MetricEndpointBuilder<T>> {{ {}.contains(&index).then(|| _ep(&self.by.client, &self.by.name, index)) }} }}\n",
"impl<T: DeserializeOwned> SeriesPattern<T> for {}<T> {{ fn get(&self, index: Index) -> Option<SeriesEndpointBuilder<T>> {{ {}.contains(&index).then(|| _ep(&self.by.client, &self.by.name, index)) }} }}\n",
pattern.name, idx_const
)
.unwrap();
@@ -542,7 +542,7 @@ pub fn generate_pattern_structs(
writeln!(
output,
" /// Create a new pattern node with accumulated metric name."
" /// Create a new pattern node with accumulated series name."
)
.unwrap();
if pattern.is_templated() {
@@ -32,7 +32,7 @@ pub fn generate_rust_client(
client::generate_imports(&mut output);
client::generate_base_client(&mut output);
client::generate_metric_pattern_trait(&mut output);
client::generate_series_pattern_trait(&mut output);
client::generate_endpoint(&mut output);
client::generate_index_accessors(&mut output, &metadata.index_set_patterns);
client::generate_pattern_structs(&mut output, &metadata.structural_patterns, metadata);
@@ -13,13 +13,13 @@ use crate::{
/// Generate tree structs.
pub fn generate_tree(output: &mut String, catalog: &TreeNode, metadata: &ClientMetadata) {
writeln!(output, "// Metrics tree\n").unwrap();
writeln!(output, "// Series tree\n").unwrap();
let pattern_lookup = metadata.pattern_lookup();
let mut generated = BTreeSet::new();
generate_tree_node(
output,
"MetricsTree",
"SeriesTree",
"",
catalog,
pattern_lookup,
@@ -42,7 +42,7 @@ fn generate_tree_node(
};
// Generate struct definition
writeln!(output, "/// Metrics tree node.").unwrap();
writeln!(output, "/// Series tree node.").unwrap();
writeln!(output, "pub struct {} {{", name).unwrap();
for child in &ctx.children {
+1 -1
View File
@@ -218,7 +218,7 @@ fn check_csv_support(operation: &Operation) -> bool {
/// Extract path parameters in the order they appear in the path URL.
fn extract_path_parameters(path: &str, operation: &Operation) -> Vec<Parameter> {
// Extract parameter names from the path in order (e.g., "/api/metric/{metric}/{index}" -> ["metric", "index"])
// Extract parameter names from the path in order (e.g., "/api/series/{series}/{index}" -> ["series", "index"])
let path_order: Vec<&str> = path
.split('/')
.filter_map(|segment| segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')))
+6 -6
View File
@@ -3,7 +3,7 @@
use std::collections::{BTreeMap, BTreeSet};
use brk_query::Vecs;
use brk_types::{Index, MetricLeafWithSchema};
use brk_types::{Index, SeriesLeafWithSchema};
use super::{GenericSyntax, IndexSetPattern, PatternField, StructuralPattern, extract_inner_type};
use crate::{PatternBaseResult, analysis};
@@ -15,7 +15,7 @@ pub struct ClientMetadata {
pub catalog: brk_types::TreeNode,
/// Structural patterns - tree node shapes that repeat
pub structural_patterns: Vec<StructuralPattern>,
/// Index set patterns - sets of indexes that appear together on metrics
/// Index set patterns - sets of indexes that appear together on series
pub index_set_patterns: Vec<IndexSetPattern>,
/// Maps field signatures to pattern names (merged from concrete instances + pattern definitions)
pattern_lookup: BTreeMap<Vec<PatternField>, String>,
@@ -135,24 +135,24 @@ impl ClientMetadata {
if let Some(accessor) = self.find_index_set_pattern(&field.indexes) {
syntax.wrap(&accessor.name, &value_type)
} else {
syntax.wrap("MetricNode", &value_type)
syntax.wrap("SeriesNode", &value_type)
}
}
/// Generate type annotation for a leaf node with language-specific syntax.
///
/// This is a simpler version of `field_type_annotation` that works directly
/// with a `MetricLeafWithSchema` node instead of a `PatternField`.
/// with a `SeriesLeafWithSchema` node instead of a `PatternField`.
pub fn field_type_annotation_from_leaf(
&self,
leaf: &MetricLeafWithSchema,
leaf: &SeriesLeafWithSchema,
syntax: GenericSyntax,
) -> String {
let value_type = leaf.kind().to_string();
if let Some(accessor) = self.find_index_set_pattern(leaf.indexes()) {
syntax.wrap(&accessor.name, &value_type)
} else {
syntax.wrap("MetricNode", &value_type)
syntax.wrap("SeriesNode", &value_type)
}
}
}
+4 -4
View File
@@ -1,4 +1,4 @@
//! Pattern mode and field parts for metric name reconstruction.
//! Pattern mode and field parts for series name reconstruction.
//!
//! Patterns are either suffix mode or prefix mode:
//! - Suffix mode: `_m(acc, relative)` → `acc_relative` or just `relative` if acc empty
@@ -6,14 +6,14 @@
use std::collections::BTreeMap;
/// How a pattern constructs metric names from the accumulator.
/// How a pattern constructs series names from the accumulator.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PatternMode {
/// Fields append their relative name to acc.
/// Formula: `_m(acc, relative)` → `{acc}_{relative}` or `{relative}` if acc empty
/// Example: `_m("lth", "max_cost_basis")` → `"lth_max_cost_basis"`
Suffix {
/// Maps field name to its relative name (full metric name when acc = "")
/// Maps field name to its relative name (full series name when acc = "")
relatives: BTreeMap<String, String>,
},
/// Fields prepend their prefix to acc.
@@ -23,7 +23,7 @@ pub enum PatternMode {
/// Maps field name to its prefix (empty string for identity)
prefixes: BTreeMap<String, String>,
},
/// Fields construct metric names using a template with a discriminator placeholder.
/// Fields construct series names using a template with a discriminator placeholder.
/// Factory takes two params: `acc` (base) and `disc` (discriminator).
/// Formula: `_m(acc, template.replace("{disc}", disc))`
/// Example: template `"ratio_{disc}_bps"` with disc `"pct99"` → `_m(acc, "ratio_pct99_bps")`
+2 -2
View File
@@ -6,7 +6,7 @@ use brk_types::Index;
use super::PatternMode;
/// A pattern of indexes that appear together on multiple metrics.
/// A pattern of indexes that appear together on multiple series.
#[derive(Debug, Clone)]
pub struct IndexSetPattern {
/// Pattern name (e.g., "DateHeightIndexes")
@@ -22,7 +22,7 @@ pub struct StructuralPattern {
pub name: String,
/// Ordered list of child fields
pub fields: Vec<PatternField>,
/// How fields construct metric names from acc (None = not parameterizable)
/// How fields construct series names from acc (None = not parameterizable)
pub mode: Option<PatternMode>,
/// If true, all leaf fields use a type parameter T
pub is_generic: bool,
+2643 -2643
View File
File diff suppressed because it is too large Load Diff
+16 -16
View File
@@ -6,15 +6,15 @@ use super::CohortName;
/// "At least X% loss" threshold names (9 thresholds).
pub const LOSS_NAMES: Loss<CohortName> = Loss {
breakeven: CohortName::new("utxos_in_loss", "<0%", "In Loss (Below Breakeven)"),
_10pct: CohortName::new("utxos_over_10pct_in_loss", "10%L", "10%+ Loss"),
_20pct: CohortName::new("utxos_over_20pct_in_loss", "20%L", "20%+ Loss"),
_30pct: CohortName::new("utxos_over_30pct_in_loss", "30%L", "30%+ Loss"),
_40pct: CohortName::new("utxos_over_40pct_in_loss", "40%L", "40%+ Loss"),
_50pct: CohortName::new("utxos_over_50pct_in_loss", "50%L", "50%+ Loss"),
_60pct: CohortName::new("utxos_over_60pct_in_loss", "60%L", "60%+ Loss"),
_70pct: CohortName::new("utxos_over_70pct_in_loss", "70%L", "70%+ Loss"),
_80pct: CohortName::new("utxos_over_80pct_in_loss", "80%L", "80%+ Loss"),
all: CohortName::new("utxos_in_loss", "All", "In Loss"),
_10pct: CohortName::new("utxos_over_10pct_in_loss", ">=10%", "Over 10% in Loss"),
_20pct: CohortName::new("utxos_over_20pct_in_loss", ">=20%", "Over 20% in Loss"),
_30pct: CohortName::new("utxos_over_30pct_in_loss", ">=30%", "Over 30% in Loss"),
_40pct: CohortName::new("utxos_over_40pct_in_loss", ">=40%", "Over 40% in Loss"),
_50pct: CohortName::new("utxos_over_50pct_in_loss", ">=50%", "Over 50% in Loss"),
_60pct: CohortName::new("utxos_over_60pct_in_loss", ">=60%", "Over 60% in Loss"),
_70pct: CohortName::new("utxos_over_70pct_in_loss", ">=70%", "Over 70% in Loss"),
_80pct: CohortName::new("utxos_over_80pct_in_loss", ">=80%", "Over 80% in Loss"),
};
/// Number of loss thresholds.
@@ -31,7 +31,7 @@ impl Loss<CohortName> {
/// Each is a suffix sum over the profitability ranges, from most loss-making up.
#[derive(Default, Clone, Traversable, Serialize)]
pub struct Loss<T> {
pub breakeven: T,
pub all: T,
pub _10pct: T,
pub _20pct: T,
pub _30pct: T,
@@ -49,7 +49,7 @@ impl<T> Loss<T> {
{
let n = &LOSS_NAMES;
Self {
breakeven: create(n.breakeven.id),
all: create(n.all.id),
_10pct: create(n._10pct.id),
_20pct: create(n._20pct.id),
_30pct: create(n._30pct.id),
@@ -67,7 +67,7 @@ impl<T> Loss<T> {
{
let n = &LOSS_NAMES;
Ok(Self {
breakeven: create(n.breakeven.id)?,
all: create(n.all.id)?,
_10pct: create(n._10pct.id)?,
_20pct: create(n._20pct.id)?,
_30pct: create(n._30pct.id)?,
@@ -81,7 +81,7 @@ impl<T> Loss<T> {
pub fn iter(&self) -> impl Iterator<Item = &T> {
[
&self.breakeven,
&self.all,
&self._10pct,
&self._20pct,
&self._30pct,
@@ -96,7 +96,7 @@ impl<T> Loss<T> {
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
[
&mut self.breakeven,
&mut self.all,
&mut self._10pct,
&mut self._20pct,
&mut self._30pct,
@@ -114,7 +114,7 @@ impl<T> Loss<T> {
T: Send + Sync,
{
[
&mut self.breakeven,
&mut self.all,
&mut self._10pct,
&mut self._20pct,
&mut self._30pct,
@@ -130,7 +130,7 @@ impl<T> Loss<T> {
/// Access as array for indexed accumulation.
pub fn as_array_mut(&mut self) -> [&mut T; LOSS_COUNT] {
[
&mut self.breakeven,
&mut self.all,
&mut self._10pct,
&mut self._20pct,
&mut self._30pct,
+21 -21
View File
@@ -6,20 +6,20 @@ use super::CohortName;
/// "At least X% profit" threshold names (14 thresholds).
pub const PROFIT_NAMES: Profit<CohortName> = Profit {
breakeven: CohortName::new("utxos_in_profit", "≥0%", "In Profit (Breakeven+)"),
_10pct: CohortName::new("utxos_over_10pct_in_profit", "10%", "10%+ Profit"),
_20pct: CohortName::new("utxos_over_20pct_in_profit", "20%", "20%+ Profit"),
_30pct: CohortName::new("utxos_over_30pct_in_profit", "30%", "30%+ Profit"),
_40pct: CohortName::new("utxos_over_40pct_in_profit", "40%", "40%+ Profit"),
_50pct: CohortName::new("utxos_over_50pct_in_profit", "50%", "50%+ Profit"),
_60pct: CohortName::new("utxos_over_60pct_in_profit", "60%", "60%+ Profit"),
_70pct: CohortName::new("utxos_over_70pct_in_profit", "70%", "70%+ Profit"),
_80pct: CohortName::new("utxos_over_80pct_in_profit", "80%", "80%+ Profit"),
_90pct: CohortName::new("utxos_over_90pct_in_profit", "90%", "90%+ Profit"),
_100pct: CohortName::new("utxos_over_100pct_in_profit", "100%", "100%+ Profit"),
_200pct: CohortName::new("utxos_over_200pct_in_profit", "200%", "200%+ Profit"),
_300pct: CohortName::new("utxos_over_300pct_in_profit", "300%", "300%+ Profit"),
_500pct: CohortName::new("utxos_over_500pct_in_profit", "500%", "500%+ Profit"),
all: CohortName::new("utxos_in_profit", "All", "In Profit"),
_10pct: CohortName::new("utxos_over_10pct_in_profit", ">=10%", "Over 10% in Profit"),
_20pct: CohortName::new("utxos_over_20pct_in_profit", ">=20%", "Over 20% in Profit"),
_30pct: CohortName::new("utxos_over_30pct_in_profit", ">=30%", "Over 30% in Profit"),
_40pct: CohortName::new("utxos_over_40pct_in_profit", ">=40%", "Over 40% in Profit"),
_50pct: CohortName::new("utxos_over_50pct_in_profit", ">=50%", "Over 50% in Profit"),
_60pct: CohortName::new("utxos_over_60pct_in_profit", ">=60%", "Over 60% in Profit"),
_70pct: CohortName::new("utxos_over_70pct_in_profit", ">=70%", "Over 70% in Profit"),
_80pct: CohortName::new("utxos_over_80pct_in_profit", ">=80%", "Over 80% in Profit"),
_90pct: CohortName::new("utxos_over_90pct_in_profit", ">=90%", "Over 90% in Profit"),
_100pct: CohortName::new("utxos_over_100pct_in_profit", ">=100%", "Over 100% in Profit"),
_200pct: CohortName::new("utxos_over_200pct_in_profit", ">=200%", "Over 200% in Profit"),
_300pct: CohortName::new("utxos_over_300pct_in_profit", ">=300%", "Over 300% in Profit"),
_500pct: CohortName::new("utxos_over_500pct_in_profit", ">=500%", "Over 500% in Profit"),
};
/// Number of profit thresholds.
@@ -36,7 +36,7 @@ impl Profit<CohortName> {
/// Each is a prefix sum over the profitability ranges, from most profitable down.
#[derive(Default, Clone, Traversable, Serialize)]
pub struct Profit<T> {
pub breakeven: T,
pub all: T,
pub _10pct: T,
pub _20pct: T,
pub _30pct: T,
@@ -59,7 +59,7 @@ impl<T> Profit<T> {
{
let n = &PROFIT_NAMES;
Self {
breakeven: create(n.breakeven.id),
all: create(n.all.id),
_10pct: create(n._10pct.id),
_20pct: create(n._20pct.id),
_30pct: create(n._30pct.id),
@@ -82,7 +82,7 @@ impl<T> Profit<T> {
{
let n = &PROFIT_NAMES;
Ok(Self {
breakeven: create(n.breakeven.id)?,
all: create(n.all.id)?,
_10pct: create(n._10pct.id)?,
_20pct: create(n._20pct.id)?,
_30pct: create(n._30pct.id)?,
@@ -101,7 +101,7 @@ impl<T> Profit<T> {
pub fn iter(&self) -> impl Iterator<Item = &T> {
[
&self.breakeven,
&self.all,
&self._10pct,
&self._20pct,
&self._30pct,
@@ -121,7 +121,7 @@ impl<T> Profit<T> {
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
[
&mut self.breakeven,
&mut self.all,
&mut self._10pct,
&mut self._20pct,
&mut self._30pct,
@@ -144,7 +144,7 @@ impl<T> Profit<T> {
T: Send + Sync,
{
[
&mut self.breakeven,
&mut self.all,
&mut self._10pct,
&mut self._20pct,
&mut self._30pct,
@@ -165,7 +165,7 @@ impl<T> Profit<T> {
/// Access as array for indexed accumulation.
pub fn as_array_mut(&mut self) -> [&mut T; PROFIT_COUNT] {
[
&mut self.breakeven,
&mut self.all,
&mut self._10pct,
&mut self._20pct,
&mut self._30pct,
+25 -25
View File
@@ -83,31 +83,31 @@ pub fn compute_profitability_boundaries(spot: Cents) -> [Cents; PROFITABILITY_BO
/// Profitability range names (25 ranges, from most profitable to most in loss)
pub const PROFITABILITY_RANGE_NAMES: ProfitabilityRange<CohortName> = ProfitabilityRange {
over_1000pct_in_profit: CohortName::new("utxos_over_1000pct_in_profit", ">1000%", "Over 1000% Profit"),
_500pct_to_1000pct_in_profit: CohortName::new("utxos_500pct_to_1000pct_in_profit", "500-1000%", "500-1000% Profit"),
_300pct_to_500pct_in_profit: CohortName::new("utxos_300pct_to_500pct_in_profit", "300-500%", "300-500% Profit"),
_200pct_to_300pct_in_profit: CohortName::new("utxos_200pct_to_300pct_in_profit", "200-300%", "200-300% Profit"),
_100pct_to_200pct_in_profit: CohortName::new("utxos_100pct_to_200pct_in_profit", "100-200%", "100-200% Profit"),
_90pct_to_100pct_in_profit: CohortName::new("utxos_90pct_to_100pct_in_profit", "90-100%", "90-100% Profit"),
_80pct_to_90pct_in_profit: CohortName::new("utxos_80pct_to_90pct_in_profit", "80-90%", "80-90% Profit"),
_70pct_to_80pct_in_profit: CohortName::new("utxos_70pct_to_80pct_in_profit", "70-80%", "70-80% Profit"),
_60pct_to_70pct_in_profit: CohortName::new("utxos_60pct_to_70pct_in_profit", "60-70%", "60-70% Profit"),
_50pct_to_60pct_in_profit: CohortName::new("utxos_50pct_to_60pct_in_profit", "50-60%", "50-60% Profit"),
_40pct_to_50pct_in_profit: CohortName::new("utxos_40pct_to_50pct_in_profit", "40-50%", "40-50% Profit"),
_30pct_to_40pct_in_profit: CohortName::new("utxos_30pct_to_40pct_in_profit", "30-40%", "30-40% Profit"),
_20pct_to_30pct_in_profit: CohortName::new("utxos_20pct_to_30pct_in_profit", "20-30%", "20-30% Profit"),
_10pct_to_20pct_in_profit: CohortName::new("utxos_10pct_to_20pct_in_profit", "10-20%", "10-20% Profit"),
_0pct_to_10pct_in_profit: CohortName::new("utxos_0pct_to_10pct_in_profit", "0-10%", "0-10% Profit"),
_0pct_to_10pct_in_loss: CohortName::new("utxos_0pct_to_10pct_in_loss", "0-10%L", "0-10% Loss"),
_10pct_to_20pct_in_loss: CohortName::new("utxos_10pct_to_20pct_in_loss", "10-20%L", "10-20% Loss"),
_20pct_to_30pct_in_loss: CohortName::new("utxos_20pct_to_30pct_in_loss", "20-30%L", "20-30% Loss"),
_30pct_to_40pct_in_loss: CohortName::new("utxos_30pct_to_40pct_in_loss", "30-40%L", "30-40% Loss"),
_40pct_to_50pct_in_loss: CohortName::new("utxos_40pct_to_50pct_in_loss", "40-50%L", "40-50% Loss"),
_50pct_to_60pct_in_loss: CohortName::new("utxos_50pct_to_60pct_in_loss", "50-60%L", "50-60% Loss"),
_60pct_to_70pct_in_loss: CohortName::new("utxos_60pct_to_70pct_in_loss", "60-70%L", "60-70% Loss"),
_70pct_to_80pct_in_loss: CohortName::new("utxos_70pct_to_80pct_in_loss", "70-80%L", "70-80% Loss"),
_80pct_to_90pct_in_loss: CohortName::new("utxos_80pct_to_90pct_in_loss", "80-90%L", "80-90% Loss"),
_90pct_to_100pct_in_loss: CohortName::new("utxos_90pct_to_100pct_in_loss", "90-100%L", "90-100% Loss"),
over_1000pct_in_profit: CohortName::new("utxos_over_1000pct_in_profit", "+>1000%", "Over 1000% in Profit"),
_500pct_to_1000pct_in_profit: CohortName::new("utxos_500pct_to_1000pct_in_profit", "+500-1000%", "500-1000% in Profit"),
_300pct_to_500pct_in_profit: CohortName::new("utxos_300pct_to_500pct_in_profit", "+300-500%", "300-500% in Profit"),
_200pct_to_300pct_in_profit: CohortName::new("utxos_200pct_to_300pct_in_profit", "+200-300%", "200-300% in Profit"),
_100pct_to_200pct_in_profit: CohortName::new("utxos_100pct_to_200pct_in_profit", "+100-200%", "100-200% in Profit"),
_90pct_to_100pct_in_profit: CohortName::new("utxos_90pct_to_100pct_in_profit", "+90-100%", "90-100% in Profit"),
_80pct_to_90pct_in_profit: CohortName::new("utxos_80pct_to_90pct_in_profit", "+80-90%", "80-90% in Profit"),
_70pct_to_80pct_in_profit: CohortName::new("utxos_70pct_to_80pct_in_profit", "+70-80%", "70-80% in Profit"),
_60pct_to_70pct_in_profit: CohortName::new("utxos_60pct_to_70pct_in_profit", "+60-70%", "60-70% in Profit"),
_50pct_to_60pct_in_profit: CohortName::new("utxos_50pct_to_60pct_in_profit", "+50-60%", "50-60% in Profit"),
_40pct_to_50pct_in_profit: CohortName::new("utxos_40pct_to_50pct_in_profit", "+40-50%", "40-50% in Profit"),
_30pct_to_40pct_in_profit: CohortName::new("utxos_30pct_to_40pct_in_profit", "+30-40%", "30-40% in Profit"),
_20pct_to_30pct_in_profit: CohortName::new("utxos_20pct_to_30pct_in_profit", "+20-30%", "20-30% in Profit"),
_10pct_to_20pct_in_profit: CohortName::new("utxos_10pct_to_20pct_in_profit", "+10-20%", "10-20% in Profit"),
_0pct_to_10pct_in_profit: CohortName::new("utxos_0pct_to_10pct_in_profit", "+0-10%", "0-10% in Profit"),
_0pct_to_10pct_in_loss: CohortName::new("utxos_0pct_to_10pct_in_loss", "-0-10%", "0-10% in Loss"),
_10pct_to_20pct_in_loss: CohortName::new("utxos_10pct_to_20pct_in_loss", "-10-20%", "10-20% in Loss"),
_20pct_to_30pct_in_loss: CohortName::new("utxos_20pct_to_30pct_in_loss", "-20-30%", "20-30% in Loss"),
_30pct_to_40pct_in_loss: CohortName::new("utxos_30pct_to_40pct_in_loss", "-30-40%", "30-40% in Loss"),
_40pct_to_50pct_in_loss: CohortName::new("utxos_40pct_to_50pct_in_loss", "-40-50%", "40-50% in Loss"),
_50pct_to_60pct_in_loss: CohortName::new("utxos_50pct_to_60pct_in_loss", "-50-60%", "50-60% in Loss"),
_60pct_to_70pct_in_loss: CohortName::new("utxos_60pct_to_70pct_in_loss", "-60-70%", "60-70% in Loss"),
_70pct_to_80pct_in_loss: CohortName::new("utxos_70pct_to_80pct_in_loss", "-70-80%", "70-80% in Loss"),
_80pct_to_90pct_in_loss: CohortName::new("utxos_80pct_to_90pct_in_loss", "-80-90%", "80-90% in Loss"),
_90pct_to_100pct_in_loss: CohortName::new("utxos_90pct_to_100pct_in_loss", "-90-100%", "90-100% in Loss"),
};
impl ProfitabilityRange<CohortName> {
@@ -27,8 +27,8 @@ impl PercentilesVecs {
let vecs = PERCENTILES
.into_iter()
.map(|p| {
let metric_name = format!("{prefix}_pct{p:02}");
Price::forced_import(db, &metric_name, version + VERSION, indexes)
let series_name = format!("{prefix}_pct{p:02}");
Price::forced_import(db, &series_name, version + VERSION, indexes)
})
.collect::<Result<Vec<_>>>()?
.try_into()
@@ -52,13 +52,13 @@ impl RatioPerBlock<BasisPoints32> {
&mut self,
starting_indexes: &Indexes,
close_price: &impl ReadableVec<Height, Cents>,
metric_price: &impl ReadableVec<Height, Cents>,
series_price: &impl ReadableVec<Height, Cents>,
exit: &Exit,
) -> Result<()> {
self.bps.height.compute_transform2(
starting_indexes.height,
close_price,
metric_price,
series_price,
|(i, close, price, ..)| {
if price == Cents::ZERO {
(i, BasisPoints32::from(1.0))
@@ -81,7 +81,7 @@ impl RatioPerBlockPercentiles {
starting_indexes: &Indexes,
exit: &Exit,
ratio_source: &impl ReadableVec<Height, StoredF32>,
metric_price: &impl ReadableVec<Height, Cents>,
series_price: &impl ReadableVec<Height, Cents>,
) -> Result<()> {
let ratio_version = ratio_source.version();
self.mut_pct_vecs().try_for_each(|v| -> Result<()> {
@@ -147,7 +147,7 @@ impl RatioPerBlockPercentiles {
.cents
.compute_binary::<Cents, BasisPoints32, PriceTimesRatioBp32Cents>(
starting_indexes.height,
metric_price,
series_price,
&self.$band.ratio.bps.height,
exit,
)?;
@@ -53,7 +53,7 @@ impl RatioPerBlockStdDevBands {
starting_indexes: &Indexes,
exit: &Exit,
ratio_source: &impl ReadableVec<Height, StoredF32>,
metric_price: &impl ReadableVec<Height, Cents>,
series_price: &impl ReadableVec<Height, Cents>,
sma: &RatioSma,
) -> Result<()> {
for (sd, sma_ratio) in [
@@ -63,7 +63,7 @@ impl RatioPerBlockStdDevBands {
(&mut self._1y, &sma._1y.ratio.height),
] {
sd.compute_all(blocks, starting_indexes, exit, ratio_source, sma_ratio)?;
sd.compute_cents_bands(starting_indexes, metric_price, sma_ratio, exit)?;
sd.compute_cents_bands(starting_indexes, series_price, sma_ratio, exit)?;
}
Ok(())
@@ -179,7 +179,7 @@ impl StdDevPerBlockExtended {
pub(crate) fn compute_cents_bands(
&mut self,
starting_indexes: &Indexes,
metric_price: &impl ReadableVec<Height, Cents>,
series_price: &impl ReadableVec<Height, Cents>,
sma: &impl ReadableVec<Height, StoredF32>,
exit: &Exit,
) -> Result<()> {
@@ -189,7 +189,7 @@ impl StdDevPerBlockExtended {
.cents
.compute_binary::<Cents, StoredF32, PriceTimesRatioCents>(
starting_indexes.height,
metric_price,
series_price,
$band_source,
exit,
)?;
+18 -18
View File
@@ -126,15 +126,15 @@ pub enum Error {
#[error("Authentication failed")]
AuthFailed,
// Metric-specific errors
// Series-specific errors
#[error("{0}")]
MetricNotFound(MetricNotFound),
SeriesNotFound(SeriesNotFound),
#[error("'{metric}' doesn't support the requested index. Try: {supported}")]
MetricUnsupportedIndex { metric: String, supported: String },
#[error("'{series}' doesn't support the requested index. Try: {supported}")]
SeriesUnsupportedIndex { series: String, supported: String },
#[error("No metrics specified")]
NoMetrics,
#[error("No series specified")]
NoSeries,
#[error("No data available")]
NoData,
@@ -221,31 +221,31 @@ fn is_io_error_permanent(e: &std::io::Error) -> bool {
}
#[derive(Debug)]
pub struct MetricNotFound {
pub metric: String,
pub struct SeriesNotFound {
pub series: String,
pub suggestions: Vec<String>,
pub total_matches: usize,
}
impl MetricNotFound {
pub fn new(mut metric: String, all_matches: Vec<String>) -> Self {
impl SeriesNotFound {
pub fn new(mut series: String, all_matches: Vec<String>) -> Self {
let total_matches = all_matches.len();
let suggestions = all_matches.into_iter().take(3).collect();
if metric.len() > 100 {
metric.truncate(100);
metric.push_str("...");
if series.len() > 100 {
series.truncate(100);
series.push_str("...");
}
Self {
metric,
series,
suggestions,
total_matches,
}
}
}
impl fmt::Display for MetricNotFound {
impl fmt::Display for SeriesNotFound {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "'{}' not found", self.metric)?;
write!(f, "'{}' not found", self.series)?;
if self.suggestions.is_empty() {
return Ok(());
@@ -258,8 +258,8 @@ impl fmt::Display for MetricNotFound {
if remaining > 0 {
write!(
f,
" ({remaining} more — /api/metrics/search?q={} for all)",
self.metric
" ({remaining} more — /api/series/search?q={} for all)",
self.series
)?;
}
+4 -4
View File
@@ -17,12 +17,12 @@ pub fn main() -> brk_error::Result<()> {
let vecs = Vecs::build(&indexer_ro, &computer_ro);
let out_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("metrics.txt");
let content = vecs.metrics.join("\n");
let out_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("series.txt");
let content = vecs.series.join("\n");
fs::write(&out_path, &content)?;
eprintln!(
"Wrote {} metrics to {}",
vecs.metrics.len(),
"Wrote {} series to {}",
vecs.series.len(),
out_path.display()
);
+4 -4
View File
@@ -67,14 +67,14 @@ pub fn main() -> Result<()> {
"bc1qwzrryqr3ja8w7hnja2spmkgfdcgvqwp5swz4af4ngsjecfz0w0pqud7k38".to_string()
)));
// dbg!(query.search_and_format(MetricSelection {
// dbg!(query.search_and_format(SeriesSelection {
// index: Index::Height,
// metrics: vec!["date"].into(),
// series: vec!["date"].into(),
// range: DataRangeFormat::default().set_from(-1),
// })?);
// dbg!(query.search_and_format(MetricSelection {
// dbg!(query.search_and_format(SeriesSelection {
// index: Index::Height,
// metrics: vec!["date", "timestamp"].into(),
// series: vec!["date", "timestamp"].into(),
// range: DataRangeFormat::default().set_from(-10).set_count(5),
// })?);
+2 -2
View File
@@ -2,10 +2,10 @@ mod address;
mod block;
mod cost_basis;
mod mempool;
mod metrics;
mod series;
mod mining;
mod price;
mod transaction;
pub use block::BLOCK_TXS_PAGE_SIZE;
pub use metrics::ResolvedQuery;
pub use series::ResolvedQuery;
@@ -3,9 +3,9 @@ use std::{collections::BTreeMap, sync::LazyLock};
use brk_error::{Error, Result};
use brk_traversable::TreeNode;
use brk_types::{
Date, DetailedMetricCount, Epoch, Etag, Format, Halving, Height, Index, IndexInfo, LegacyValue,
Limit, Metric, MetricData, MetricInfo, MetricOutput, MetricOutputLegacy, MetricSelection,
Output, OutputLegacy, PaginatedMetrics, Pagination, PaginationIndex, RangeIndex, RangeMap,
Date, DetailedSeriesCount, Epoch, Etag, Format, Halving, Height, Index, IndexInfo, LegacyValue,
Limit, Series, SeriesData, SeriesInfo, SeriesOutput, SeriesOutputLegacy, SeriesSelection,
Output, OutputLegacy, PaginatedSeries, Pagination, PaginationIndex, RangeIndex, RangeMap,
SearchQuery, Timestamp, Version,
};
use parking_lot::RwLock;
@@ -13,7 +13,7 @@ use vecdb::{AnyExportableVec, ReadableVec};
use crate::{
Query,
vecs::{IndexToVec, MetricToVec},
vecs::{IndexToVec, SeriesToVec},
};
/// Monotonic block timestamps → height. Lazily extended as new blocks are indexed.
@@ -26,31 +26,31 @@ const CSV_HEADER_BYTES_PER_COL: usize = 10;
const CSV_CELL_BYTES: usize = 15;
impl Query {
pub fn search_metrics(&self, query: &SearchQuery) -> Vec<&'static str> {
pub fn search_series(&self, query: &SearchQuery) -> Vec<&'static str> {
self.vecs().matches(&query.q, query.limit)
}
pub fn metric_not_found_error(&self, metric: &Metric) -> Error {
// Check if metric exists but with different indexes
if let Some(indexes) = self.vecs().metric_to_indexes(metric.clone()) {
pub fn series_not_found_error(&self, series: &Series) -> Error {
// Check if series exists but with different indexes
if let Some(indexes) = self.vecs().series_to_indexes(series.clone()) {
let supported = indexes
.iter()
.map(|i| format!("/api/metric/{metric}/{}", i.name()))
.map(|i| format!("/api/series/{series}/{}", i.name()))
.collect::<Vec<_>>()
.join(", ");
return Error::MetricUnsupportedIndex {
metric: metric.to_string(),
return Error::SeriesUnsupportedIndex {
series: series.to_string(),
supported,
};
}
// Metric doesn't exist, suggest alternatives
// Series doesn't exist, suggest alternatives
let matches = self
.vecs().matches(metric, Limit::DEFAULT)
.vecs().matches(series, Limit::DEFAULT)
.into_iter()
.map(|s| s.to_string())
.collect();
Error::MetricNotFound(brk_error::MetricNotFound::new(metric.to_string(), matches))
Error::SeriesNotFound(brk_error::SeriesNotFound::new(series.to_string(), matches))
}
pub(crate) fn columns_to_csv(
@@ -107,44 +107,44 @@ impl Query {
Ok(csv)
}
/// Returns the latest value for a single metric as a JSON value.
pub fn latest(&self, metric: &Metric, index: Index) -> Result<serde_json::Value> {
/// Returns the latest value for a single series as a JSON value.
pub fn latest(&self, series: &Series, index: Index) -> Result<serde_json::Value> {
let vec = self
.vecs()
.get(metric, index)
.ok_or_else(|| self.metric_not_found_error(metric))?;
.get(series, index)
.ok_or_else(|| self.series_not_found_error(series))?;
vec.last_json_value().ok_or(Error::NoData)
}
/// Returns the length (total data points) for a single metric.
pub fn len(&self, metric: &Metric, index: Index) -> Result<usize> {
/// Returns the length (total data points) for a single series.
pub fn len(&self, series: &Series, index: Index) -> Result<usize> {
let vec = self
.vecs()
.get(metric, index)
.ok_or_else(|| self.metric_not_found_error(metric))?;
.get(series, index)
.ok_or_else(|| self.series_not_found_error(series))?;
Ok(vec.len())
}
/// Returns the version for a single metric.
pub fn version(&self, metric: &Metric, index: Index) -> Result<Version> {
/// Returns the version for a single series.
pub fn version(&self, series: &Series, index: Index) -> Result<Version> {
let vec = self
.vecs()
.get(metric, index)
.ok_or_else(|| self.metric_not_found_error(metric))?;
.get(series, index)
.ok_or_else(|| self.series_not_found_error(series))?;
Ok(vec.version())
}
/// Search for vecs matching the given metrics and index.
/// Returns error if no metrics requested or any requested metric is not found.
pub fn search(&self, params: &MetricSelection) -> Result<Vec<&'static dyn AnyExportableVec>> {
if params.metrics.is_empty() {
return Err(Error::NoMetrics);
/// Search for vecs matching the given series and index.
/// Returns error if no series requested or any requested series is not found.
pub fn search(&self, params: &SeriesSelection) -> Result<Vec<&'static dyn AnyExportableVec>> {
if params.series.is_empty() {
return Err(Error::NoSeries);
}
let mut vecs = Vec::with_capacity(params.metrics.len());
for metric in params.metrics.iter() {
match self.vecs().get(metric, params.index) {
let mut vecs = Vec::with_capacity(params.series.len());
for series in params.series.iter() {
match self.vecs().get(series, params.index) {
Some(vec) => vecs.push(vec),
None => return Err(self.metric_not_found_error(metric)),
None => return Err(self.series_not_found_error(series)),
}
}
Ok(vecs)
@@ -157,7 +157,7 @@ impl Query {
/// Resolve query metadata without formatting (cheap).
/// Use with `format` for lazy formatting after ETag check.
pub fn resolve(&self, params: MetricSelection, max_weight: usize) -> Result<ResolvedQuery> {
pub fn resolve(&self, params: SeriesSelection, max_weight: usize) -> Result<ResolvedQuery> {
let vecs = self.search(&params)?;
let total = vecs.iter().map(|v| v.len()).min().unwrap_or(0);
@@ -209,7 +209,7 @@ impl Query {
/// Format a resolved query (expensive).
/// Call after ETag/cache checks to avoid unnecessary work.
pub fn format(&self, resolved: ResolvedQuery) -> Result<MetricOutput> {
pub fn format(&self, resolved: ResolvedQuery) -> Result<SeriesOutput> {
let ResolvedQuery {
vecs,
format,
@@ -227,7 +227,7 @@ impl Query {
let count = end.saturating_sub(start);
if vecs.len() == 1 {
let mut buf = Vec::with_capacity(count * 12 + 256);
MetricData::serialize(vecs[0], index, start, end, &mut buf)?;
SeriesData::serialize(vecs[0], index, start, end, &mut buf)?;
Output::Json(buf)
} else {
let mut buf = Vec::with_capacity(count * 12 * vecs.len() + 256);
@@ -236,7 +236,7 @@ impl Query {
if i > 0 {
buf.push(b',');
}
MetricData::serialize(*vec, index, start, end, &mut buf)?;
SeriesData::serialize(*vec, index, start, end, &mut buf)?;
}
buf.push(b']');
Output::Json(buf)
@@ -244,7 +244,7 @@ impl Query {
}
};
Ok(MetricOutput {
Ok(SeriesOutput {
output,
version,
total,
@@ -253,9 +253,9 @@ impl Query {
})
}
/// Format a resolved query as raw data (just the JSON array, no MetricData wrapper).
/// Format a resolved query as raw data (just the JSON array, no SeriesData wrapper).
/// CSV output is identical to `format` (no wrapper distinction for CSV).
pub fn format_raw(&self, resolved: ResolvedQuery) -> Result<MetricOutput> {
pub fn format_raw(&self, resolved: ResolvedQuery) -> Result<SeriesOutput> {
if resolved.format() == Format::CSV {
return self.format(resolved);
}
@@ -268,7 +268,7 @@ impl Query {
let mut buf = Vec::with_capacity(count * 12 + 2);
vecs[0].write_json(Some(start), Some(end), &mut buf)?;
Ok(MetricOutput {
Ok(SeriesOutput {
output: Output::Json(buf),
version,
total,
@@ -277,16 +277,16 @@ impl Query {
})
}
pub fn metric_to_index_to_vec(&self) -> &BTreeMap<&str, IndexToVec<'_>> {
&self.vecs().metric_to_index_to_vec
pub fn series_to_index_to_vec(&self) -> &BTreeMap<&str, IndexToVec<'_>> {
&self.vecs().series_to_index_to_vec
}
pub fn index_to_metric_to_vec(&self) -> &BTreeMap<Index, MetricToVec<'_>> {
&self.vecs().index_to_metric_to_vec
pub fn index_to_series_to_vec(&self) -> &BTreeMap<Index, SeriesToVec<'_>> {
&self.vecs().index_to_series_to_vec
}
pub fn metric_count(&self) -> DetailedMetricCount {
DetailedMetricCount {
pub fn series_count(&self) -> DetailedSeriesCount {
DetailedSeriesCount {
total: self.vecs().counts.clone(),
by_db: self.vecs().counts_by_db.clone(),
}
@@ -296,11 +296,11 @@ impl Query {
&self.vecs().indexes
}
pub fn metrics(&self, pagination: Pagination) -> PaginatedMetrics {
self.vecs().metrics(pagination)
pub fn series_list(&self, pagination: Pagination) -> PaginatedSeries {
self.vecs().series(pagination)
}
pub fn metrics_catalog(&self) -> &TreeNode {
pub fn series_catalog(&self) -> &TreeNode {
self.vecs().catalog()
}
@@ -308,18 +308,18 @@ impl Query {
self.vecs().index_to_ids(paginated_index)
}
pub fn metric_info(&self, metric: &Metric) -> Option<MetricInfo> {
let index_to_vec = self.vecs().metric_to_index_to_vec.get(metric.replace("-", "_").as_str())?;
pub fn series_info(&self, series: &Series) -> Option<SeriesInfo> {
let index_to_vec = self.vecs().series_to_index_to_vec.get(series.replace("-", "_").as_str())?;
let value_type = index_to_vec.values().next()?.value_type_to_string();
let indexes = index_to_vec.keys().copied().collect();
Some(MetricInfo {
Some(SeriesInfo {
indexes,
value_type: value_type.into(),
})
}
pub fn metric_to_indexes(&self, metric: Metric) -> Option<&Vec<Index>> {
self.vecs().metric_to_indexes(metric)
pub fn series_to_indexes(&self, series: Series) -> Option<&Vec<Index>> {
self.vecs().series_to_indexes(series)
}
/// Resolve a RangeIndex to an i64 offset for the given index type.
@@ -379,7 +379,7 @@ impl Query {
}
/// Deprecated - format a resolved query as legacy output (expensive).
pub fn format_legacy(&self, resolved: ResolvedQuery) -> Result<MetricOutputLegacy> {
pub fn format_legacy(&self, resolved: ResolvedQuery) -> Result<SeriesOutputLegacy> {
let ResolvedQuery {
vecs,
format,
@@ -391,7 +391,7 @@ impl Query {
} = resolved;
if vecs.is_empty() {
return Ok(MetricOutputLegacy {
return Ok(SeriesOutputLegacy {
output: OutputLegacy::default(format),
version: Version::ZERO,
total: 0,
@@ -407,14 +407,14 @@ impl Query {
Format::CSV => OutputLegacy::CSV(Self::columns_to_csv(&vecs, start, end)?),
Format::JSON => {
if vecs.len() == 1 {
let metric = vecs[0];
let count = metric.range_count(from, to);
let col = vecs[0];
let count = col.range_count(from, to);
let mut buf = Vec::new();
if count == 1 {
metric.write_json_value(Some(start), &mut buf)?;
col.write_json_value(Some(start), &mut buf)?;
OutputLegacy::Json(LegacyValue::Value(buf))
} else {
metric.write_json(Some(start), Some(end), &mut buf)?;
col.write_json(Some(start), Some(end), &mut buf)?;
OutputLegacy::Json(LegacyValue::List(buf))
}
} else {
@@ -429,7 +429,7 @@ impl Query {
}
};
Ok(MetricOutputLegacy {
Ok(SeriesOutputLegacy {
output,
version,
total,
@@ -439,7 +439,7 @@ impl Query {
}
}
/// A resolved metric query ready for formatting.
/// A resolved series query ready for formatting.
/// Contains the vecs and metadata needed to build an ETag or format the output.
pub struct ResolvedQuery {
pub vecs: Vec<&'static dyn AnyExportableVec>,
@@ -454,7 +454,7 @@ pub struct ResolvedQuery {
impl ResolvedQuery {
pub fn etag(&self) -> Etag {
Etag::from_metric(self.version, self.total, self.start, self.end, self.height)
Etag::from_series(self.version, self.total, self.start, self.end, self.height)
}
pub fn format(&self) -> Format {
+1 -1
View File
@@ -61,7 +61,7 @@ impl Query {
Height::from(self.indexer().vecs.blocks.blockhash.stamp())
}
/// Current computed height (metrics)
/// Current computed height (series)
pub fn computed_height(&self) -> Height {
Height::from(self.computer().distribution.supply_state.len())
}
+40 -40
View File
@@ -4,7 +4,7 @@ use brk_computer::Computer;
use brk_indexer::Indexer;
use brk_traversable::{Traversable, TreeNode};
use brk_types::{
Index, IndexInfo, Limit, Metric, MetricCount, PaginatedMetrics, Pagination, PaginationIndex,
Index, IndexInfo, Limit, PaginatedSeries, Pagination, PaginationIndex, Series, SeriesCount,
};
use derive_more::{Deref, DerefMut};
use quickmatch::{QuickMatch, QuickMatchConfig};
@@ -12,16 +12,16 @@ use vecdb::AnyExportableVec;
#[derive(Default)]
pub struct Vecs<'a> {
pub metric_to_index_to_vec: BTreeMap<&'a str, IndexToVec<'a>>,
pub index_to_metric_to_vec: BTreeMap<Index, MetricToVec<'a>>,
pub metrics: Vec<&'a str>,
pub series_to_index_to_vec: BTreeMap<&'a str, IndexToVec<'a>>,
pub index_to_series_to_vec: BTreeMap<Index, SeriesToVec<'a>>,
pub series: Vec<&'a str>,
pub indexes: Vec<IndexInfo>,
pub counts: MetricCount,
pub counts_by_db: BTreeMap<String, MetricCount>,
pub counts: SeriesCount,
pub counts_by_db: BTreeMap<String, SeriesCount>,
catalog: Option<TreeNode>,
matcher: Option<QuickMatch<'a>>,
metric_to_indexes: BTreeMap<&'a str, Vec<Index>>,
index_to_metrics: BTreeMap<Index, Vec<&'a str>>,
series_to_indexes: BTreeMap<&'a str, Vec<Index>>,
index_to_series: BTreeMap<Index, Vec<&'a str>>,
}
impl<'a> Vecs<'a> {
@@ -55,7 +55,7 @@ impl<'a> Vecs<'a> {
computed_vecs.for_each(|(db, vec)| this.insert(vec, db));
let mut ids = this
.metric_to_index_to_vec
.series_to_index_to_vec
.keys()
.cloned()
.collect::<Vec<_>>();
@@ -73,22 +73,22 @@ impl<'a> Vecs<'a> {
sort_ids(&mut ids);
this.metrics = ids;
this.counts.distinct_metrics = this.metric_to_index_to_vec.keys().count();
this.series = ids;
this.counts.distinct_series = this.series_to_index_to_vec.keys().count();
this.counts.total_endpoints = this
.index_to_metric_to_vec
.index_to_series_to_vec
.values()
.map(|tree| tree.len())
.sum::<usize>();
this.counts.lazy_endpoints = this
.index_to_metric_to_vec
.index_to_series_to_vec
.values()
.flat_map(|tree| tree.values())
.filter(|vec| vec.region_names().is_empty())
.count();
this.counts.stored_endpoints = this.counts.total_endpoints - this.counts.lazy_endpoints;
this.indexes = this
.index_to_metric_to_vec
.index_to_series_to_vec
.keys()
.map(|i| IndexInfo {
index: *i,
@@ -100,17 +100,17 @@ impl<'a> Vecs<'a> {
})
.collect();
this.metric_to_indexes = this
.metric_to_index_to_vec
this.series_to_indexes = this
.series_to_index_to_vec
.iter()
.map(|(id, index_to_vec)| (*id, index_to_vec.keys().copied().collect::<Vec<_>>()))
.collect();
this.index_to_metrics = this
.index_to_metric_to_vec
this.index_to_series = this
.index_to_series_to_vec
.iter()
.map(|(index, id_to_vec)| (*index, id_to_vec.keys().cloned().collect::<Vec<_>>()))
.collect();
this.index_to_metrics.values_mut().for_each(sort_ids);
this.index_to_series.values_mut().for_each(sort_ids);
this.catalog.replace(
TreeNode::Branch(
[
@@ -123,7 +123,7 @@ impl<'a> Vecs<'a> {
.merge_branches()
.unwrap(),
);
this.matcher = Some(QuickMatch::new(&this.metrics));
this.matcher = Some(QuickMatch::new(&this.series));
this
}
@@ -135,23 +135,23 @@ impl<'a> Vecs<'a> {
.unwrap_or_else(|_| panic!("Unknown index type: {serialized_index}"));
let prev = self
.metric_to_index_to_vec
.series_to_index_to_vec
.entry(name)
.or_default()
.insert(index, vec);
assert!(
prev.is_none(),
"Duplicate metric: {name} for index {index:?}"
"Duplicate series: {name} for index {index:?}"
);
let prev = self
.index_to_metric_to_vec
.index_to_series_to_vec
.entry(index)
.or_default()
.insert(name, vec);
assert!(
prev.is_none(),
"Duplicate metric: {name} for index {index:?}"
"Duplicate series: {name} for index {index:?}"
);
// Track per-db counts
@@ -162,36 +162,36 @@ impl<'a> Vecs<'a> {
.add_endpoint(name, is_lazy);
}
pub fn metrics(&'static self, pagination: Pagination) -> PaginatedMetrics {
let len = self.metrics.len();
pub fn series(&'static self, pagination: Pagination) -> PaginatedSeries {
let len = self.series.len();
let per_page = pagination.per_page();
let start = pagination.start(len);
let end = pagination.end(len);
let max_page = len.div_ceil(per_page).saturating_sub(1);
PaginatedMetrics {
PaginatedSeries {
current_page: pagination.page(),
max_page,
total_count: len,
per_page,
has_more: pagination.page() < max_page,
metrics: self.metrics[start..end]
series: self.series[start..end]
.iter()
.map(|&s| Cow::Borrowed(s))
.collect(),
}
}
pub fn metric_to_indexes(&self, metric: Metric) -> Option<&Vec<Index>> {
self.metric_to_indexes
.get(metric.replace("-", "_").as_str())
pub fn series_to_indexes(&self, series: Series) -> Option<&Vec<Index>> {
self.series_to_indexes
.get(series.replace("-", "_").as_str())
}
pub fn index_to_ids(
&self,
PaginationIndex { index, pagination }: PaginationIndex,
) -> Option<&[&'a str]> {
let vec = self.index_to_metrics.get(&index)?;
let vec = self.index_to_series.get(&index)?;
let len = vec.len();
let start = pagination.start(len);
@@ -204,21 +204,21 @@ impl<'a> Vecs<'a> {
self.catalog.as_ref().expect("catalog not initialized")
}
pub fn matches(&self, metric: &Metric, limit: Limit) -> Vec<&'_ str> {
pub fn matches(&self, series: &Series, limit: Limit) -> Vec<&'_ str> {
if limit.is_zero() {
return Vec::new();
}
self.matcher
.as_ref()
.expect("matcher not initialized")
.matches_with(metric, &QuickMatchConfig::new().with_limit(*limit))
.matches_with(series, &QuickMatchConfig::new().with_limit(*limit))
}
/// Look up a vec by metric name and index
pub fn get(&self, metric: &Metric, index: Index) -> Option<&'a dyn AnyExportableVec> {
let metric_name = metric.replace("-", "_");
self.metric_to_index_to_vec
.get(metric_name.as_str())
/// Look up a vec by series name and index
pub fn get(&self, series: &Series, index: Index) -> Option<&'a dyn AnyExportableVec> {
let series_name = series.replace("-", "_");
self.series_to_index_to_vec
.get(series_name.as_str())
.and_then(|index_to_vec| index_to_vec.get(&index).copied())
}
}
@@ -227,4 +227,4 @@ impl<'a> Vecs<'a> {
pub struct IndexToVec<'a>(BTreeMap<Index, &'a dyn AnyExportableVec>);
#[derive(Default, Deref, DerefMut)]
pub struct MetricToVec<'a>(BTreeMap<&'a str, &'a dyn AnyExportableVec>);
pub struct SeriesToVec<'a>(BTreeMap<&'a str, &'a dyn AnyExportableVec>);
@@ -10,55 +10,52 @@ use axum::{
use brk_traversable::TreeNode;
use brk_types::{
CostBasisCohortParam, CostBasisFormatted, CostBasisParams, CostBasisQuery, DataRangeFormat,
Date, Index, IndexInfo, Metric, MetricCount, MetricData, MetricInfo, MetricParam,
MetricSelection, MetricSelectionLegacy, MetricWithIndex, Metrics, PaginatedMetrics, Pagination,
SearchQuery,
Date, DetailedSeriesCount, Index, IndexInfo, PaginatedSeries, Pagination, SearchQuery, Series,
SeriesData, SeriesInfo, SeriesList, SeriesSelection, SeriesSelectionLegacy,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{CacheStrategy, Error, extended::TransformResponseExtended};
use super::AppState;
use super::series::legacy;
mod bulk;
mod data;
mod legacy;
/// Maximum allowed request weight in bytes (650KB)
const MAX_WEIGHT: usize = 65 * 10_000;
/// Maximum allowed request weight for localhost (50MB)
const MAX_WEIGHT_LOCALHOST: usize = 50 * 1_000_000;
/// Cache control header for metric data responses
const CACHE_CONTROL: &str = "public, max-age=1, must-revalidate";
/// Returns the max weight for a request based on the client address.
/// Localhost requests get a generous limit, external requests get a stricter one.
fn max_weight(addr: &SocketAddr) -> usize {
if addr.ip().is_loopback() {
MAX_WEIGHT_LOCALHOST
} else {
MAX_WEIGHT
}
/// Legacy path parameter for `/api/metric/{metric}`
#[derive(Deserialize, JsonSchema)]
struct LegacySeriesParam {
metric: Series,
}
pub trait ApiMetricsRoutes {
fn add_metrics_routes(self) -> Self;
/// Legacy path parameters for `/api/metric/{metric}/{index}`
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
struct LegacySeriesWithIndex {
metric: Series,
index: Index,
}
impl ApiMetricsRoutes for ApiRouter<AppState> {
fn add_metrics_routes(self) -> Self {
self.api_route(
pub trait ApiMetricsLegacyRoutes {
fn add_metrics_legacy_routes(self) -> Self;
}
impl ApiMetricsLegacyRoutes for ApiRouter<AppState> {
fn add_metrics_legacy_routes(self) -> Self {
self
// --- Deprecated /api/metrics routes ---
.api_route(
"/api/metrics",
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.metrics_catalog().clone())).await
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.series_catalog().clone())).await
},
|op| op
.id("get_metrics_tree")
.id("get_metrics_tree_deprecated")
.metrics_tag()
.summary("Metrics catalog")
.deprecated()
.summary("Metrics catalog (deprecated)")
.description(
"Returns the complete hierarchical catalog of available metrics organized as a tree structure. \
Metrics are grouped by categories and subcategories."
"**DEPRECATED** - Use `/api/series` instead.\n\n\
Sunset date: 2027-01-01."
)
.ok_response::<TreeNode>()
.not_modified(),
@@ -72,14 +69,18 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
headers: HeaderMap,
State(state): State<AppState>
| {
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.metric_count())).await
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.series_count())).await
},
|op| op
.id("get_metrics_count")
.id("get_metrics_count_deprecated")
.metrics_tag()
.summary("Metric count")
.description("Returns the number of metrics available per index type.")
.ok_response::<Vec<MetricCount>>()
.deprecated()
.summary("Metric count (deprecated)")
.description(
"**DEPRECATED** - Use `/api/series/count` instead.\n\n\
Sunset date: 2027-01-01."
)
.ok_response::<DetailedSeriesCount>()
.not_modified(),
),
)
@@ -94,11 +95,13 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.indexes().to_vec())).await
},
|op| op
.id("get_indexes")
.id("get_indexes_deprecated")
.metrics_tag()
.summary("List available indexes")
.deprecated()
.summary("List available indexes (deprecated)")
.description(
"Returns all available indexes with their accepted query aliases. Use any alias when querying metrics."
"**DEPRECATED** - Use `/api/series/indexes` instead.\n\n\
Sunset date: 2027-01-01."
)
.ok_response::<Vec<IndexInfo>>()
.not_modified(),
@@ -113,14 +116,18 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
State(state): State<AppState>,
Query(pagination): Query<Pagination>
| {
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| Ok(q.metrics(pagination))).await
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| Ok(q.series_list(pagination))).await
},
|op| op
.id("list_metrics")
.id("list_metrics_deprecated")
.metrics_tag()
.summary("Metrics list")
.description("Paginated flat list of all available metric names. Use `page` query param for pagination.")
.ok_response::<PaginatedMetrics>()
.deprecated()
.summary("Metrics list (deprecated)")
.description(
"**DEPRECATED** - Use `/api/series/list` instead.\n\n\
Sunset date: 2027-01-01."
)
.ok_response::<PaginatedSeries>()
.not_modified(),
),
)
@@ -133,18 +140,45 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
State(state): State<AppState>,
Query(query): Query<SearchQuery>
| {
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| Ok(q.search_metrics(&query))).await
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| Ok(q.search_series(&query))).await
},
|op| op
.id("search_metrics")
.id("search_metrics_deprecated")
.metrics_tag()
.summary("Search metrics")
.description("Fuzzy search for metrics by name. Supports partial matches and typos.")
.ok_response::<Vec<Metric>>()
.deprecated()
.summary("Search metrics (deprecated)")
.description(
"**DEPRECATED** - Use `/api/series/search` instead.\n\n\
Sunset date: 2027-01-01."
)
.ok_response::<Vec<&str>>()
.not_modified()
.server_error(),
),
)
.api_route(
"/api/metrics/bulk",
get_with(
|uri: Uri, headers: HeaderMap, addr: Extension<SocketAddr>, query: Query<SeriesSelection>, state: State<AppState>| async move {
legacy::handler(uri, headers, addr, query, state)
.await
.into_response()
},
|op| op
.id("get_metrics_bulk_deprecated")
.metrics_tag()
.deprecated()
.summary("Bulk metric data (deprecated)")
.description(
"**DEPRECATED** - Use `/api/series/bulk` instead.\n\n\
Sunset date: 2027-01-01."
)
.ok_response::<Vec<SeriesData>>()
.csv_response()
.not_modified(),
),
)
// --- Deprecated /api/metric/{metric} routes ---
.api_route(
"/api/metric/{metric}",
get_with(
@@ -152,20 +186,22 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
uri: Uri,
headers: HeaderMap,
State(state): State<AppState>,
Path(path): Path<MetricParam>
Path(path): Path<LegacySeriesParam>
| {
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| {
q.metric_info(&path.metric).ok_or_else(|| q.metric_not_found_error(&path.metric))
q.series_info(&path.metric).ok_or_else(|| q.series_not_found_error(&path.metric))
}).await
},
|op| op
.id("get_metric_info")
.id("get_metric_info_deprecated")
.metrics_tag()
.summary("Get metric info")
.deprecated()
.summary("Get metric info (deprecated)")
.description(
"Returns the supported indexes and value type for the specified metric."
"**DEPRECATED** - Use `/api/series/{series}` instead.\n\n\
Sunset date: 2027-01-01."
)
.ok_response::<MetricInfo>()
.ok_response::<SeriesInfo>()
.not_modified()
.not_found()
.server_error(),
@@ -178,28 +214,24 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
headers: HeaderMap,
addr: Extension<SocketAddr>,
state: State<AppState>,
Path(path): Path<MetricWithIndex>,
Path(path): Path<LegacySeriesWithIndex>,
Query(range): Query<DataRangeFormat>|
-> Response {
data::handler(
uri,
headers,
addr,
Query(MetricSelection::from((path.index, path.metric, range))),
state,
)
.await
.into_response()
let params = SeriesSelection::from((path.index, path.metric, range));
legacy::handler(uri, headers, addr, Query(params), state)
.await
.into_response()
},
|op| op
.id("get_metric")
.id("get_metric_deprecated")
.metrics_tag()
.summary("Get metric data")
.deprecated()
.summary("Get metric data (deprecated)")
.description(
"Fetch data for a specific metric at the given index. \
Use query parameters to filter by date range and format (json/csv)."
"**DEPRECATED** - Use `/api/series/{series}/{index}` instead.\n\n\
Sunset date: 2027-01-01."
)
.ok_response::<MetricData>()
.ok_response::<SeriesData>()
.csv_response()
.not_modified()
.not_found(),
@@ -212,26 +244,22 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
headers: HeaderMap,
addr: Extension<SocketAddr>,
state: State<AppState>,
Path(path): Path<MetricWithIndex>,
Path(path): Path<LegacySeriesWithIndex>,
Query(range): Query<DataRangeFormat>|
-> Response {
data::raw_handler(
uri,
headers,
addr,
Query(MetricSelection::from((path.index, path.metric, range))),
state,
)
.await
.into_response()
let params = SeriesSelection::from((path.index, path.metric, range));
legacy::handler(uri, headers, addr, Query(params), state)
.await
.into_response()
},
|op| op
.id("get_metric_data")
.id("get_metric_data_deprecated")
.metrics_tag()
.summary("Get raw metric data")
.deprecated()
.summary("Get raw metric data (deprecated)")
.description(
"Returns just the data array without the MetricData wrapper. \
Supports the same range and format parameters as the standard endpoint."
"**DEPRECATED** - Use `/api/series/{series}/{index}/data` instead.\n\n\
Sunset date: 2027-01-01."
)
.ok_response::<Vec<serde_json::Value>>()
.csv_response()
@@ -245,7 +273,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
async |uri: Uri,
headers: HeaderMap,
State(state): State<AppState>,
Path(path): Path<MetricWithIndex>| {
Path(path): Path<LegacySeriesWithIndex>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
q.latest(&path.metric, path.index)
@@ -253,11 +281,13 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
.await
},
|op| op
.id("get_metric_latest")
.id("get_metric_latest_deprecated")
.metrics_tag()
.summary("Get latest metric value")
.deprecated()
.summary("Get latest metric value (deprecated)")
.description(
"Returns the single most recent value for a metric, unwrapped (not inside a MetricData object)."
"**DEPRECATED** - Use `/api/series/{series}/{index}/latest` instead.\n\n\
Sunset date: 2027-01-01."
)
.ok_response::<serde_json::Value>()
.not_found(),
@@ -269,7 +299,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
async |uri: Uri,
headers: HeaderMap,
State(state): State<AppState>,
Path(path): Path<MetricWithIndex>| {
Path(path): Path<LegacySeriesWithIndex>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
q.len(&path.metric, path.index)
@@ -277,10 +307,14 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
.await
},
|op| op
.id("get_metric_len")
.id("get_metric_len_deprecated")
.metrics_tag()
.summary("Get metric data length")
.description("Returns the total number of data points for a metric at the given index.")
.deprecated()
.summary("Get metric data length (deprecated)")
.description(
"**DEPRECATED** - Use `/api/series/{series}/{index}/len` instead.\n\n\
Sunset date: 2027-01-01."
)
.ok_response::<usize>()
.not_found(),
),
@@ -291,7 +325,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
async |uri: Uri,
headers: HeaderMap,
State(state): State<AppState>,
Path(path): Path<MetricWithIndex>| {
Path(path): Path<LegacySeriesWithIndex>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
q.version(&path.metric, path.index)
@@ -299,34 +333,19 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
.await
},
|op| op
.id("get_metric_version")
.id("get_metric_version_deprecated")
.metrics_tag()
.summary("Get metric version")
.description("Returns the current version of a metric. Changes when the metric data is updated.")
.deprecated()
.summary("Get metric version (deprecated)")
.description(
"**DEPRECATED** - Use `/api/series/{series}/{index}/version` instead.\n\n\
Sunset date: 2027-01-01."
)
.ok_response::<brk_types::Version>()
.not_found(),
),
)
.api_route(
"/api/metrics/bulk",
get_with(
|uri, headers, addr, query, state| async move {
bulk::handler(uri, headers, addr, query, state).await.into_response()
},
|op| op
.id("get_metrics")
.metrics_tag()
.summary("Bulk metric data")
.description(
"Fetch multiple metrics in a single request. Supports filtering by index and date range. \
Returns an array of MetricData objects. For a single metric, use `get_metric` instead."
)
.ok_response::<Vec<MetricData>>()
.csv_response()
.not_modified(),
),
)
// Cost basis distribution endpoints
// --- Deprecated cost basis routes ---
.api_route(
"/api/metrics/cost-basis",
get_with(
@@ -336,10 +355,14 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
.await
},
|op| {
op.id("get_cost_basis_cohorts")
op.id("get_cost_basis_cohorts_deprecated")
.metrics_tag()
.summary("Available cost basis cohorts")
.description("List available cohorts for cost basis distribution.")
.deprecated()
.summary("Available cost basis cohorts (deprecated)")
.description(
"**DEPRECATED** - Use `/api/series/cost-basis` instead.\n\n\
Sunset date: 2027-01-01."
)
.ok_response::<Vec<String>>()
.server_error()
},
@@ -359,10 +382,14 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
.await
},
|op| {
op.id("get_cost_basis_dates")
op.id("get_cost_basis_dates_deprecated")
.metrics_tag()
.summary("Available cost basis dates")
.description("List available dates for a cohort's cost basis distribution.")
.deprecated()
.summary("Available cost basis dates (deprecated)")
.description(
"**DEPRECATED** - Use `/api/series/cost-basis/{cohort}/dates` instead.\n\n\
Sunset date: 2027-01-01."
)
.ok_response::<Vec<Date>>()
.not_found()
.server_error()
@@ -389,14 +416,13 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
.await
},
|op| {
op.id("get_cost_basis")
op.id("get_cost_basis_deprecated")
.metrics_tag()
.summary("Cost basis distribution")
.deprecated()
.summary("Cost basis distribution (deprecated)")
.description(
"Get the cost basis distribution for a cohort on a specific date.\n\n\
Query params:\n\
- `bucket`: raw (default), lin200, lin500, lin1000, log10, log50, log100\n\
- `value`: supply (default, in BTC), realized (USD), unrealized (USD)",
"**DEPRECATED** - Use `/api/series/cost-basis/{cohort}/{date}` instead.\n\n\
Sunset date: 2027-01-01."
)
.ok_response::<CostBasisFormatted>()
.not_found()
@@ -404,7 +430,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
},
),
)
// Deprecated endpoints
// --- Deprecated /api/vecs/ routes (moved from series module) ---
.api_route(
"/api/vecs/{variant}",
get_with(
@@ -426,9 +452,9 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
).into_response();
};
let params = MetricSelection::from((
let params = SeriesSelection::from((
index,
Metrics::from(split.collect::<Vec<_>>().join(separator)),
SeriesList::from(split.collect::<Vec<_>>().join(separator)),
range,
));
legacy::handler(uri, headers, addr, Query(params), state)
@@ -439,10 +465,10 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
.metrics_tag()
.summary("Legacy variant endpoint")
.description(
"**DEPRECATED** - Use `/api/metric/{metric}/{index}` instead.\n\n\
"**DEPRECATED** - Use `/api/series/{series}/{index}` instead.\n\n\
Sunset date: 2027-01-01. May be removed earlier in case of abuse.\n\n\
Legacy endpoint for querying metrics by variant path (e.g., `day1_to_price`). \
Returns raw data without the MetricData wrapper."
Legacy endpoint for querying series by variant path (e.g., `day1_to_price`). \
Returns raw data without the SeriesData wrapper."
)
.deprecated()
.ok_response::<serde_json::Value>()
@@ -455,10 +481,10 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
async |uri: Uri,
headers: HeaderMap,
addr: Extension<SocketAddr>,
Query(params): Query<MetricSelectionLegacy>,
Query(params): Query<SeriesSelectionLegacy>,
state: State<AppState>|
-> Response {
let params: MetricSelection = params.into();
let params: SeriesSelection = params.into();
legacy::handler(uri, headers, addr, Query(params), state)
.await
.into_response()
@@ -467,9 +493,9 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
.metrics_tag()
.summary("Legacy query endpoint")
.description(
"**DEPRECATED** - Use `/api/metric/{metric}/{index}` or `/api/metrics/bulk` instead.\n\n\
"**DEPRECATED** - Use `/api/series/{series}/{index}` or `/api/series/bulk` instead.\n\n\
Sunset date: 2027-01-01. May be removed earlier in case of abuse.\n\n\
Legacy endpoint for querying metrics. Returns raw data without the MetricData wrapper."
Legacy endpoint for querying series. Returns raw data without the SeriesData wrapper."
)
.deprecated()
.ok_response::<serde_json::Value>()
+6 -4
View File
@@ -15,8 +15,8 @@ use crate::{
Error,
api::{
addresses::AddressRoutes, blocks::BlockRoutes, mempool::MempoolRoutes,
metrics::ApiMetricsRoutes, mining::MiningRoutes, server::ServerRoutes,
transactions::TxRoutes,
metrics_legacy::ApiMetricsLegacyRoutes, mining::MiningRoutes,
series::ApiSeriesRoutes, server::ServerRoutes, transactions::TxRoutes,
},
extended::{ResponseExtended, TransformResponseExtended},
};
@@ -26,7 +26,8 @@ use super::AppState;
mod addresses;
mod blocks;
mod mempool;
mod metrics;
mod metrics_legacy;
mod series;
mod mining;
mod openapi;
mod server;
@@ -41,7 +42,8 @@ pub trait ApiRoutes {
impl ApiRoutes for ApiRouter<AppState> {
fn add_api_routes(self) -> Self {
self.add_server_routes()
.add_metrics_routes()
.add_series_routes()
.add_metrics_legacy_routes()
.add_block_routes()
.add_tx_routes()
.add_addresses_routes()
+19 -12
View File
@@ -22,12 +22,12 @@ pub fn create_openapi() -> OpenApi {
let info = Info {
title: "Bitcoin Research Kit".to_string(),
description: Some(
r#"API for querying Bitcoin blockchain data and on-chain metrics.
r#"API for querying Bitcoin blockchain data and on-chain series.
### Features
- **Metrics**: Thousands of time-series metrics across multiple indexes (date, block height, etc.)
- **[Mempool.space](https://mempool.space/docs/api/rest) compatible** (WIP): Most non-metrics endpoints follow the mempool.space API format
- **Series**: Thousands of time-series across multiple indexes (date, block height, etc.)
- **[Mempool.space](https://mempool.space/docs/api/rest) compatible** (WIP): Most non-series endpoints follow the mempool.space API format
- **Multiple formats**: JSON and CSV output
- **LLM-optimized**: [`/llms.txt`](/llms.txt) for discovery, [`/api.json`](/api.json) compact OpenAPI spec for tool use (full spec at [`/openapi.json`](/openapi.json))
@@ -35,9 +35,9 @@ pub fn create_openapi() -> OpenApi {
```bash
curl -s https://bitview.space/api/block-height/0
curl -s https://bitview.space/api/metrics/search?q=price
curl -s https://bitview.space/api/metric/price/day
curl -s https://bitview.space/api/metric/price/day/latest
curl -s https://bitview.space/api/series/search?q=price
curl -s https://bitview.space/api/series/price/day
curl -s https://bitview.space/api/series/price/day/latest
```
### Errors
@@ -48,7 +48,7 @@ All errors return structured JSON with a consistent format:
{
"error": {
"type": "not_found",
"code": "metric_not_found",
"code": "series_not_found",
"message": "'foo' not found, did you mean 'bar'?",
"doc_url": "https://bitcoinresearchkit.org/api"
}
@@ -56,7 +56,7 @@ All errors return structured JSON with a consistent format:
```
- **`type`**: Error category — `invalid_request` (400), `forbidden` (403), `not_found` (404), `unavailable` (503), or `internal` (500)
- **`code`**: Machine-readable error code (e.g. `invalid_address`, `metric_not_found`, `weight_exceeded`)
- **`code`**: Machine-readable error code (e.g. `invalid_address`, `series_not_found`, `weight_exceeded`)
- **`message`**: Human-readable description
- **`doc_url`**: Link to API documentation
@@ -97,13 +97,20 @@ All errors return structured JSON with a consistent format:
),
..Default::default()
},
Tag {
name: "Series".to_string(),
description: Some(
"Access thousands of Bitcoin network time-series data. Query historical statistics \
across various indexes (date, week, month, block height) with JSON or CSV output.\n\n\
**Note:** Series names are subject to change while the project is in active development."
.to_string(),
),
..Default::default()
},
Tag {
name: "Metrics".to_string(),
description: Some(
"Access thousands of Bitcoin network metrics and time-series data. Query historical statistics \
across various indexes (date, week, month, block height) with JSON or CSV output.\n\n\
**Note:** Metric names are subject to change while the project is in active development."
.to_string(),
"Deprecated — use Series".to_string(),
),
..Default::default()
},
@@ -7,11 +7,11 @@ use axum::{
http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
};
use brk_types::{Format, MetricSelection, Output};
use brk_types::{Format, Output, SeriesSelection};
use crate::{
Result,
api::metrics::{CACHE_CONTROL, max_weight},
api::series::{CACHE_CONTROL, max_weight},
extended::{ContentEncoding, HeaderMapExtended},
};
@@ -21,7 +21,7 @@ pub async fn handler(
uri: Uri,
headers: HeaderMap,
Extension(addr): Extension<SocketAddr>,
Query(params): Query<MetricSelection>,
Query(params): Query<SeriesSelection>,
State(state): State<AppState>,
) -> Result<Response> {
// Phase 1: Search and resolve metadata (cheap)
@@ -9,11 +9,11 @@ use axum::{
};
use brk_error::Result as BrkResult;
use brk_query::{Query as BrkQuery, ResolvedQuery};
use brk_types::{Format, MetricOutput, MetricSelection, Output};
use brk_types::{Format, Output, SeriesOutput, SeriesSelection};
use crate::{
Result,
api::metrics::{CACHE_CONTROL, max_weight},
api::series::{CACHE_CONTROL, max_weight},
extended::{ContentEncoding, HeaderMapExtended},
};
@@ -23,7 +23,7 @@ pub async fn handler(
uri: Uri,
headers: HeaderMap,
addr: Extension<SocketAddr>,
Query(params): Query<MetricSelection>,
Query(params): Query<SeriesSelection>,
state: State<AppState>,
) -> Result<Response> {
format_and_respond(uri, headers, addr, params, state, |q, r| q.format(r)).await
@@ -33,7 +33,7 @@ pub async fn raw_handler(
uri: Uri,
headers: HeaderMap,
addr: Extension<SocketAddr>,
Query(params): Query<MetricSelection>,
Query(params): Query<SeriesSelection>,
state: State<AppState>,
) -> Result<Response> {
format_and_respond(uri, headers, addr, params, state, |q, r| q.format_raw(r)).await
@@ -43,9 +43,9 @@ async fn format_and_respond(
uri: Uri,
headers: HeaderMap,
Extension(addr): Extension<SocketAddr>,
params: MetricSelection,
params: SeriesSelection,
state: State<AppState>,
formatter: fn(&BrkQuery, ResolvedQuery) -> BrkResult<MetricOutput>,
formatter: fn(&BrkQuery, ResolvedQuery) -> BrkResult<SeriesOutput>,
) -> Result<Response> {
// Phase 1: Search and resolve metadata (cheap)
let resolved = state
@@ -7,15 +7,15 @@ use axum::{
http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
};
use brk_types::{Format, MetricSelection, OutputLegacy};
use brk_types::{Format, OutputLegacy, SeriesSelection};
use crate::{
Result,
api::metrics::{CACHE_CONTROL, max_weight},
api::series::{CACHE_CONTROL, max_weight},
extended::{ContentEncoding, HeaderMapExtended},
};
const SUNSET: &str = "2027-01-01T00:00:00Z";
pub const SUNSET: &str = "2027-01-01T00:00:00Z";
use super::AppState;
@@ -23,7 +23,7 @@ pub async fn handler(
uri: Uri,
headers: HeaderMap,
Extension(addr): Extension<SocketAddr>,
Query(params): Query<MetricSelection>,
Query(params): Query<SeriesSelection>,
State(state): State<AppState>,
) -> Result<Response> {
// Phase 1: Search and resolve metadata (cheap)
+407
View File
@@ -0,0 +1,407 @@
use std::net::SocketAddr;
use aide::axum::{ApiRouter, routing::get_with};
use axum::{
Extension,
extract::{Path, Query, State},
http::{HeaderMap, Uri},
response::{IntoResponse, Response},
};
use brk_traversable::TreeNode;
use brk_types::{
CostBasisCohortParam, CostBasisFormatted, CostBasisParams, CostBasisQuery, DataRangeFormat,
Date, IndexInfo, PaginatedSeries, Pagination, SearchQuery, SeriesCount, SeriesData,
SeriesInfo, SeriesParam, SeriesSelection, SeriesWithIndex,
};
use crate::{CacheStrategy, extended::TransformResponseExtended};
use super::AppState;
mod bulk;
mod data;
pub mod legacy;
/// Maximum allowed request weight in bytes (650KB)
const MAX_WEIGHT: usize = 65 * 10_000;
/// Maximum allowed request weight for localhost (50MB)
const MAX_WEIGHT_LOCALHOST: usize = 50 * 1_000_000;
/// Cache control header for series data responses
const CACHE_CONTROL: &str = "public, max-age=1, must-revalidate";
/// Returns the max weight for a request based on the client address.
/// Localhost requests get a generous limit, external requests get a stricter one.
fn max_weight(addr: &SocketAddr) -> usize {
if addr.ip().is_loopback() {
MAX_WEIGHT_LOCALHOST
} else {
MAX_WEIGHT
}
}
pub trait ApiSeriesRoutes {
fn add_series_routes(self) -> Self;
}
impl ApiSeriesRoutes for ApiRouter<AppState> {
fn add_series_routes(self) -> Self {
self.api_route(
"/api/series",
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.series_catalog().clone())).await
},
|op| op
.id("get_series_tree")
.series_tag()
.summary("Series catalog")
.description(
"Returns the complete hierarchical catalog of available series organized as a tree structure. \
Series are grouped by categories and subcategories."
)
.ok_response::<TreeNode>()
.not_modified(),
),
)
.api_route(
"/api/series/count",
get_with(
async |
uri: Uri,
headers: HeaderMap,
State(state): State<AppState>
| {
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.series_count())).await
},
|op| op
.id("get_series_count")
.series_tag()
.summary("Series count")
.description("Returns the number of series available per index type.")
.ok_response::<Vec<SeriesCount>>()
.not_modified(),
),
)
.api_route(
"/api/series/indexes",
get_with(
async |
uri: Uri,
headers: HeaderMap,
State(state): State<AppState>
| {
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.indexes().to_vec())).await
},
|op| op
.id("get_indexes")
.series_tag()
.summary("List available indexes")
.description(
"Returns all available indexes with their accepted query aliases. Use any alias when querying series."
)
.ok_response::<Vec<IndexInfo>>()
.not_modified(),
),
)
.api_route(
"/api/series/list",
get_with(
async |
uri: Uri,
headers: HeaderMap,
State(state): State<AppState>,
Query(pagination): Query<Pagination>
| {
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| Ok(q.series_list(pagination))).await
},
|op| op
.id("list_series")
.series_tag()
.summary("Series list")
.description("Paginated flat list of all available series names. Use `page` query param for pagination.")
.ok_response::<PaginatedSeries>()
.not_modified(),
),
)
.api_route(
"/api/series/search",
get_with(
async |
uri: Uri,
headers: HeaderMap,
State(state): State<AppState>,
Query(query): Query<SearchQuery>
| {
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| Ok(q.search_series(&query))).await
},
|op| op
.id("search_series")
.series_tag()
.summary("Search series")
.description("Fuzzy search for series by name. Supports partial matches and typos.")
.ok_response::<Vec<&str>>()
.not_modified()
.server_error(),
),
)
.api_route(
"/api/series/{series}",
get_with(
async |
uri: Uri,
headers: HeaderMap,
State(state): State<AppState>,
Path(path): Path<SeriesParam>
| {
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| {
q.series_info(&path.series).ok_or_else(|| q.series_not_found_error(&path.series))
}).await
},
|op| op
.id("get_series_info")
.series_tag()
.summary("Get series info")
.description(
"Returns the supported indexes and value type for the specified series."
)
.ok_response::<SeriesInfo>()
.not_modified()
.not_found()
.server_error(),
),
)
.api_route(
"/api/series/{series}/{index}",
get_with(
async |uri: Uri,
headers: HeaderMap,
addr: Extension<SocketAddr>,
state: State<AppState>,
Path(path): Path<SeriesWithIndex>,
Query(range): Query<DataRangeFormat>|
-> Response {
data::handler(
uri,
headers,
addr,
Query(SeriesSelection::from((path.index, path.series, range))),
state,
)
.await
.into_response()
},
|op| op
.id("get_series")
.series_tag()
.summary("Get series data")
.description(
"Fetch data for a specific series at the given index. \
Use query parameters to filter by date range and format (json/csv)."
)
.ok_response::<SeriesData>()
.csv_response()
.not_modified()
.not_found(),
),
)
.api_route(
"/api/series/{series}/{index}/data",
get_with(
async |uri: Uri,
headers: HeaderMap,
addr: Extension<SocketAddr>,
state: State<AppState>,
Path(path): Path<SeriesWithIndex>,
Query(range): Query<DataRangeFormat>|
-> Response {
data::raw_handler(
uri,
headers,
addr,
Query(SeriesSelection::from((path.index, path.series, range))),
state,
)
.await
.into_response()
},
|op| op
.id("get_series_data")
.series_tag()
.summary("Get raw series data")
.description(
"Returns just the data array without the SeriesData wrapper. \
Supports the same range and format parameters as the standard endpoint."
)
.ok_response::<Vec<serde_json::Value>>()
.csv_response()
.not_modified()
.not_found(),
),
)
.api_route(
"/api/series/{series}/{index}/latest",
get_with(
async |uri: Uri,
headers: HeaderMap,
State(state): State<AppState>,
Path(path): Path<SeriesWithIndex>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
q.latest(&path.series, path.index)
})
.await
},
|op| op
.id("get_series_latest")
.series_tag()
.summary("Get latest series value")
.description(
"Returns the single most recent value for a series, unwrapped (not inside a SeriesData object)."
)
.ok_response::<serde_json::Value>()
.not_found(),
),
)
.api_route(
"/api/series/{series}/{index}/len",
get_with(
async |uri: Uri,
headers: HeaderMap,
State(state): State<AppState>,
Path(path): Path<SeriesWithIndex>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
q.len(&path.series, path.index)
})
.await
},
|op| op
.id("get_series_len")
.series_tag()
.summary("Get series data length")
.description("Returns the total number of data points for a series at the given index.")
.ok_response::<usize>()
.not_found(),
),
)
.api_route(
"/api/series/{series}/{index}/version",
get_with(
async |uri: Uri,
headers: HeaderMap,
State(state): State<AppState>,
Path(path): Path<SeriesWithIndex>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
q.version(&path.series, path.index)
})
.await
},
|op| op
.id("get_series_version")
.series_tag()
.summary("Get series version")
.description("Returns the current version of a series. Changes when the series data is updated.")
.ok_response::<brk_types::Version>()
.not_found(),
),
)
.api_route(
"/api/series/bulk",
get_with(
|uri, headers, addr, query, state| async move {
bulk::handler(uri, headers, addr, query, state).await.into_response()
},
|op| op
.id("get_series_bulk")
.series_tag()
.summary("Bulk series data")
.description(
"Fetch multiple series in a single request. Supports filtering by index and date range. \
Returns an array of SeriesData objects. For a single series, use `get_series` instead."
)
.ok_response::<Vec<SeriesData>>()
.csv_response()
.not_modified(),
),
)
// Cost basis distribution endpoints
.api_route(
"/api/series/cost-basis",
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state
.cached_json(&headers, CacheStrategy::Static, &uri, |q| q.cost_basis_cohorts())
.await
},
|op| {
op.id("get_cost_basis_cohorts")
.series_tag()
.summary("Available cost basis cohorts")
.description("List available cohorts for cost basis distribution.")
.ok_response::<Vec<String>>()
.server_error()
},
),
)
.api_route(
"/api/series/cost-basis/{cohort}/dates",
get_with(
async |uri: Uri,
headers: HeaderMap,
Path(params): Path<CostBasisCohortParam>,
State(state): State<AppState>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
q.cost_basis_dates(&params.cohort)
})
.await
},
|op| {
op.id("get_cost_basis_dates")
.series_tag()
.summary("Available cost basis dates")
.description("List available dates for a cohort's cost basis distribution.")
.ok_response::<Vec<Date>>()
.not_found()
.server_error()
},
),
)
.api_route(
"/api/series/cost-basis/{cohort}/{date}",
get_with(
async |uri: Uri,
headers: HeaderMap,
Path(params): Path<CostBasisParams>,
Query(query): Query<CostBasisQuery>,
State(state): State<AppState>| {
state
.cached_json(&headers, CacheStrategy::Static, &uri, move |q| {
q.cost_basis_formatted(
&params.cohort,
params.date,
query.bucket,
query.value,
)
})
.await
},
|op| {
op.id("get_cost_basis")
.series_tag()
.summary("Cost basis distribution")
.description(
"Get the cost basis distribution for a cohort on a specific date.\n\n\
Query params:\n\
- `bucket`: raw (default), lin200, lin500, lin1000, log10, log50, log100\n\
- `value`: supply (default, in BTC), realized (USD), unrealized (USD)",
)
.ok_response::<CostBasisFormatted>()
.not_found()
.server_error()
},
),
)
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ pub enum CacheStrategy {
/// Etag = VERSION-{height}, Cache-Control: must-revalidate
Height,
/// Static/immutable data (blocks by hash, validate-address, metrics catalog)
/// Static/immutable data (blocks by hash, validate-address, series catalog)
/// Etag = VERSION only, Cache-Control: must-revalidate
Static,
+7 -7
View File
@@ -23,7 +23,7 @@ struct ErrorDetail {
/// Error category: "invalid_request", "forbidden", "not_found", "unavailable", or "internal"
#[schemars(with = "String")]
r#type: &'static str,
/// Machine-readable error code (e.g. "invalid_address", "metric_not_found")
/// Machine-readable error code (e.g. "invalid_address", "series_not_found")
#[schemars(with = "String")]
code: &'static str,
/// Human-readable description
@@ -50,8 +50,8 @@ fn error_status(e: &BrkError) -> StatusCode {
| BrkError::InvalidAddress
| BrkError::UnsupportedType(_)
| BrkError::Parse(_)
| BrkError::NoMetrics
| BrkError::MetricUnsupportedIndex { .. }
| BrkError::NoSeries
| BrkError::SeriesUnsupportedIndex { .. }
| BrkError::WeightExceeded { .. } => StatusCode::BAD_REQUEST,
BrkError::UnknownAddress
@@ -59,7 +59,7 @@ fn error_status(e: &BrkError) -> StatusCode {
| BrkError::NotFound(_)
| BrkError::NoData
| BrkError::OutOfRange(_)
| BrkError::MetricNotFound(_) => StatusCode::NOT_FOUND,
| BrkError::SeriesNotFound(_) => StatusCode::NOT_FOUND,
BrkError::AuthFailed => StatusCode::FORBIDDEN,
BrkError::MempoolNotAvailable => StatusCode::SERVICE_UNAVAILABLE,
@@ -75,15 +75,15 @@ fn error_code(e: &BrkError) -> &'static str {
BrkError::InvalidNetwork => "invalid_network",
BrkError::UnsupportedType(_) => "unsupported_type",
BrkError::Parse(_) => "parse_error",
BrkError::NoMetrics => "no_metrics",
BrkError::MetricUnsupportedIndex { .. } => "metric_unsupported_index",
BrkError::NoSeries => "no_series",
BrkError::SeriesUnsupportedIndex { .. } => "series_unsupported_index",
BrkError::WeightExceeded { .. } => "weight_exceeded",
BrkError::UnknownAddress => "unknown_address",
BrkError::UnknownTxid => "unknown_txid",
BrkError::NotFound(_) => "not_found",
BrkError::OutOfRange(_) => "out_of_range",
BrkError::NoData => "no_data",
BrkError::MetricNotFound(_) => "metric_not_found",
BrkError::SeriesNotFound(_) => "series_not_found",
BrkError::MempoolNotAvailable => "mempool_not_available",
BrkError::AuthFailed => "auth_failed",
_ => "internal_error",
@@ -11,6 +11,7 @@ pub trait TransformResponseExtended<'t> {
fn mempool_tag(self) -> Self;
fn metrics_tag(self) -> Self;
fn mining_tag(self) -> Self;
fn series_tag(self) -> Self;
fn server_tag(self) -> Self;
fn transactions_tag(self) -> Self;
@@ -55,6 +56,10 @@ impl<'t> TransformResponseExtended<'t> for TransformOperation<'t> {
self.tag("Metrics")
}
fn series_tag(self) -> Self {
self.tag("Series")
}
fn mining_tag(self) -> Self {
self.tag("Mining")
}
+4 -4
View File
@@ -1,6 +1,6 @@
use std::{collections::BTreeMap, fmt::Display};
pub use brk_types::{Index, MetricLeaf, MetricLeafWithSchema, TreeNode};
pub use brk_types::{Index, SeriesLeaf, SeriesLeafWithSchema, TreeNode};
pub use indexmap::IndexMap;
#[cfg(feature = "derive")]
@@ -18,13 +18,13 @@ pub trait Traversable {
fn iter_any_exportable(&self) -> impl Iterator<Item = &dyn AnyExportableVec>;
}
/// Helper to create a MetricLeafWithSchema from a vec
/// Helper to create a SeriesLeafWithSchema from a vec
fn make_leaf<I: VecIndex, T: JsonSchema, V: AnyVec>(vec: &V) -> TreeNode {
let index_str = I::to_string();
let index = Index::try_from(index_str).ok();
let indexes = index.into_iter().collect();
let leaf = MetricLeaf::new(
let leaf = SeriesLeaf::new(
vec.name().to_string(),
vec.value_type_to_string().to_string(),
indexes,
@@ -33,7 +33,7 @@ fn make_leaf<I: VecIndex, T: JsonSchema, V: AnyVec>(vec: &V) -> TreeNode {
let schema = schemars::SchemaGenerator::default().into_root_schema_for::<T>();
let schema_json = serde_json::to_value(schema).unwrap_or_default();
TreeNode::Leaf(MetricLeafWithSchema::new(leaf, schema_json))
TreeNode::Leaf(SeriesLeafWithSchema::new(leaf, schema_json))
}
// BytesVec implementation
+4 -4
View File
@@ -40,14 +40,14 @@ impl From<&str> for Etag {
}
impl Etag {
/// Create ETag from metric data response info.
/// Create ETag from series data response info.
///
/// Format varies based on whether the slice touches the end:
/// - Slice ends before total: `{version:x}-{start}-{end}` (len irrelevant, data won't change if metric grows)
/// - Slice ends before total: `{version:x}-{start}-{end}` (len irrelevant, data won't change if series grows)
/// - Slice reaches the end: `{version:x}-{start}-{total}-{height}` (includes height since last value may be recomputed each block)
///
/// `version` is the metric version for single queries, or the sum of versions for bulk queries.
pub fn from_metric(
/// `version` is the series version for single queries, or the sum of versions for bulk queries.
pub fn from_series(
version: super::Version,
total: usize,
start: usize,
+1 -1
View File
@@ -15,7 +15,7 @@ use super::{
minute10::MINUTE10_INTERVAL, minute30::MINUTE30_INTERVAL, timestamp::INDEX_EPOCH,
};
/// Aggregation dimension for querying metrics. Includes time-based (date, week, month, year),
/// Aggregation dimension for querying series. Includes time-based (date, week, month, year),
/// block-based (height, tx_index), and address/output type indexes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
+22 -22
View File
@@ -89,17 +89,17 @@ mod limit_param;
mod mempool_block;
mod mempool_entry_info;
mod mempool_info;
mod metric;
mod metric_count;
mod metric_data;
mod metric_info;
mod metric_output;
mod metric_param;
mod metrics;
mod metric_selection;
mod metric_selection_legacy;
mod metrics_paginated;
mod metric_with_index;
mod series;
mod series_count;
mod series_data;
mod series_info;
mod series_output;
mod series_param;
mod series_list;
mod series_selection;
mod series_selection_legacy;
mod series_paginated;
mod series_with_index;
mod minute10;
mod minute30;
mod month1;
@@ -284,17 +284,17 @@ pub use limit_param::*;
pub use mempool_block::*;
pub use mempool_entry_info::*;
pub use mempool_info::*;
pub use metric::*;
pub use metric_count::*;
pub use metric_data::*;
pub use metric_info::*;
pub use metric_output::*;
pub use metric_param::*;
pub use metrics::*;
pub use metric_selection::*;
pub use metric_selection_legacy::*;
pub use metrics_paginated::*;
pub use metric_with_index::*;
pub use series::*;
pub use series_count::*;
pub use series_data::*;
pub use series_info::*;
pub use series_output::*;
pub use series_param::*;
pub use series_list::*;
pub use series_selection::*;
pub use series_selection_legacy::*;
pub use series_paginated::*;
pub use series_with_index::*;
pub use minute10::*;
pub use minute30::*;
pub use month1::*;
-43
View File
@@ -1,43 +0,0 @@
use derive_more::Deref;
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{DataRangeFormat, Index, Metric, Metrics};
/// Selection of metrics to query
#[derive(Debug, Deref, Deserialize, JsonSchema)]
pub struct MetricSelection {
/// Requested metrics
#[serde(alias = "m")]
pub metrics: Metrics,
/// Index to query
#[serde(alias = "i")]
pub index: Index,
#[deref]
#[serde(flatten)]
pub range: DataRangeFormat,
}
impl From<(Index, Metric, DataRangeFormat)> for MetricSelection {
#[inline]
fn from((index, metric, range): (Index, Metric, DataRangeFormat)) -> Self {
Self {
index,
metrics: Metrics::from(metric),
range,
}
}
}
impl From<(Index, Metrics, DataRangeFormat)> for MetricSelection {
#[inline]
fn from((index, metrics, range): (Index, Metrics, DataRangeFormat)) -> Self {
Self {
index,
metrics,
range,
}
}
}
@@ -1,26 +0,0 @@
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{DataRangeFormat, Index, MetricSelection, Metrics};
/// Legacy metric selection parameters (deprecated)
#[derive(Debug, Deserialize, JsonSchema)]
pub struct MetricSelectionLegacy {
#[serde(alias = "i")]
pub index: Index,
#[serde(alias = "v")]
pub ids: Metrics,
#[serde(flatten)]
pub range: DataRangeFormat,
}
impl From<MetricSelectionLegacy> for MetricSelection {
#[inline]
fn from(value: MetricSelectionLegacy) -> Self {
MetricSelection {
index: value.index,
metrics: value.ids,
range: value.range,
}
}
}
-37
View File
@@ -1,37 +0,0 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{Index, Metric};
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct MetricWithIndex {
/// Metric name
pub metric: Metric,
/// Aggregation index
pub index: Index,
}
impl MetricWithIndex {
pub fn new(metric: impl Into<Metric>, index: Index) -> Self {
Self {
metric: metric.into(),
index,
}
}
}
impl From<(Metric, Index)> for MetricWithIndex {
fn from((metric, index): (Metric, Index)) -> Self {
Self { metric, index }
}
}
impl From<(&str, Index)> for MetricWithIndex {
fn from((metric, index): (&str, Index)) -> Self {
Self {
metric: metric.into(),
index,
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::Format;
/// Metric data output format
/// Series data output format
#[derive(Debug)]
pub enum Output {
Json(Vec<u8>),
+1 -1
View File
@@ -377,7 +377,7 @@ pub enum PoolSlug {
}
impl PoolSlug {
/// Pools with dominance above per-window thresholds get full metrics.
/// Pools with dominance above per-window thresholds get full series.
/// Thresholds: all-time>=1.0%, 1y>=1.0%, 1m>=0.75%, 1w>=0.5%.
/// Generated by `scripts/pool_major_threshold.py`.
pub fn is_major(&self) -> bool {
+2 -2
View File
@@ -1,12 +1,12 @@
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{Limit, Metric};
use crate::{Limit, Series};
#[derive(Debug, Deserialize, JsonSchema)]
pub struct SearchQuery {
/// Search query string
pub q: Metric,
pub q: Series,
/// Maximum number of results
#[serde(default)]
pub limit: Limit,
@@ -4,7 +4,7 @@ use derive_more::Deref;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// Metric name
/// Series name
#[derive(Debug, Clone, Deref, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
#[schemars(
@@ -13,23 +13,23 @@ use serde::{Deserialize, Serialize};
example = &"market_cap",
example = &"realized_price"
)]
pub struct Metric(String);
pub struct Series(String);
impl From<String> for Metric {
impl From<String> for Series {
#[inline]
fn from(metric: String) -> Self {
Self(metric)
fn from(series: String) -> Self {
Self(series)
}
}
impl From<&str> for Metric {
impl From<&str> for Series {
#[inline]
fn from(metric: &str) -> Self {
Self(metric.to_owned())
fn from(series: &str) -> Self {
Self(series.to_owned())
}
}
impl Display for Metric {
impl Display for Series {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
@@ -4,26 +4,26 @@ use rustc_hash::FxHashSet;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// Metric count statistics - distinct metrics and total metric-index combinations
/// Series count statistics - distinct series and total series-index combinations
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MetricCount {
/// Number of unique metrics available (e.g., realized_price, market_cap)
pub struct SeriesCount {
/// Number of unique series available (e.g., realized_price, market_cap)
#[schemars(example = 3141)]
pub distinct_metrics: usize,
/// Total number of metric-index combinations across all timeframes
pub distinct_series: usize,
/// Total number of series-index combinations across all timeframes
#[schemars(example = 21000)]
pub total_endpoints: usize,
/// Number of lazy (computed on-the-fly) metric-index combinations
/// Number of lazy (computed on-the-fly) series-index combinations
#[schemars(example = 5000)]
pub lazy_endpoints: usize,
/// Number of eager (stored on disk) metric-index combinations
/// Number of eager (stored on disk) series-index combinations
#[schemars(example = 16000)]
pub stored_endpoints: usize,
#[serde(skip)]
seen: FxHashSet<String>,
}
impl MetricCount {
impl SeriesCount {
pub fn add_endpoint(&mut self, name: &str, is_lazy: bool) {
self.total_endpoints += 1;
if is_lazy {
@@ -32,17 +32,17 @@ impl MetricCount {
self.stored_endpoints += 1;
}
if self.seen.insert(name.to_string()) {
self.distinct_metrics += 1;
self.distinct_series += 1;
}
}
}
/// Detailed metric count with per-database breakdown
/// Detailed series count with per-database breakdown
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DetailedMetricCount {
pub struct DetailedSeriesCount {
/// Aggregate counts
#[serde(flatten)]
pub total: MetricCount,
pub total: SeriesCount,
/// Per-database breakdown of counts
pub by_db: BTreeMap<String, MetricCount>,
pub by_db: BTreeMap<String, SeriesCount>,
}
@@ -7,20 +7,20 @@ use vecdb::AnySerializableVec;
use super::{Date, Index, Timestamp, Version};
/// Metric data with range information.
/// Series data with range information.
///
/// All metric data endpoints return this structure when format is JSON.
/// This type is not instantiated - use `MetricData::serialize()` to write JSON bytes directly.
/// All series data endpoints return this structure when format is JSON.
/// This type is not instantiated - use `SeriesData::serialize()` to write JSON bytes directly.
#[derive(Debug, JsonSchema, Deserialize)]
pub struct MetricData<T = Value> {
/// Version of the metric data
pub struct SeriesData<T = Value> {
/// Version of the series data
pub version: Version,
/// The index type used for this query
pub index: Index,
/// Value type (e.g. "f32", "u64", "Sats")
#[serde(rename = "type", default)]
pub value_type: String,
/// Total number of data points in the metric
/// Total number of data points in the series
pub total: usize,
/// Start index (inclusive) of the returned range
pub start: usize,
@@ -28,12 +28,12 @@ pub struct MetricData<T = Value> {
pub end: usize,
/// ISO 8601 timestamp of when the response was generated
pub stamp: String,
/// The metric data
/// The series data
pub data: Vec<T>,
}
impl MetricData {
/// Write metric data as JSON to buffer: `{"version":N,"index":"...","total":N,"start":N,"end":N,"stamp":"...","data":[...]}`
impl SeriesData {
/// Write series data as JSON to buffer: `{"version":N,"index":"...","total":N,"start":N,"end":N,"stamp":"...","data":[...]}`
pub fn serialize(
vec: &dyn AnySerializableVec,
index: Index,
@@ -69,13 +69,13 @@ impl MetricData {
}
}
impl<T> MetricData<T> {
impl<T> SeriesData<T> {
/// Returns an iterator over the index range.
pub fn indexes(&self) -> std::ops::Range<usize> {
self.start..self.end
}
/// Returns true if this metric uses a date-based index.
/// Returns true if this series uses a date-based index.
pub fn is_date_based(&self) -> bool {
self.index.is_date_based()
}
@@ -122,16 +122,16 @@ impl<T> MetricData<T> {
}
}
/// Metric data that is guaranteed to use a date-based index.
/// Series data that is guaranteed to use a date-based index.
///
/// This is a newtype around `MetricData<T>` that guarantees `is_date_based()` is true,
/// This is a newtype around `SeriesData<T>` that guarantees `is_date_based()` is true,
/// making date methods infallible.
#[derive(Debug)]
pub struct DateMetricData<T>(MetricData<T>);
pub struct DateSeriesData<T>(SeriesData<T>);
impl<T> DateMetricData<T> {
/// Create a `DateMetricData` from a `MetricData`, returning `Err` if the index is not date-based.
pub fn try_new(inner: MetricData<T>) -> Result<Self, MetricData<T>> {
impl<T> DateSeriesData<T> {
/// Create a `DateSeriesData` from a `SeriesData`, returning `Err` if the index is not date-based.
pub fn try_new(inner: SeriesData<T>) -> Result<Self, SeriesData<T>> {
if inner.is_date_based() {
Ok(Self(inner))
} else {
@@ -139,8 +139,8 @@ impl<T> DateMetricData<T> {
}
}
/// Consume and return the inner `MetricData`.
pub fn into_inner(self) -> MetricData<T> {
/// Consume and return the inner `SeriesData`.
pub fn into_inner(self) -> SeriesData<T> {
self.0
}
@@ -161,7 +161,7 @@ impl<T> DateMetricData<T> {
pub fn timestamps(&self) -> impl Iterator<Item = Timestamp> + '_ {
self.0
.timestamps()
.expect("DateMetricData is always date-based")
.expect("DateSeriesData is always date-based")
}
/// Iterate over (timestamp, &value) pairs (infallible).
@@ -169,23 +169,23 @@ impl<T> DateMetricData<T> {
pub fn iter_timestamps(&self) -> impl Iterator<Item = (Timestamp, &T)> + '_ {
self.0
.iter_timestamps()
.expect("DateMetricData is always date-based")
.expect("DateSeriesData is always date-based")
}
}
impl<T> Deref for DateMetricData<T> {
type Target = MetricData<T>;
impl<T> Deref for DateSeriesData<T> {
type Target = SeriesData<T>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'de, T: DeserializeOwned> Deserialize<'de> for DateMetricData<T> {
impl<'de, T: DeserializeOwned> Deserialize<'de> for DateSeriesData<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let inner = MetricData::<T>::deserialize(deserializer)?;
let inner = SeriesData::<T>::deserialize(deserializer)?;
Self::try_new(inner).map_err(|m| {
serde::de::Error::custom(format!("expected date-based index, got {:?}", m.index))
})
@@ -5,9 +5,9 @@ use serde::{Deserialize, Serialize};
use crate::Index;
/// Metadata about a metric
/// Metadata about a series
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct MetricInfo {
pub struct SeriesInfo {
/// Available indexes
pub indexes: Vec<Index>,
/// Value type (e.g. "f32", "u64", "Sats")
@@ -4,9 +4,9 @@ use derive_more::Deref;
use schemars::JsonSchema;
use serde::Deserialize;
use super::Metric;
use super::Series;
/// Comma-separated list of metric names
/// Comma-separated list of series names
#[derive(Debug, Deref, JsonSchema)]
#[schemars(
with = "String",
@@ -15,38 +15,38 @@ use super::Metric;
example = &"price_close,market_cap",
example = &"realized_price,market_cap,mvrv"
)]
pub struct Metrics(Vec<Metric>);
pub struct SeriesList(Vec<Series>);
const MAX_VECS: usize = 32;
const MAX_STRING_SIZE: usize = 64 * MAX_VECS;
impl From<Metric> for Metrics {
impl From<Series> for SeriesList {
#[inline]
fn from(metric: Metric) -> Self {
Self(vec![metric])
fn from(series: Series) -> Self {
Self(vec![series])
}
}
impl From<String> for Metrics {
impl From<String> for SeriesList {
#[inline]
fn from(value: String) -> Self {
Self::from(Metric::from(value.replace("-", "_").to_lowercase()))
Self::from(Series::from(value.replace("-", "_").to_lowercase()))
}
}
impl<'a> From<Vec<&'a str>> for Metrics {
impl<'a> From<Vec<&'a str>> for SeriesList {
#[inline]
fn from(value: Vec<&'a str>) -> Self {
Self(
value
.iter()
.map(|s| Metric::from(s.replace("-", "_").to_lowercase()))
.map(|s| Series::from(s.replace("-", "_").to_lowercase()))
.collect::<Vec<_>>(),
)
}
}
impl<'de> Deserialize<'de> for Metrics {
impl<'de> Deserialize<'de> for SeriesList {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
@@ -58,7 +58,7 @@ impl<'de> Deserialize<'de> for Metrics {
Ok(Self(
sanitize(str.split(",").map(|s| s.to_string()))
.into_iter()
.map(Metric::from)
.map(Series::from)
.collect(),
))
} else {
@@ -69,7 +69,7 @@ impl<'de> Deserialize<'de> for Metrics {
Ok(Self(
sanitize(vec.iter().filter_map(|s| s.as_str().map(String::from)))
.into_iter()
.map(Metric::from)
.map(Series::from)
.collect(),
))
} else {
@@ -81,7 +81,7 @@ impl<'de> Deserialize<'de> for Metrics {
}
}
impl fmt::Display for Metrics {
impl fmt::Display for SeriesList {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = self
.0
@@ -1,8 +1,8 @@
use crate::{Output, OutputLegacy, Version};
/// Metric output with metadata for caching.
/// Series output with metadata for caching.
#[derive(Debug)]
pub struct MetricOutput {
pub struct SeriesOutput {
pub output: Output,
pub version: Version,
pub total: usize,
@@ -10,9 +10,9 @@ pub struct MetricOutput {
pub end: usize,
}
/// Deprecated: Legacy metric output with metadata for caching.
/// Deprecated: Legacy series output with metadata for caching.
#[derive(Debug)]
pub struct MetricOutputLegacy {
pub struct SeriesOutputLegacy {
pub output: OutputLegacy,
pub version: Version,
pub total: usize,
@@ -3,21 +3,21 @@ use std::borrow::Cow;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// A paginated list of available metric names (1000 per page)
/// A paginated list of available series names (1000 per page)
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct PaginatedMetrics {
pub struct PaginatedSeries {
/// Current page number (0-indexed)
#[schemars(example = 0)]
pub current_page: usize,
/// Maximum valid page index (0-indexed)
#[schemars(example = 21)]
pub max_page: usize,
/// Total number of metrics
/// Total number of series
pub total_count: usize,
/// Results per page
pub per_page: usize,
/// Whether more pages are available after the current one
pub has_more: bool,
/// List of metric names
pub metrics: Vec<Cow<'static, str>>,
/// List of series names
pub series: Vec<Cow<'static, str>>,
}
@@ -1,9 +1,9 @@
use schemars::JsonSchema;
use serde::Deserialize;
use crate::Metric;
use crate::Series;
#[derive(Deserialize, JsonSchema)]
pub struct MetricParam {
pub metric: Metric,
pub struct SeriesParam {
pub series: Series,
}
+43
View File
@@ -0,0 +1,43 @@
use derive_more::Deref;
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{DataRangeFormat, Index, Series, SeriesList};
/// Selection of series to query
#[derive(Debug, Deref, Deserialize, JsonSchema)]
pub struct SeriesSelection {
/// Requested series
#[serde(alias = "m", alias = "metrics")]
pub series: SeriesList,
/// Index to query
#[serde(alias = "i")]
pub index: Index,
#[deref]
#[serde(flatten)]
pub range: DataRangeFormat,
}
impl From<(Index, Series, DataRangeFormat)> for SeriesSelection {
#[inline]
fn from((index, series, range): (Index, Series, DataRangeFormat)) -> Self {
Self {
index,
series: SeriesList::from(series),
range,
}
}
}
impl From<(Index, SeriesList, DataRangeFormat)> for SeriesSelection {
#[inline]
fn from((index, series, range): (Index, SeriesList, DataRangeFormat)) -> Self {
Self {
index,
series,
range,
}
}
}
@@ -0,0 +1,26 @@
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{DataRangeFormat, Index, SeriesList, SeriesSelection};
/// Legacy series selection parameters (deprecated)
#[derive(Debug, Deserialize, JsonSchema)]
pub struct SeriesSelectionLegacy {
#[serde(alias = "i")]
pub index: Index,
#[serde(alias = "v")]
pub ids: SeriesList,
#[serde(flatten)]
pub range: DataRangeFormat,
}
impl From<SeriesSelectionLegacy> for SeriesSelection {
#[inline]
fn from(value: SeriesSelectionLegacy) -> Self {
SeriesSelection {
index: value.index,
series: value.ids,
range: value.range,
}
}
}
+37
View File
@@ -0,0 +1,37 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{Index, Series};
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct SeriesWithIndex {
/// Series name
pub series: Series,
/// Aggregation index
pub index: Index,
}
impl SeriesWithIndex {
pub fn new(series: impl Into<Series>, index: Index) -> Self {
Self {
series: series.into(),
index,
}
}
}
impl From<(Series, Index)> for SeriesWithIndex {
fn from((series, index): (Series, Index)) -> Self {
Self { series, index }
}
}
impl From<(&str, Index)> for SeriesWithIndex {
fn from((series, index): (&str, Index)) -> Self {
Self {
series: series.into(),
index,
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ use crate::{Height, Timestamp};
pub struct SyncStatus {
/// Height of the last indexed block
pub indexed_height: Height,
/// Height of the last computed block (metrics)
/// Height of the last computed block (series)
pub computed_height: Height,
/// Height of the chain tip (from Bitcoin node)
pub tip_height: Height,
+1 -1
View File
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
/// Time period for mining statistics.
///
/// Used to specify the lookback window for pool statistics, hashrate calculations,
/// and other time-based mining metrics.
/// and other time-based mining series.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum TimePeriod {
#[default]
+119 -119
View File
@@ -6,18 +6,18 @@ use serde::{Deserialize, Serialize};
use super::Index;
/// Leaf node containing metric metadata
/// Leaf node containing series metadata
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct MetricLeaf {
/// The metric name/identifier
pub struct SeriesLeaf {
/// The series name/identifier
pub name: String,
/// The Rust type (e.g., "Sats", "StoredF64")
pub kind: String,
/// Available indexes for this metric
/// Available indexes for this series
pub indexes: BTreeSet<Index>,
}
impl MetricLeaf {
impl SeriesLeaf {
pub fn new(name: String, kind: String, indexes: BTreeSet<Index>) -> Self {
Self {
name,
@@ -27,17 +27,17 @@ impl MetricLeaf {
}
/// Merge another leaf's indexes into this one (union)
pub fn merge_indexes(&mut self, other: &MetricLeaf) {
pub fn merge_indexes(&mut self, other: &SeriesLeaf) {
self.indexes.extend(other.indexes.iter().copied());
}
}
/// MetricLeaf with JSON Schema for client generation
/// SeriesLeaf with JSON Schema for client generation
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MetricLeafWithSchema {
/// The core metric metadata
pub struct SeriesLeafWithSchema {
/// The core series metadata
#[serde(flatten)]
pub leaf: MetricLeaf,
pub leaf: SeriesLeaf,
/// JSON Schema type (e.g., "integer", "number", "string", "boolean", "array", "object")
#[serde(rename = "type")]
pub openapi_type: String,
@@ -92,8 +92,8 @@ fn extract_json_type_inner(node: &serde_json::Value, root: &serde_json::Value) -
"object".to_string()
}
impl MetricLeafWithSchema {
pub fn new(leaf: MetricLeaf, schema: serde_json::Value) -> Self {
impl SeriesLeafWithSchema {
pub fn new(leaf: SeriesLeaf, schema: serde_json::Value) -> Self {
let openapi_type = extract_json_type(&schema);
Self {
leaf,
@@ -107,7 +107,7 @@ impl MetricLeafWithSchema {
&self.openapi_type
}
/// The metric name/identifier
/// The series name/identifier
pub fn name(&self) -> &str {
&self.leaf.name
}
@@ -117,38 +117,38 @@ impl MetricLeafWithSchema {
&self.leaf.kind
}
/// Available indexes for this metric
/// Available indexes for this series
pub fn indexes(&self) -> &BTreeSet<Index> {
&self.leaf.indexes
}
/// Check if this leaf refers to the same metric as another
pub fn is_same_metric(&self, other: &MetricLeafWithSchema) -> bool {
/// Check if this leaf refers to the same series as another
pub fn is_same_series(&self, other: &SeriesLeafWithSchema) -> bool {
self.leaf.name == other.leaf.name
}
/// Merge another leaf's indexes into this one (union)
pub fn merge_indexes(&mut self, other: &MetricLeafWithSchema) {
pub fn merge_indexes(&mut self, other: &SeriesLeafWithSchema) {
self.leaf.merge_indexes(&other.leaf);
}
}
impl PartialEq for MetricLeafWithSchema {
impl PartialEq for SeriesLeafWithSchema {
fn eq(&self, other: &Self) -> bool {
self.leaf == other.leaf
}
}
impl Eq for MetricLeafWithSchema {}
impl Eq for SeriesLeafWithSchema {}
/// Hierarchical tree node for organizing metrics into categories
/// Hierarchical tree node for organizing series into categories
#[derive(Debug, Clone, Serialize, PartialEq, Eq, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum TreeNode {
/// Branch node containing subcategories
Branch(IndexMap<String, TreeNode>),
/// Leaf node containing metric metadata with schema
Leaf(MetricLeafWithSchema),
/// Leaf node containing series metadata with schema
Leaf(SeriesLeafWithSchema),
}
const BASE: &str = "raw";
@@ -180,7 +180,7 @@ impl TreeNode {
/// Merges all first-level branches into a single flattened structure (consuming version).
/// Direct leaves use their key (use #[traversable(rename = "...")] to control).
/// Branch children are lifted with their keys.
/// If all resulting children are leaves with the same metric name, collapses to a single leaf.
/// If all resulting children are leaves with the same series name, collapses to a single leaf.
/// Returns None if conflicts are found (same key with incompatible values).
pub fn merge_branches(self) -> Option<Self> {
let Self::Branch(tree) = self else {
@@ -204,11 +204,11 @@ impl TreeNode {
}
}
// If all children are leaves with the same metric name, collapse into single leaf
// If all children are leaves with the same series name, collapse into single leaf
Some(Self::try_collapse_same_name_leaves(merged))
}
/// If all entries in the map are leaves with the same metric name,
/// If all entries in the map are leaves with the same series name,
/// collapse them into a single leaf with merged indexes.
fn try_collapse_same_name_leaves(map: IndexMap<String, TreeNode>) -> Self {
if map.is_empty() {
@@ -216,7 +216,7 @@ impl TreeNode {
}
// Check if all entries are leaves with the same name
let mut first_leaf: Option<&MetricLeafWithSchema> = None;
let mut first_leaf: Option<&SeriesLeafWithSchema> = None;
let mut merged_indexes = BTreeSet::new();
for node in map.values() {
@@ -241,8 +241,8 @@ impl TreeNode {
// All entries were leaves with the same name
let first = first_leaf.unwrap();
Self::Leaf(MetricLeafWithSchema::new(
MetricLeaf::new(
Self::Leaf(SeriesLeafWithSchema::new(
SeriesLeaf::new(
first.name().to_string(),
first.kind().to_string(),
merged_indexes,
@@ -265,7 +265,7 @@ impl TreeNode {
}
Some(existing) => {
match (existing, node) {
(Self::Leaf(a), Self::Leaf(b)) if a.is_same_metric(&b) => {
(Self::Leaf(a), Self::Leaf(b)) if a.is_same_series(&b) => {
a.merge_indexes(&b);
Some(())
}
@@ -313,8 +313,8 @@ mod tests {
use super::*;
fn leaf(name: &str, index: Index) -> TreeNode {
TreeNode::Leaf(MetricLeafWithSchema {
leaf: MetricLeaf {
TreeNode::Leaf(SeriesLeafWithSchema {
leaf: SeriesLeaf {
name: name.to_string(),
kind: "TestType".to_string(),
indexes: BTreeSet::from([index]),
@@ -344,7 +344,7 @@ mod tests {
#[test]
fn merge_leaf_passthrough() {
let tree = leaf("metric", Index::Height);
let tree = leaf("s", Index::Height);
let merged = tree.merge_branches().unwrap();
assert!(matches!(merged, TreeNode::Leaf(_)));
}
@@ -365,8 +365,8 @@ mod tests {
fn merge_direct_leaves_keep_keys() {
// Direct leaves with different keys stay separate
let tree = branch(vec![
("sum", leaf("metric_sum", Index::Height)),
("cumulative", leaf("metric_cumulative", Index::Height)),
("sum", leaf("s_sum", Index::Height)),
("cumulative", leaf("s_cumulative", Index::Height)),
]);
let merged = tree.merge_branches().unwrap();
@@ -388,8 +388,8 @@ mod tests {
let tree = branch(vec![(
"week1",
branch(vec![
("sum", leaf("metric_sum", Index::Week1)),
("cumulative", leaf("metric_cumulative", Index::Week1)),
("sum", leaf("s_sum", Index::Week1)),
("cumulative", leaf("s_cumulative", Index::Week1)),
]),
)]);
let merged = tree.merge_branches().unwrap();
@@ -411,15 +411,15 @@ mod tests {
(
"week1",
branch(vec![
("sum", leaf("metric_sum", Index::Week1)),
("cumulative", leaf("metric_cumulative", Index::Week1)),
("sum", leaf("s_sum", Index::Week1)),
("cumulative", leaf("s_cumulative", Index::Week1)),
]),
),
(
"month1",
branch(vec![
("sum", leaf("metric_sum", Index::Month1)),
("cumulative", leaf("metric_cumulative", Index::Month1)),
("sum", leaf("s_sum", Index::Month1)),
("cumulative", leaf("s_cumulative", Index::Month1)),
]),
),
]);
@@ -446,12 +446,12 @@ mod tests {
// Direct leaf with key "cumulative" merges with lifted "cumulative" from branch
// This simulates: height_cumulative (renamed) + day1 branch
let tree = branch(vec![
("cumulative", leaf("metric_cumulative", Index::Height)),
("cumulative", leaf("s_cumulative", Index::Height)),
(
"day1",
branch(vec![
("sum", leaf("metric_sum", Index::Day1)),
("cumulative", leaf("metric_cumulative", Index::Day1)),
("sum", leaf("s_sum", Index::Day1)),
("cumulative", leaf("s_cumulative", Index::Day1)),
]),
),
]);
@@ -483,26 +483,26 @@ mod tests {
// - week1 (flattened from dates) → branch with sum/cumulative at Week1
// - epoch → branch with sum/cumulative at Epoch
let tree = branch(vec![
("cumulative", leaf("metric_cumulative", Index::Height)),
("cumulative", leaf("s_cumulative", Index::Height)),
(
"day1",
branch(vec![
("sum", leaf("metric_sum", Index::Day1)),
("cumulative", leaf("metric_cumulative", Index::Day1)),
("sum", leaf("s_sum", Index::Day1)),
("cumulative", leaf("s_cumulative", Index::Day1)),
]),
),
(
"week1",
branch(vec![
("sum", leaf("metric_sum", Index::Week1)),
("cumulative", leaf("metric_cumulative", Index::Week1)),
("sum", leaf("s_sum", Index::Week1)),
("cumulative", leaf("s_cumulative", Index::Week1)),
]),
),
(
"epoch",
branch(vec![
("sum", leaf("metric_sum", Index::Epoch)),
("cumulative", leaf("metric_cumulative", Index::Epoch)),
("sum", leaf("s_sum", Index::Epoch)),
("cumulative", leaf("s_cumulative", Index::Epoch)),
]),
),
]);
@@ -535,24 +535,24 @@ mod tests {
#[test]
fn merge_conflict_from_lifted_branches() {
// Two branches lifting children with same key but different metric names → conflict
// Two branches lifting children with same key but different series names → conflict
let tree = branch(vec![
("a", branch(vec![("data", leaf("metric_a", Index::Height))])),
("b", branch(vec![("data", leaf("metric_b", Index::Day1))])),
("a", branch(vec![("data", leaf("s_a", Index::Height))])),
("b", branch(vec![("data", leaf("s_b", Index::Day1))])),
]);
let result = tree.merge_branches();
assert!(result.is_none(), "Should detect conflict");
}
#[test]
fn merge_no_conflict_same_metric_different_indexes() {
// Same key, same metric name, different indexes → merges indexes → collapses to Leaf
fn merge_no_conflict_same_series_different_indexes() {
// Same key, same series name, different indexes → merges indexes → collapses to Leaf
let tree = branch(vec![
(
"a",
branch(vec![("sum", leaf("metric_sum", Index::Height))]),
branch(vec![("sum", leaf("s_sum", Index::Height))]),
),
("b", branch(vec![("sum", leaf("metric_sum", Index::Day1))])),
("b", branch(vec![("sum", leaf("s_sum", Index::Day1))])),
]);
let result = tree.merge_branches();
assert!(result.is_some(), "Should merge successfully");
@@ -560,7 +560,7 @@ mod tests {
let merged = result.unwrap();
match merged {
TreeNode::Leaf(leaf) => {
assert_eq!(leaf.name(), "metric_sum");
assert_eq!(leaf.name(), "s_sum");
let indexes = leaf.indexes();
assert!(indexes.contains(&Index::Height));
assert!(indexes.contains(&Index::Day1));
@@ -578,7 +578,7 @@ mod tests {
"outer",
branch(vec![(
"inner",
branch(vec![("leaf", leaf("metric", Index::Height))]),
branch(vec![("leaf", leaf("s", Index::Height))]),
)]),
)]);
let merged = tree.merge_branches().unwrap();
@@ -600,7 +600,7 @@ mod tests {
// ComputedVecsDateLast pattern:
// - day1: direct leaf (field name as key)
// - rest (flattened): DerivedDateLast → branches with "last" children
// All leaves have same metric name → collapse to single Leaf
// All leaves have same series name → collapse to single Leaf
let tree = branch(vec![
// Direct leaf from day1 field (no wrap attribute)
("day1", leaf("1m_block_count", Index::Day1)),
@@ -635,11 +635,11 @@ mod tests {
}
}
// ========== Case 1: DerivedDateLast (all same metric name) ==========
// ========== Case 1: DerivedDateLast (all same series name) ==========
#[test]
fn case1_derived_date_last() {
// All leaves have the same metric name, all wrapped as "last"
// All leaves have the same series name, all wrapped as "last"
// All branches lift to same key → collapses to single Leaf
let tree = branch(vec![
(
@@ -686,15 +686,15 @@ mod tests {
(
"day1",
branch(vec![
("sum", leaf("metric_sum", Index::Day1)),
("cumulative", leaf("metric_cumulative", Index::Day1)),
("sum", leaf("s_sum", Index::Day1)),
("cumulative", leaf("s_cumulative", Index::Day1)),
]),
),
(
"week1",
branch(vec![
("sum", leaf("metric_sum", Index::Week1)),
("cumulative", leaf("metric_cumulative", Index::Week1)),
("sum", leaf("s_sum", Index::Week1)),
("cumulative", leaf("s_cumulative", Index::Week1)),
]),
),
]);
@@ -729,16 +729,16 @@ mod tests {
// height wrapped as "raw"
(
"height",
branch(vec![("raw", leaf("metric", Index::Height))]),
branch(vec![("raw", leaf("s", Index::Height))]),
),
// rest (flattened) produces branches
(
"day1",
branch(vec![("sum", leaf("metric_sum", Index::Day1))]),
branch(vec![("sum", leaf("s_sum", Index::Day1))]),
),
(
"week1",
branch(vec![("sum", leaf("metric_sum", Index::Week1))]),
branch(vec![("sum", leaf("s_sum", Index::Week1))]),
),
]);
@@ -780,16 +780,16 @@ mod tests {
// height wrapped as "raw"
(
"height",
branch(vec![("raw", leaf("metric", Index::Height))]),
branch(vec![("raw", leaf("s", Index::Height))]),
),
// rest (flattened) produces branches with "last" key
(
"day1",
branch(vec![("last", leaf("metric_last", Index::Day1))]),
branch(vec![("last", leaf("s_last", Index::Day1))]),
),
(
"week1",
branch(vec![("last", leaf("metric_last", Index::Week1))]),
branch(vec![("last", leaf("s_last", Index::Week1))]),
),
]);
@@ -835,36 +835,36 @@ mod tests {
// height wrapped as "raw" (raw values at height granularity)
(
"height",
branch(vec![("raw", leaf("metric", Index::Height))]),
branch(vec![("raw", leaf("s", Index::Height))]),
),
// height_cumulative wrapped as cumulative
(
"height_cumulative",
branch(vec![(
"cumulative",
leaf("metric_cumulative", Index::Height),
leaf("s_cumulative", Index::Height),
)]),
),
// day1 Full
(
"day1",
branch(vec![
("average", leaf("metric_average", Index::Day1)),
("min", leaf("metric_min", Index::Day1)),
("max", leaf("metric_max", Index::Day1)),
("sum", leaf("metric_sum", Index::Day1)),
("cumulative", leaf("metric_cumulative", Index::Day1)),
("average", leaf("s_average", Index::Day1)),
("min", leaf("s_min", Index::Day1)),
("max", leaf("s_max", Index::Day1)),
("sum", leaf("s_sum", Index::Day1)),
("cumulative", leaf("s_cumulative", Index::Day1)),
]),
),
// week1 (from flattened dates)
(
"week1",
branch(vec![
("average", leaf("metric_average", Index::Week1)),
("min", leaf("metric_min", Index::Week1)),
("max", leaf("metric_max", Index::Week1)),
("sum", leaf("metric_sum", Index::Week1)),
("cumulative", leaf("metric_cumulative", Index::Week1)),
("average", leaf("s_average", Index::Week1)),
("min", leaf("s_min", Index::Week1)),
("max", leaf("s_max", Index::Week1)),
("sum", leaf("s_sum", Index::Week1)),
("cumulative", leaf("s_cumulative", Index::Week1)),
]),
),
]);
@@ -912,7 +912,7 @@ mod tests {
#[test]
fn case6_lazy_date_last_all_branches_same_key_collapses() {
// LazyDateLast pattern: All fields are branches with same inner key "last"
// All leaves have the same metric name → should collapse to single Leaf
// All leaves have the same series name → should collapse to single Leaf
let tree = branch(vec![
(
"day1",
@@ -938,7 +938,7 @@ mod tests {
let merged = tree.merge_branches().unwrap();
// All branches lifted to same "last" key, all same metric name → collapse to Leaf
// All branches lifted to same "last" key, all same series name → collapse to Leaf
match &merged {
TreeNode::Leaf(leaf) => {
assert_eq!(leaf.name(), "price_200d_sma");
@@ -973,14 +973,14 @@ mod tests {
// sats with wrap="sats" produces Branch { sats: Leaf }
(
"sats",
branch(vec![("sats", leaf("metric", Index::Height))]),
branch(vec![("sats", leaf("s", Index::Height))]),
),
// rest with flatten: LazyDerivedBlockValue fields lifted
(
"rest",
branch(vec![
("bitcoin", leaf("metric_btc", Index::Height)),
("dollars", leaf("metric_usd", Index::Height)),
("bitcoin", leaf("s_btc", Index::Height)),
("dollars", leaf("s_usd", Index::Height)),
]),
),
]);
@@ -1020,25 +1020,25 @@ mod tests {
// height with wrap="raw"
(
"height",
branch(vec![("raw", leaf("metric", Index::Height))]),
branch(vec![("raw", leaf("s", Index::Height))]),
),
// height_cumulative with wrap="cumulative"
(
"height_cumulative",
branch(vec![(
"cumulative",
leaf("metric_cumulative", Index::Height),
leaf("s_cumulative", Index::Height),
)]),
),
// From rest (flatten) - inner struct already merged to { sum, cumulative }
// Each leaf has merged indexes from all time periods
(
"sum",
leaf("metric_sum", Index::Day1), // Would have all time indexes
leaf("s_sum", Index::Day1), // Would have all time indexes
),
(
"cumulative",
leaf("metric_cumulative", Index::Day1), // Would have all time indexes
leaf("s_cumulative", Index::Day1), // Would have all time indexes
),
]);
@@ -1082,21 +1082,21 @@ mod tests {
// Each denomination has already been merged internally
// Simulating the output after inner merge
let sats_merged = branch(vec![
("raw", leaf("metric", Index::Height)),
("sum", leaf("metric_sum", Index::Day1)),
("cumulative", leaf("metric_cumulative", Index::Height)),
("raw", leaf("s", Index::Height)),
("sum", leaf("s_sum", Index::Day1)),
("cumulative", leaf("s_cumulative", Index::Height)),
]);
let bitcoin_merged = branch(vec![
("raw", leaf("metric_btc", Index::Height)),
("sum", leaf("metric_btc_sum", Index::Day1)),
("cumulative", leaf("metric_btc_cumulative", Index::Height)),
("raw", leaf("s_btc", Index::Height)),
("sum", leaf("s_btc_sum", Index::Day1)),
("cumulative", leaf("s_btc_cumulative", Index::Height)),
]);
let dollars_merged = branch(vec![
("raw", leaf("metric_usd", Index::Height)),
("sum", leaf("metric_usd_sum", Index::Day1)),
("cumulative", leaf("metric_usd_cumulative", Index::Height)),
("raw", leaf("s_usd", Index::Height)),
("sum", leaf("s_usd_sum", Index::Day1)),
("cumulative", leaf("s_usd_cumulative", Index::Height)),
]);
// Outer struct has no merge, so denominations stay as branches
@@ -1133,19 +1133,19 @@ mod tests {
fn case10_derived_date_last_collapses_to_leaf() {
// DerivedDateLast<T> with merge: all fields have wrap="last"
// week1: { last: Leaf }, month1: { last: Leaf }, etc.
// After merge: all "last" keys merge, same metric name → collapses to Leaf
// After merge: all "last" keys merge, same series name → collapses to Leaf
let tree = branch(vec![
(
"week1",
branch(vec![("last", leaf("metric", Index::Week1))]),
branch(vec![("last", leaf("s", Index::Week1))]),
),
(
"month1",
branch(vec![("last", leaf("metric", Index::Month1))]),
branch(vec![("last", leaf("s", Index::Month1))]),
),
(
"year1",
branch(vec![("last", leaf("metric", Index::Year1))]),
branch(vec![("last", leaf("s", Index::Year1))]),
),
]);
@@ -1175,18 +1175,18 @@ mod tests {
// - rest (flatten): DerivedDateLast already merged to Leaf
// → flatten inserts with field name "rest" as key
//
// Both have same metric name → collapses to single Leaf
// Both have same series name → collapses to single Leaf
let tree = branch(vec![
// day1 with wrap="raw"
("day1", branch(vec![("raw", leaf("metric", Index::Day1))])),
("day1", branch(vec![("raw", leaf("s", Index::Day1))])),
// rest (flatten): DerivedDateLast merged to Leaf
// Same metric name as base
("rest", leaf("metric", Index::Week1)),
// Same series name as base
("rest", leaf("s", Index::Week1)),
]);
let merged = tree.merge_branches().unwrap();
// Same metric name → collapses to single Leaf with all indexes
// Same series name → collapses to single Leaf with all indexes
match &merged {
TreeNode::Leaf(leaf) => {
let indexes = leaf.indexes();
@@ -1216,23 +1216,23 @@ mod tests {
// From sats_day1 with wrap="sats"
(
"sats_day1",
branch(vec![("sats", leaf("metric", Index::Day1))]),
branch(vec![("sats", leaf("s", Index::Day1))]),
),
// From rest (flatten): ValueDerivedDateLast
(
"rest",
branch(vec![
// sats field: DerivedDateLast merged to Leaf
("sats", leaf("metric", Index::Week1)), // Same metric name!
("bitcoin", leaf("metric_btc", Index::Day1)),
("dollars", leaf("metric_usd", Index::Day1)),
("sats", leaf("s", Index::Week1)), // Same series name!
("bitcoin", leaf("s_btc", Index::Day1)),
("dollars", leaf("s_usd", Index::Day1)),
]),
),
]);
let merged = tree.merge_branches();
// Should succeed because both "sats" have the same metric name
// Should succeed because both "sats" have the same series name
// Indexes should be merged
match merged {
Some(TreeNode::Branch(map)) => {
@@ -1258,9 +1258,9 @@ mod tests {
// Simulating final merged output
let tree = branch(vec![
("sats", leaf("metric", Index::Day1)), // placeholder, would have all indexes
("bitcoin", leaf("metric_btc", Index::Day1)),
("dollars", leaf("metric_usd", Index::Day1)),
("sats", leaf("s", Index::Day1)), // placeholder, would have all indexes
("bitcoin", leaf("s_btc", Index::Day1)),
("dollars", leaf("s_usd", Index::Day1)),
]);
match &tree {
+2053 -2030
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -19,7 +19,7 @@ import urllib.request
import concurrent.futures
from pathlib import Path
API_BASE = "https://bitview.space/api/metric"
API_BASE = "https://bitview.space/api/series"
POOLSLUG_PATH = Path(__file__).resolve().parent.parent / "crates/brk_types/src/poolslug.rs"
HEADERS = {"User-Agent": "pool-threshold-script"}
WINDOWS = {"1w": 7, "1m": 30, "1y": 365}
+32 -32
View File
@@ -88,8 +88,8 @@ export function createChart({ parent, brk, fitContent }) {
/** @param {ChartableIndex} idx */
const getTimeEndpoint = (idx) =>
idx === "height"
? brk.metrics.blocks.time.timestampMonotonic.by[idx]
: brk.metrics.blocks.time.timestamp.by[idx];
? brk.series.blocks.time.timestampMonotonic.by[idx]
: brk.series.blocks.time.timestamp.by[idx];
const index = {
/** @type {Set<(index: ChartableIndex) => void>} */
@@ -118,9 +118,9 @@ export function createChart({ parent, brk, fitContent }) {
let generation = 0;
const time = {
/** @type {MetricData<number> | null} */
/** @type {SeriesData<number> | null} */
data: null,
/** @type {Set<(data: MetricData<number>) => void>} */
/** @type {Set<(data: SeriesData<number>) => void>} */
callbacks: new Set(),
/** @type {ReturnType<typeof getTimeEndpoint> | null} */
endpoint: null,
@@ -150,7 +150,7 @@ export function createChart({ parent, brk, fitContent }) {
};
// Memory cache for instant index switching
/** @type {Map<string, MetricData<any>>} */
/** @type {Map<string, SeriesData<any>>} */
const cache = new Map();
// Range state: localStorage stores all ranges per-index, URL stores current range only
@@ -425,7 +425,7 @@ export function createChart({ parent, brk, fitContent }) {
* @param {string} args.name
* @param {number} args.order
* @param {Color[]} args.colors
* @param {AnyMetricPattern} args.metric
* @param {AnySeriesPattern} args.source
* @param {number} args.paneIndex
* @param {Unit} args.unit
* @param {string} [args.key] - Optional key for persistence (derived from name if not provided)
@@ -438,7 +438,7 @@ export function createChart({ parent, brk, fitContent }) {
* @param {() => void} args.onRemove
*/
create({
metric,
source,
name,
order,
paneIndex,
@@ -486,7 +486,7 @@ export function createChart({ parent, brk, fitContent }) {
lastTimeVersion: null,
/** @type {VoidFunction | null} */
fetch: null,
/** @type {((data: MetricData<number>) => void) | null} */
/** @type {((data: SeriesData<number>) => void) | null} */
onTime: null,
reset() {
this.hasData = false;
@@ -572,8 +572,8 @@ export function createChart({ parent, brk, fitContent }) {
state.reset();
state.fetch = null;
const _valuesEndpoint = metric.by[idx];
// Gracefully skip - series may be about to be removed by option change
const _valuesEndpoint = source.by[idx];
// Gracefully skip - source may be about to be removed by option change
if (!_valuesEndpoint) return;
const valuesEndpoint = _valuesEndpoint;
@@ -702,7 +702,7 @@ export function createChart({ parent, brk, fitContent }) {
}
async function fetchAndProcess() {
/** @type {MetricData<number> | null} */
/** @type {SeriesData<number> | null} */
let timeData = null;
/** @type {(number | null | [number, number, number, number])[] | null} */
let valuesData = null;
@@ -769,7 +769,7 @@ export function createChart({ parent, brk, fitContent }) {
* @param {Object} args
* @param {string} args.name
* @param {number} args.order
* @param {AnyMetricPattern} args.metric
* @param {AnySeriesPattern} args.source
* @param {Unit} args.unit
* @param {string} [args.key] - Optional key for persistence (derived from name if not provided)
* @param {number} [args.paneIndex]
@@ -778,7 +778,7 @@ export function createChart({ parent, brk, fitContent }) {
* @param {CandlestickSeriesPartialOptions} [args.options]
*/
addCandlestick({
metric,
source,
name,
key,
order,
@@ -830,7 +830,7 @@ export function createChart({ parent, brk, fitContent }) {
paneIndex,
unit,
defaultActive,
metric,
source,
setOrder(order) {
candlestickISeries.setSeriesOrder(order);
lineISeries.setSeriesOrder(order);
@@ -880,7 +880,7 @@ export function createChart({ parent, brk, fitContent }) {
* @param {Object} args
* @param {string} args.name
* @param {number} args.order
* @param {AnyMetricPattern} args.metric
* @param {AnySeriesPattern} args.source
* @param {Unit} args.unit
* @param {string} [args.key] - Optional key for persistence (derived from name if not provided)
* @param {Color | [Color, Color]} [args.color] - Single color or [positive, negative] colors
@@ -889,7 +889,7 @@ export function createChart({ parent, brk, fitContent }) {
* @param {HistogramSeriesPartialOptions} [args.options]
*/
addHistogram({
metric,
source,
name,
key,
color = colors.bi.p1,
@@ -920,7 +920,7 @@ export function createChart({ parent, brk, fitContent }) {
paneIndex,
unit,
defaultActive,
metric,
source,
setOrder: (order) => iseries.setSeriesOrder(order),
applyOptions(active, highlighted) {
iseries.applyOptions({
@@ -953,7 +953,7 @@ export function createChart({ parent, brk, fitContent }) {
* @param {Object} args
* @param {string} args.name
* @param {number} args.order
* @param {AnyMetricPattern} args.metric
* @param {AnySeriesPattern} args.source
* @param {Unit} args.unit
* @param {string} [args.key] - Optional key for persistence (derived from name if not provided)
* @param {Color} args.color
@@ -962,7 +962,7 @@ export function createChart({ parent, brk, fitContent }) {
* @param {LineSeriesPartialOptions} [args.options]
*/
addLine({
metric,
source,
name,
key,
order,
@@ -991,7 +991,7 @@ export function createChart({ parent, brk, fitContent }) {
paneIndex,
unit,
defaultActive,
metric,
source,
setOrder: (order) => iseries.setSeriesOrder(order),
applyOptions(active, highlighted) {
iseries.applyOptions({
@@ -1010,7 +1010,7 @@ export function createChart({ parent, brk, fitContent }) {
* @param {Object} args
* @param {string} args.name
* @param {number} args.order
* @param {AnyMetricPattern} args.metric
* @param {AnySeriesPattern} args.source
* @param {Unit} args.unit
* @param {string} [args.key] - Optional key for persistence (derived from name if not provided)
* @param {Color} args.color
@@ -1019,7 +1019,7 @@ export function createChart({ parent, brk, fitContent }) {
* @param {LineSeriesPartialOptions} [args.options]
*/
addDots({
metric,
source,
name,
key,
order,
@@ -1063,7 +1063,7 @@ export function createChart({ parent, brk, fitContent }) {
paneIndex,
unit,
defaultActive,
metric,
source,
setOrder: (order) => iseries.setSeriesOrder(order),
applyOptions(active, highlighted) {
iseries.applyOptions({
@@ -1089,7 +1089,7 @@ export function createChart({ parent, brk, fitContent }) {
* @param {Object} args
* @param {string} args.name
* @param {number} args.order
* @param {AnyMetricPattern} args.metric
* @param {AnySeriesPattern} args.source
* @param {Unit} args.unit
* @param {string} [args.key] - Optional key for persistence (derived from name if not provided)
* @param {number} [args.paneIndex]
@@ -1099,7 +1099,7 @@ export function createChart({ parent, brk, fitContent }) {
* @param {BaselineSeriesPartialOptions} [args.options]
*/
addBaseline({
metric,
source,
name,
key,
order,
@@ -1139,7 +1139,7 @@ export function createChart({ parent, brk, fitContent }) {
paneIndex,
unit,
defaultActive,
metric,
source,
setOrder: (order) => iseries.setSeriesOrder(order),
applyOptions(active, highlighted) {
iseries.applyOptions({
@@ -1159,7 +1159,7 @@ export function createChart({ parent, brk, fitContent }) {
/**
* Add a DotsBaseline series (baseline with point markers instead of line)
* @param {Object} args
* @param {AnyMetricPattern} args.metric
* @param {AnySeriesPattern} args.source
* @param {string} args.name
* @param {string} [args.key]
* @param {number} args.order
@@ -1171,7 +1171,7 @@ export function createChart({ parent, brk, fitContent }) {
* @param {BaselineSeriesPartialOptions} [args.options]
*/
addDotsBaseline({
metric,
source,
name,
key,
order,
@@ -1224,7 +1224,7 @@ export function createChart({ parent, brk, fitContent }) {
paneIndex,
unit,
defaultActive,
metric,
source,
setOrder: (order) => iseries.setSeriesOrder(order),
applyOptions(active, highlighted) {
iseries.applyOptions({
@@ -1343,10 +1343,10 @@ export function createChart({ parent, brk, fitContent }) {
const defaultColor = unit === Unit.usd ? colors.usd : colors.bitcoin;
map.get(unit)?.forEach((blueprint, order) => {
if (!Object.keys(blueprint.metric.by).includes(idx)) return;
if (!Object.keys(blueprint.series.by).includes(idx)) return;
const common = {
metric: blueprint.metric,
source: blueprint.series,
name: blueprint.title,
key: blueprint.key,
defaultActive: blueprint.defaultActive,
@@ -1398,7 +1398,7 @@ export function createChart({ parent, brk, fitContent }) {
pane.series.push(
serieses.addCandlestick({
...common,
metric: blueprint.ohlcMetric,
source: blueprint.ohlcSeries,
colors: blueprint.colors,
}),
);
+1 -1
View File
@@ -119,7 +119,7 @@ export function createLegend() {
anchor.href = series.url;
anchor.target = "_blank";
anchor.rel = "noopener noreferrer";
anchor.title = "Open the metric data in a new tab";
anchor.title = "Open the series data in a new tab";
div.append(anchor);
}
},
+45 -45
View File
@@ -9,7 +9,7 @@ import { satsBtcUsd, priceRatioPercentilesTree } from "./shared.js";
* @returns {PartialOptionsGroup}
*/
export function createCointimeSection() {
const { cointime, cohorts, supply } = brk.metrics;
const { cointime, cohorts, supply } = brk.series;
const {
prices: cointimePrices,
cap,
@@ -24,9 +24,9 @@ export function createCointimeSection() {
// Reference lines for cap comparisons
const capReferenceLines = /** @type {const} */ ([
{ metric: supply.marketCap.usd, name: "Market", color: colors.default },
{ series: supply.marketCap.usd, name: "Market", color: colors.default },
{
metric: all.realized.cap.usd,
series: all.realized.cap.usd,
name: "Realized",
color: colors.realized,
},
@@ -76,11 +76,11 @@ export function createCointimeSection() {
]);
const caps = /** @type {const} */ ([
{ metric: cap.vaulted.usd, name: "Vaulted", color: colors.vaulted },
{ metric: cap.active.usd, name: "Active", color: colors.active },
{ metric: cap.cointime.usd, name: "Cointime", color: colors.cointime },
{ metric: cap.investor.usd, name: "Investor", color: colors.investor },
{ metric: cap.thermo.usd, name: "Thermo", color: colors.thermo },
{ series: cap.vaulted.usd, name: "Vaulted", color: colors.vaulted },
{ series: cap.active.usd, name: "Active", color: colors.active },
{ series: cap.cointime.usd, name: "Cointime", color: colors.cointime },
{ series: cap.investor.usd, name: "Investor", color: colors.investor },
{ series: cap.thermo.usd, name: "Thermo", color: colors.thermo },
]);
const supplyBreakdown = /** @type {const} */ ([
@@ -159,17 +159,17 @@ export function createCointimeSection() {
title: "Cointime Prices",
top: [
price({
metric: all.realized.price,
series: all.realized.price,
name: "Realized",
color: colors.realized,
}),
price({
metric: all.realized.investor.price,
series: all.realized.investor.price,
name: "Investor",
color: colors.investor,
}),
...prices.map(({ pattern, name, color }) =>
price({ metric: pattern, name, color }),
price({ series: pattern, name, color }),
),
],
},
@@ -181,7 +181,7 @@ export function createCointimeSection() {
legend: name,
color,
priceReferences: [
price({ metric: all.realized.price, name: "Realized", color: colors.realized, defaultActive: false }),
price({ series: all.realized.price, name: "Realized", color: colors.realized, defaultActive: false }),
],
}),
})),
@@ -196,22 +196,22 @@ export function createCointimeSection() {
name: "Compare",
title: "Cointime Caps",
bottom: [
...capReferenceLines.map(({ metric, name, color }) =>
line({ metric, name, color, unit: Unit.usd }),
...capReferenceLines.map(({ series, name, color }) =>
line({ series, name, color, unit: Unit.usd }),
),
...caps.map(({ metric, name, color }) =>
line({ metric, name, color, unit: Unit.usd }),
...caps.map(({ series, name, color }) =>
line({ series, name, color, unit: Unit.usd }),
),
],
},
...caps.map(({ metric, name, color }) => ({
...caps.map(({ series, name, color }) => ({
name,
title: `${name} Cap`,
bottom: [
line({ metric, name, color, unit: Unit.usd }),
line({ series, name, color, unit: Unit.usd }),
...capReferenceLines.map((ref) =>
line({
metric: ref.metric,
series: ref.series,
name: ref.name,
color: ref.color,
unit: Unit.usd,
@@ -237,19 +237,19 @@ export function createCointimeSection() {
title: "Liveliness & Vaultedness",
bottom: [
line({
metric: activity.liveliness,
series: activity.liveliness,
name: "Liveliness",
color: colors.liveliness,
unit: Unit.ratio,
}),
line({
metric: activity.vaultedness,
series: activity.vaultedness,
name: "Vaultedness",
color: colors.vaulted,
unit: Unit.ratio,
}),
line({
metric: activity.ratio,
series: activity.ratio,
name: "L/V Ratio",
color: colors.activity,
unit: Unit.ratio,
@@ -270,7 +270,7 @@ export function createCointimeSection() {
title: "Coinblocks",
bottom: coinblocks.map(({ pattern, name, color }) =>
line({
metric: pattern.base,
series: pattern.base,
name,
color,
unit: Unit.coinblocks,
@@ -282,7 +282,7 @@ export function createCointimeSection() {
title: "Coinblocks (Total)",
bottom: coinblocks.map(({ pattern, name, color }) =>
line({
metric: pattern.cumulative,
series: pattern.cumulative,
name,
color,
unit: Unit.coinblocks,
@@ -299,7 +299,7 @@ export function createCointimeSection() {
title,
bottom: [
line({
metric: pattern.base,
series: pattern.base,
name,
color,
unit: Unit.coinblocks,
@@ -312,7 +312,7 @@ export function createCointimeSection() {
title: `${title} (Total)`,
bottom: [
line({
metric: pattern.cumulative,
series: pattern.cumulative,
name,
color,
unit: Unit.coinblocks,
@@ -336,10 +336,10 @@ export function createCointimeSection() {
title: "Cointime Value",
bottom: [
...cointimeValues.map(({ pattern, name, color }) =>
line({ metric: pattern.base, name, color, unit: Unit.usd }),
line({ series: pattern.base, name, color, unit: Unit.usd }),
),
line({
metric: vocdd.pattern.base,
series: vocdd.pattern.base,
name: vocdd.name,
color: vocdd.color,
unit: Unit.usd,
@@ -352,14 +352,14 @@ export function createCointimeSection() {
bottom: [
...cointimeValues.map(({ pattern, name, color }) =>
line({
metric: pattern.cumulative,
series: pattern.cumulative,
name,
color,
unit: Unit.usd,
}),
),
line({
metric: vocdd.pattern.cumulative,
series: vocdd.pattern.cumulative,
name: vocdd.name,
color: vocdd.color,
unit: Unit.usd,
@@ -375,7 +375,7 @@ export function createCointimeSection() {
name: "Base",
title,
bottom: [
line({ metric: pattern.base, name, color, unit: Unit.usd }),
line({ series: pattern.base, name, color, unit: Unit.usd }),
],
},
rollingWindowsTree({ windows: pattern.sum, title, unit: Unit.usd }),
@@ -384,7 +384,7 @@ export function createCointimeSection() {
title: `${title} (Total)`,
bottom: [
line({
metric: pattern.cumulative,
series: pattern.cumulative,
name,
color,
unit: Unit.usd,
@@ -401,13 +401,13 @@ export function createCointimeSection() {
title: vocdd.title,
bottom: [
line({
metric: vocdd.pattern.base,
series: vocdd.pattern.base,
name: vocdd.name,
color: vocdd.color,
unit: Unit.usd,
}),
line({
metric: reserveRisk.vocddMedian1y,
series: reserveRisk.vocddMedian1y,
name: "365d Median",
color: colors.time._1y,
unit: Unit.usd,
@@ -420,7 +420,7 @@ export function createCointimeSection() {
title: `${vocdd.title} (Total)`,
bottom: [
line({
metric: vocdd.pattern.cumulative,
series: vocdd.pattern.cumulative,
name: vocdd.name,
color: vocdd.color,
unit: Unit.usd,
@@ -432,7 +432,7 @@ export function createCointimeSection() {
],
},
// Indicators - derived decision metrics
// Indicators - derived decision series
{
name: "Indicators",
tree: [
@@ -441,7 +441,7 @@ export function createCointimeSection() {
title: "Reserve Risk",
bottom: [
line({
metric: reserveRisk.value,
series: reserveRisk.value,
name: "Ratio",
color: colors.reserveRisk,
unit: Unit.ratio,
@@ -453,7 +453,7 @@ export function createCointimeSection() {
title: "AVIV Ratio",
bottom: [
baseline({
metric: cap.aviv.ratio,
series: cap.aviv.ratio,
name: "Ratio",
color: colors.reserveRisk,
unit: Unit.ratio,
@@ -466,7 +466,7 @@ export function createCointimeSection() {
title: "HODL Bank",
bottom: [
line({
metric: reserveRisk.hodlBank,
series: reserveRisk.hodlBank,
name: "Value",
color: colors.hodlBank,
unit: Unit.usd,
@@ -476,7 +476,7 @@ export function createCointimeSection() {
],
},
// Cointime-Adjusted - comparing base vs adjusted metrics
// Cointime-Adjusted - comparing base vs adjusted series
{
name: "Cointime-Adjusted",
tree: [
@@ -485,7 +485,7 @@ export function createCointimeSection() {
title: "Cointime-Adjusted Inflation",
bottom: [
dots({
metric: supply.inflationRate.percent,
series: supply.inflationRate.percent,
name: "Base",
color: colors.base,
unit: Unit.percentage,
@@ -505,13 +505,13 @@ export function createCointimeSection() {
title: "Cointime-Adjusted BTC Velocity",
bottom: [
line({
metric: supply.velocity.native,
series: supply.velocity.native,
name: "Base",
color: colors.base,
unit: Unit.ratio,
}),
line({
metric: adjusted.txVelocityNative,
series: adjusted.txVelocityNative,
name: "Cointime-Adjusted",
color: colors.adjusted,
unit: Unit.ratio,
@@ -523,13 +523,13 @@ export function createCointimeSection() {
title: "Cointime-Adjusted USD Velocity",
bottom: [
line({
metric: supply.velocity.fiat,
series: supply.velocity.fiat,
name: "Base",
color: colors.thermo,
unit: Unit.ratio,
}),
line({
metric: adjusted.txVelocityFiat,
series: adjusted.txVelocityFiat,
name: "Cointime-Adjusted",
color: colors.vaulted,
unit: Unit.ratio,
+6 -6
View File
@@ -7,17 +7,17 @@ import { line } from "./series.js";
/**
* Get constant pattern by number dynamically from tree
* Examples: 0 _0, 38.2 _382, -1 minus1
* @param {BrkClient["metrics"]["constants"]} constants
* @param {BrkClient["series"]["constants"]} constants
* @param {number} num
* @returns {AnyMetricPattern}
* @returns {AnySeriesPattern}
*/
export function getConstant(constants, num) {
const key =
num >= 0
? `_${String(num).replace(".", "")}`
: `minus${Math.abs(num)}`;
const constant = /** @type {AnyMetricPattern | undefined} */ (
/** @type {Record<string, AnyMetricPattern>} */ (constants)[key]
const constant = /** @type {AnySeriesPattern | undefined} */ (
/** @type {Record<string, AnySeriesPattern>} */ (constants)[key]
);
if (!constant) throw new Error(`Unknown constant: ${num} (key: ${key})`);
return constant;
@@ -25,12 +25,12 @@ export function getConstant(constants, num) {
/**
* Create a price line series (horizontal reference line)
* @param {{ number?: number, name?: string } & Omit<(Parameters<typeof line>)[0], 'name' | 'metric'>} args
* @param {{ number?: number, name?: string } & Omit<(Parameters<typeof line>)[0], 'name' | 'series'>} args
*/
export function priceLine(args) {
return line({
...args,
metric: getConstant(brk.metrics.constants, args.number || 0),
series: getConstant(brk.series.constants, args.number || 0),
name: args.name || `${args.number ?? 0}`,
color: args.color ?? colors.gray,
options: {
+155 -155
View File
@@ -23,7 +23,7 @@ import { colors } from "../../utils/colors.js";
/**
* @param {{ sent: Brk.BaseCumulativeInSumPattern, coindaysDestroyed: Brk.BaseCumulativeSumPattern<number> }} activity
* @param {Color} color
* @param {(metric: string) => string} title
* @param {(name: string) => string} title
* @returns {PartialOptionsTree}
*/
function volumeAndCoinsTree(activity, color, title) {
@@ -35,18 +35,18 @@ function volumeAndCoinsTree(activity, color, title) {
name: "Sum",
title: title("Sent Volume"),
bottom: [
line({ metric: activity.sent.base, name: "Sum", color, unit: Unit.sats }),
line({ metric: activity.sent.sum._24h, name: "24h", color: colors.time._24h, unit: Unit.sats, defaultActive: false }),
line({ metric: activity.sent.sum._1w, name: "1w", color: colors.time._1w, unit: Unit.sats, defaultActive: false }),
line({ metric: activity.sent.sum._1m, name: "1m", color: colors.time._1m, unit: Unit.sats, defaultActive: false }),
line({ metric: activity.sent.sum._1y, name: "1y", color: colors.time._1y, unit: Unit.sats, defaultActive: false }),
line({ series: activity.sent.base, name: "Sum", color, unit: Unit.sats }),
line({ series: activity.sent.sum._24h, name: "24h", color: colors.time._24h, unit: Unit.sats, defaultActive: false }),
line({ series: activity.sent.sum._1w, name: "1w", color: colors.time._1w, unit: Unit.sats, defaultActive: false }),
line({ series: activity.sent.sum._1m, name: "1m", color: colors.time._1m, unit: Unit.sats, defaultActive: false }),
line({ series: activity.sent.sum._1y, name: "1y", color: colors.time._1y, unit: Unit.sats, defaultActive: false }),
],
},
{
name: "Cumulative",
title: title("Sent Volume (Total)"),
bottom: [
line({ metric: activity.sent.cumulative, name: "All-time", color, unit: Unit.sats }),
line({ series: activity.sent.cumulative, name: "All-time", color, unit: Unit.sats }),
],
},
],
@@ -58,18 +58,18 @@ function volumeAndCoinsTree(activity, color, title) {
name: "Base",
title: title("Coindays Destroyed"),
bottom: [
line({ metric: activity.coindaysDestroyed.base, name: "Base", color, unit: Unit.coindays }),
line({ metric: activity.coindaysDestroyed.sum._24h, name: "24h", color: colors.time._24h, unit: Unit.coindays, defaultActive: false }),
line({ metric: activity.coindaysDestroyed.sum._1w, name: "1w", color: colors.time._1w, unit: Unit.coindays, defaultActive: false }),
line({ metric: activity.coindaysDestroyed.sum._1m, name: "1m", color: colors.time._1m, unit: Unit.coindays, defaultActive: false }),
line({ metric: activity.coindaysDestroyed.sum._1y, name: "1y", color: colors.time._1y, unit: Unit.coindays, defaultActive: false }),
line({ series: activity.coindaysDestroyed.base, name: "Base", color, unit: Unit.coindays }),
line({ series: activity.coindaysDestroyed.sum._24h, name: "24h", color: colors.time._24h, unit: Unit.coindays, defaultActive: false }),
line({ series: activity.coindaysDestroyed.sum._1w, name: "1w", color: colors.time._1w, unit: Unit.coindays, defaultActive: false }),
line({ series: activity.coindaysDestroyed.sum._1m, name: "1m", color: colors.time._1m, unit: Unit.coindays, defaultActive: false }),
line({ series: activity.coindaysDestroyed.sum._1y, name: "1y", color: colors.time._1y, unit: Unit.coindays, defaultActive: false }),
],
},
{
name: "Cumulative",
title: title("Cumulative Coindays Destroyed"),
bottom: [
line({ metric: activity.coindaysDestroyed.cumulative, name: "All-time", color, unit: Unit.coindays }),
line({ series: activity.coindaysDestroyed.cumulative, name: "All-time", color, unit: Unit.coindays }),
],
},
],
@@ -80,7 +80,7 @@ function volumeAndCoinsTree(activity, color, title) {
/**
* Sent in profit/loss breakdown tree (shared by full and mid-level activity)
* @param {Brk.BaseCumulativeInSumPattern} sent
* @param {(metric: string) => string} title
* @param {(name: string) => string} title
* @returns {PartialOptionsTree}
*/
function sentProfitLossTree(sent, title) {
@@ -92,39 +92,39 @@ function sentProfitLossTree(sent, title) {
name: "USD",
title: title("Sent Volume In Profit"),
bottom: [
line({ metric: sent.inProfit.base.usd, name: "Base", color: colors.profit, unit: Unit.usd }),
line({ metric: sent.inProfit.sum._24h.usd, name: "24h", color: colors.time._24h, unit: Unit.usd, defaultActive: false }),
line({ metric: sent.inProfit.sum._1w.usd, name: "1w", color: colors.time._1w, unit: Unit.usd, defaultActive: false }),
line({ metric: sent.inProfit.sum._1m.usd, name: "1m", color: colors.time._1m, unit: Unit.usd, defaultActive: false }),
line({ metric: sent.inProfit.sum._1y.usd, name: "1y", color: colors.time._1y, unit: Unit.usd, defaultActive: false }),
line({ series: sent.inProfit.base.usd, name: "Base", color: colors.profit, unit: Unit.usd }),
line({ series: sent.inProfit.sum._24h.usd, name: "24h", color: colors.time._24h, unit: Unit.usd, defaultActive: false }),
line({ series: sent.inProfit.sum._1w.usd, name: "1w", color: colors.time._1w, unit: Unit.usd, defaultActive: false }),
line({ series: sent.inProfit.sum._1m.usd, name: "1m", color: colors.time._1m, unit: Unit.usd, defaultActive: false }),
line({ series: sent.inProfit.sum._1y.usd, name: "1y", color: colors.time._1y, unit: Unit.usd, defaultActive: false }),
],
},
{
name: "BTC",
title: title("Sent Volume In Profit (BTC)"),
bottom: [
line({ metric: sent.inProfit.base.btc, name: "Base", color: colors.profit, unit: Unit.btc }),
line({ metric: sent.inProfit.sum._24h.btc, name: "24h", color: colors.time._24h, unit: Unit.btc, defaultActive: false }),
line({ metric: sent.inProfit.sum._1w.btc, name: "1w", color: colors.time._1w, unit: Unit.btc, defaultActive: false }),
line({ metric: sent.inProfit.sum._1m.btc, name: "1m", color: colors.time._1m, unit: Unit.btc, defaultActive: false }),
line({ metric: sent.inProfit.sum._1y.btc, name: "1y", color: colors.time._1y, unit: Unit.btc, defaultActive: false }),
line({ series: sent.inProfit.base.btc, name: "Base", color: colors.profit, unit: Unit.btc }),
line({ series: sent.inProfit.sum._24h.btc, name: "24h", color: colors.time._24h, unit: Unit.btc, defaultActive: false }),
line({ series: sent.inProfit.sum._1w.btc, name: "1w", color: colors.time._1w, unit: Unit.btc, defaultActive: false }),
line({ series: sent.inProfit.sum._1m.btc, name: "1m", color: colors.time._1m, unit: Unit.btc, defaultActive: false }),
line({ series: sent.inProfit.sum._1y.btc, name: "1y", color: colors.time._1y, unit: Unit.btc, defaultActive: false }),
],
},
{
name: "Sats",
title: title("Sent Volume In Profit (Sats)"),
bottom: [
line({ metric: sent.inProfit.base.sats, name: "Base", color: colors.profit, unit: Unit.sats }),
line({ metric: sent.inProfit.sum._24h.sats, name: "24h", color: colors.time._24h, unit: Unit.sats, defaultActive: false }),
line({ metric: sent.inProfit.sum._1w.sats, name: "1w", color: colors.time._1w, unit: Unit.sats, defaultActive: false }),
line({ metric: sent.inProfit.sum._1m.sats, name: "1m", color: colors.time._1m, unit: Unit.sats, defaultActive: false }),
line({ metric: sent.inProfit.sum._1y.sats, name: "1y", color: colors.time._1y, unit: Unit.sats, defaultActive: false }),
line({ series: sent.inProfit.base.sats, name: "Base", color: colors.profit, unit: Unit.sats }),
line({ series: sent.inProfit.sum._24h.sats, name: "24h", color: colors.time._24h, unit: Unit.sats, defaultActive: false }),
line({ series: sent.inProfit.sum._1w.sats, name: "1w", color: colors.time._1w, unit: Unit.sats, defaultActive: false }),
line({ series: sent.inProfit.sum._1m.sats, name: "1m", color: colors.time._1m, unit: Unit.sats, defaultActive: false }),
line({ series: sent.inProfit.sum._1y.sats, name: "1y", color: colors.time._1y, unit: Unit.sats, defaultActive: false }),
],
},
{ name: "Cumulative", title: title("Cumulative Sent In Profit"), bottom: [
line({ metric: sent.inProfit.cumulative.usd, name: "USD", color: colors.profit, unit: Unit.usd }),
line({ metric: sent.inProfit.cumulative.btc, name: "BTC", color: colors.profit, unit: Unit.btc, defaultActive: false }),
line({ metric: sent.inProfit.cumulative.sats, name: "Sats", color: colors.profit, unit: Unit.sats, defaultActive: false }),
line({ series: sent.inProfit.cumulative.usd, name: "USD", color: colors.profit, unit: Unit.usd }),
line({ series: sent.inProfit.cumulative.btc, name: "BTC", color: colors.profit, unit: Unit.btc, defaultActive: false }),
line({ series: sent.inProfit.cumulative.sats, name: "Sats", color: colors.profit, unit: Unit.sats, defaultActive: false }),
]},
],
},
@@ -135,39 +135,39 @@ function sentProfitLossTree(sent, title) {
name: "USD",
title: title("Sent Volume In Loss"),
bottom: [
line({ metric: sent.inLoss.base.usd, name: "Base", color: colors.loss, unit: Unit.usd }),
line({ metric: sent.inLoss.sum._24h.usd, name: "24h", color: colors.time._24h, unit: Unit.usd, defaultActive: false }),
line({ metric: sent.inLoss.sum._1w.usd, name: "1w", color: colors.time._1w, unit: Unit.usd, defaultActive: false }),
line({ metric: sent.inLoss.sum._1m.usd, name: "1m", color: colors.time._1m, unit: Unit.usd, defaultActive: false }),
line({ metric: sent.inLoss.sum._1y.usd, name: "1y", color: colors.time._1y, unit: Unit.usd, defaultActive: false }),
line({ series: sent.inLoss.base.usd, name: "Base", color: colors.loss, unit: Unit.usd }),
line({ series: sent.inLoss.sum._24h.usd, name: "24h", color: colors.time._24h, unit: Unit.usd, defaultActive: false }),
line({ series: sent.inLoss.sum._1w.usd, name: "1w", color: colors.time._1w, unit: Unit.usd, defaultActive: false }),
line({ series: sent.inLoss.sum._1m.usd, name: "1m", color: colors.time._1m, unit: Unit.usd, defaultActive: false }),
line({ series: sent.inLoss.sum._1y.usd, name: "1y", color: colors.time._1y, unit: Unit.usd, defaultActive: false }),
],
},
{
name: "BTC",
title: title("Sent Volume In Loss (BTC)"),
bottom: [
line({ metric: sent.inLoss.base.btc, name: "Base", color: colors.loss, unit: Unit.btc }),
line({ metric: sent.inLoss.sum._24h.btc, name: "24h", color: colors.time._24h, unit: Unit.btc, defaultActive: false }),
line({ metric: sent.inLoss.sum._1w.btc, name: "1w", color: colors.time._1w, unit: Unit.btc, defaultActive: false }),
line({ metric: sent.inLoss.sum._1m.btc, name: "1m", color: colors.time._1m, unit: Unit.btc, defaultActive: false }),
line({ metric: sent.inLoss.sum._1y.btc, name: "1y", color: colors.time._1y, unit: Unit.btc, defaultActive: false }),
line({ series: sent.inLoss.base.btc, name: "Base", color: colors.loss, unit: Unit.btc }),
line({ series: sent.inLoss.sum._24h.btc, name: "24h", color: colors.time._24h, unit: Unit.btc, defaultActive: false }),
line({ series: sent.inLoss.sum._1w.btc, name: "1w", color: colors.time._1w, unit: Unit.btc, defaultActive: false }),
line({ series: sent.inLoss.sum._1m.btc, name: "1m", color: colors.time._1m, unit: Unit.btc, defaultActive: false }),
line({ series: sent.inLoss.sum._1y.btc, name: "1y", color: colors.time._1y, unit: Unit.btc, defaultActive: false }),
],
},
{
name: "Sats",
title: title("Sent Volume In Loss (Sats)"),
bottom: [
line({ metric: sent.inLoss.base.sats, name: "Base", color: colors.loss, unit: Unit.sats }),
line({ metric: sent.inLoss.sum._24h.sats, name: "24h", color: colors.time._24h, unit: Unit.sats, defaultActive: false }),
line({ metric: sent.inLoss.sum._1w.sats, name: "1w", color: colors.time._1w, unit: Unit.sats, defaultActive: false }),
line({ metric: sent.inLoss.sum._1m.sats, name: "1m", color: colors.time._1m, unit: Unit.sats, defaultActive: false }),
line({ metric: sent.inLoss.sum._1y.sats, name: "1y", color: colors.time._1y, unit: Unit.sats, defaultActive: false }),
line({ series: sent.inLoss.base.sats, name: "Base", color: colors.loss, unit: Unit.sats }),
line({ series: sent.inLoss.sum._24h.sats, name: "24h", color: colors.time._24h, unit: Unit.sats, defaultActive: false }),
line({ series: sent.inLoss.sum._1w.sats, name: "1w", color: colors.time._1w, unit: Unit.sats, defaultActive: false }),
line({ series: sent.inLoss.sum._1m.sats, name: "1m", color: colors.time._1m, unit: Unit.sats, defaultActive: false }),
line({ series: sent.inLoss.sum._1y.sats, name: "1y", color: colors.time._1y, unit: Unit.sats, defaultActive: false }),
],
},
{ name: "Cumulative", title: title("Cumulative Sent In Loss"), bottom: [
line({ metric: sent.inLoss.cumulative.usd, name: "USD", color: colors.loss, unit: Unit.usd }),
line({ metric: sent.inLoss.cumulative.btc, name: "BTC", color: colors.loss, unit: Unit.btc, defaultActive: false }),
line({ metric: sent.inLoss.cumulative.sats, name: "Sats", color: colors.loss, unit: Unit.sats, defaultActive: false }),
line({ series: sent.inLoss.cumulative.usd, name: "USD", color: colors.loss, unit: Unit.usd }),
line({ series: sent.inLoss.cumulative.btc, name: "BTC", color: colors.loss, unit: Unit.btc, defaultActive: false }),
line({ series: sent.inLoss.cumulative.sats, name: "Sats", color: colors.loss, unit: Unit.sats, defaultActive: false }),
]},
],
},
@@ -178,7 +178,7 @@ function sentProfitLossTree(sent, title) {
* Volume and coins tree with coinyears, dormancy, and sent in profit/loss (All/STH/LTH)
* @param {Brk.CoindaysCoinyearsDormancySentPattern} activity
* @param {Color} color
* @param {(metric: string) => string} title
* @param {(name: string) => string} title
* @returns {PartialOptionsTree}
*/
function fullVolumeTree(activity, color, title) {
@@ -188,12 +188,12 @@ function fullVolumeTree(activity, color, title) {
{
name: "Coinyears Destroyed",
title: title("Coinyears Destroyed"),
bottom: [line({ metric: activity.coinyearsDestroyed, name: "CYD", color, unit: Unit.years })],
bottom: [line({ series: activity.coinyearsDestroyed, name: "CYD", color, unit: Unit.years })],
},
{
name: "Dormancy",
title: title("Dormancy"),
bottom: [line({ metric: activity.dormancy, name: "Dormancy", color, unit: Unit.days })],
bottom: [line({ series: activity.dormancy, name: "Dormancy", color, unit: Unit.days })],
},
];
}
@@ -204,7 +204,7 @@ function fullVolumeTree(activity, color, title) {
/**
* @param {Brk._1m1w1y24hPattern<number>} ratio
* @param {(metric: string) => string} title
* @param {(name: string) => string} title
* @param {string} [prefix]
* @returns {PartialOptionsTree}
*/
@@ -214,31 +214,31 @@ function singleRollingSoprTree(ratio, title, prefix = "") {
name: "Compare",
title: title(`Rolling ${prefix}SOPR`),
bottom: [
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 }),
baseline({ series: ratio._24h, name: "24h", color: colors.time._24h, unit: Unit.ratio, base: 1 }),
baseline({ series: ratio._1w, name: "7d", color: colors.time._1w, unit: Unit.ratio, base: 1 }),
baseline({ series: ratio._1m, name: "30d", color: colors.time._1m, unit: Unit.ratio, base: 1 }),
baseline({ series: ratio._1y, name: "1y", color: colors.time._1y, unit: Unit.ratio, base: 1 }),
],
},
{
name: "24h",
title: title(`${prefix}SOPR (24h)`),
bottom: [dotsBaseline({ metric: ratio._24h, name: "24h", unit: Unit.ratio, base: 1 })],
bottom: [dotsBaseline({ series: ratio._24h, name: "24h", unit: Unit.ratio, base: 1 })],
},
{
name: "7d",
title: title(`${prefix}SOPR (7d)`),
bottom: [baseline({ metric: ratio._1w, name: "SOPR", unit: Unit.ratio, base: 1 })],
bottom: [baseline({ series: ratio._1w, name: "SOPR", unit: Unit.ratio, base: 1 })],
},
{
name: "30d",
title: title(`${prefix}SOPR (30d)`),
bottom: [baseline({ metric: ratio._1m, name: "SOPR", unit: Unit.ratio, base: 1 })],
bottom: [baseline({ series: ratio._1m, name: "SOPR", unit: Unit.ratio, base: 1 })],
},
{
name: "1y",
title: title(`${prefix}SOPR (1y)`),
bottom: [baseline({ metric: ratio._1y, name: "SOPR", unit: Unit.ratio, base: 1 })],
bottom: [baseline({ series: ratio._1y, name: "SOPR", unit: Unit.ratio, base: 1 })],
},
];
}
@@ -249,7 +249,7 @@ function singleRollingSoprTree(ratio, title, prefix = "") {
/**
* @param {Brk._1m1w1y24hPattern6} sellSideRisk
* @param {(metric: string) => string} title
* @param {(name: string) => string} title
* @returns {PartialOptionsTree}
*/
function singleSellSideRiskTree(sellSideRisk, title) {
@@ -294,7 +294,7 @@ function singleSellSideRiskTree(sellSideRisk, title) {
/**
* @param {Brk.BaseCumulativeSumPattern<number>} valueCreated
* @param {Brk.BaseCumulativeSumPattern<number>} valueDestroyed
* @param {(metric: string) => string} title
* @param {(name: string) => string} title
* @param {string} [prefix]
* @returns {PartialOptionsTree}
*/
@@ -307,20 +307,20 @@ function singleRollingValueTree(valueCreated, valueDestroyed, title, prefix = ""
name: "Created",
title: title(`Rolling ${prefix}Value Created`),
bottom: [
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 }),
line({ series: valueCreated.sum._24h, name: "24h", color: colors.time._24h, unit: Unit.usd }),
line({ series: valueCreated.sum._1w, name: "7d", color: colors.time._1w, unit: Unit.usd }),
line({ series: valueCreated.sum._1m, name: "30d", color: colors.time._1m, unit: Unit.usd }),
line({ series: valueCreated.sum._1y, name: "1y", color: colors.time._1y, unit: Unit.usd }),
],
},
{
name: "Destroyed",
title: title(`Rolling ${prefix}Value Destroyed`),
bottom: [
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 }),
line({ series: valueDestroyed.sum._24h, name: "24h", color: colors.time._24h, unit: Unit.usd }),
line({ series: valueDestroyed.sum._1w, name: "7d", color: colors.time._1w, unit: Unit.usd }),
line({ series: valueDestroyed.sum._1m, name: "30d", color: colors.time._1m, unit: Unit.usd }),
line({ series: valueDestroyed.sum._1y, name: "1y", color: colors.time._1y, unit: Unit.usd }),
],
},
],
@@ -329,40 +329,40 @@ function singleRollingValueTree(valueCreated, valueDestroyed, title, prefix = ""
name: "24h",
title: title(`${prefix}Value Created & Destroyed (24h)`),
bottom: [
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 }),
line({ series: valueCreated.sum._24h, name: "Created", color: colors.usd, unit: Unit.usd }),
line({ series: valueDestroyed.sum._24h, name: "Destroyed", color: colors.loss, unit: Unit.usd }),
],
},
{
name: "7d",
title: title(`${prefix}Value Created & Destroyed (7d)`),
bottom: [
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 }),
line({ series: valueCreated.sum._1w, name: "Created", color: colors.usd, unit: Unit.usd }),
line({ series: valueDestroyed.sum._1w, name: "Destroyed", color: colors.loss, unit: Unit.usd }),
],
},
{
name: "30d",
title: title(`${prefix}Value Created & Destroyed (30d)`),
bottom: [
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 }),
line({ series: valueCreated.sum._1m, name: "Created", color: colors.usd, unit: Unit.usd }),
line({ series: valueDestroyed.sum._1m, name: "Destroyed", color: colors.loss, unit: Unit.usd }),
],
},
{
name: "1y",
title: title(`${prefix}Value Created & Destroyed (1y)`),
bottom: [
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 }),
line({ series: valueCreated.sum._1y, name: "Created", color: colors.usd, unit: Unit.usd }),
line({ series: valueDestroyed.sum._1y, name: "Destroyed", color: colors.loss, unit: Unit.usd }),
],
},
{
name: "Cumulative",
title: title(`${prefix}Value Created & Destroyed (Total)`),
bottom: [
line({ metric: valueCreated.cumulative, name: "Created", color: colors.usd, unit: Unit.usd }),
line({ metric: valueDestroyed.cumulative, name: "Destroyed", color: colors.loss, unit: Unit.usd }),
line({ series: valueCreated.cumulative, name: "Created", color: colors.usd, unit: Unit.usd }),
line({ series: valueDestroyed.cumulative, name: "Destroyed", color: colors.loss, unit: Unit.usd }),
],
},
];
@@ -374,12 +374,12 @@ function singleRollingValueTree(valueCreated, valueDestroyed, title, prefix = ""
* @param {Brk.BaseCapitulationCumulativeNegativeRelSumValuePattern} loss
* @param {Brk.BaseCumulativeSumPattern<number>} valueCreated
* @param {Brk.BaseCumulativeSumPattern<number>} valueDestroyed
* @param {AnyFetchedSeriesBlueprint[]} extraValueMetrics
* @param {AnyFetchedSeriesBlueprint[]} extraValueSeries
* @param {PartialOptionsTree} rollingTree
* @param {(metric: string) => string} title
* @param {(name: string) => string} title
* @returns {PartialOptionsGroup}
*/
function fullValueSection(profit, loss, valueCreated, valueDestroyed, extraValueMetrics, rollingTree, title) {
function fullValueSection(profit, loss, valueCreated, valueDestroyed, extraValueSeries, rollingTree, title) {
return {
name: "Value",
tree: [
@@ -387,17 +387,17 @@ function fullValueSection(profit, loss, valueCreated, valueDestroyed, extraValue
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 }),
line({ series: profit.distributionFlow, name: "Distribution Flow", color: colors.profit, unit: Unit.usd }),
line({ series: 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,
line({ series: valueCreated.base, name: "Created", color: colors.usd, unit: Unit.usd }),
line({ series: valueDestroyed.base, name: "Destroyed", color: colors.loss, unit: Unit.usd }),
...extraValueSeries,
],
},
{
@@ -407,16 +407,16 @@ function fullValueSection(profit, loss, valueCreated, valueDestroyed, extraValue
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 }),
line({ series: profit.valueCreated.base, name: "Created", color: colors.profit, unit: Unit.usd }),
line({ series: 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 }),
line({ series: loss.valueCreated.base, name: "Created", color: colors.profit, unit: Unit.usd }),
line({ series: loss.valueDestroyed.base, name: "Destroyed", color: colors.loss, unit: Unit.usd }),
],
},
],
@@ -430,7 +430,7 @@ function fullValueSection(profit, loss, valueCreated, valueDestroyed, extraValue
* Simple value section (created & destroyed + rolling)
* @param {Brk.BaseCumulativeSumPattern<number>} valueCreated
* @param {Brk.BaseCumulativeSumPattern<number>} valueDestroyed
* @param {(metric: string) => string} title
* @param {(name: string) => string} title
* @returns {PartialOptionsGroup}
*/
function simpleValueSection(valueCreated, valueDestroyed, title) {
@@ -441,8 +441,8 @@ function simpleValueSection(valueCreated, valueDestroyed, title) {
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 }),
line({ series: valueCreated.base, name: "Created", color: colors.usd, unit: Unit.usd }),
line({ series: valueDestroyed.base, name: "Destroyed", color: colors.loss, unit: Unit.usd }),
],
},
{
@@ -459,7 +459,7 @@ function simpleValueSection(valueCreated, valueDestroyed, title) {
/**
* Full activity with adjusted SOPR (All/STH)
* @param {{ cohort: CohortAll | CohortFull, title: (metric: string) => string }} args
* @param {{ cohort: CohortAll | CohortFull, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createActivitySectionWithAdjusted({ cohort, title }) {
@@ -489,8 +489,8 @@ export function createActivitySectionWithAdjusted({ cohort, title }) {
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 }),
line({ series: sopr.adjusted.valueCreated.base, name: "Adjusted Created", color: colors.adjustedCreated, unit: Unit.usd, defaultActive: false }),
line({ series: sopr.adjusted.valueDestroyed.base, name: "Adjusted Destroyed", color: colors.adjustedDestroyed, unit: Unit.usd, defaultActive: false }),
],
[
{
@@ -510,7 +510,7 @@ export function createActivitySectionWithAdjusted({ cohort, title }) {
/**
* Activity section for cohorts with rolling SOPR + sell side risk (LTH, also CohortFull | CohortLongTerm)
* @param {{ cohort: CohortFull | CohortLongTerm, title: (metric: string) => string }} args
* @param {{ cohort: CohortFull | CohortLongTerm, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createActivitySection({ cohort, title }) {
@@ -540,7 +540,7 @@ export function createActivitySection({ cohort, title }) {
/**
* Activity section for cohorts with activity but basic realized (AgeRange/MaxAge 24h SOPR only)
* @param {{ cohort: CohortAgeRange | CohortWithAdjusted, title: (metric: string) => string }} args
* @param {{ cohort: CohortAgeRange | CohortWithAdjusted, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createActivitySectionWithActivity({ cohort, title }) {
@@ -555,7 +555,7 @@ export function createActivitySectionWithActivity({ cohort, title }) {
{
name: "SOPR",
title: title("SOPR (24h)"),
bottom: [dotsBaseline({ metric: sopr.ratio._24h, name: "SOPR", unit: Unit.ratio, base: 1 })],
bottom: [dotsBaseline({ series: sopr.ratio._24h, name: "SOPR", unit: Unit.ratio, base: 1 })],
},
simpleValueSection(sopr.valueCreated, sopr.valueDestroyed, title),
],
@@ -564,7 +564,7 @@ export function createActivitySectionWithActivity({ cohort, title }) {
/**
* Minimal activity section for cohorts without activity field (value only)
* @param {{ cohort: CohortBasicWithMarketCap | CohortBasicWithoutMarketCap | CohortWithoutRelative | CohortAddress | AddressCohortObject, title: (metric: string) => string }} args
* @param {{ cohort: CohortBasicWithMarketCap | CohortBasicWithoutMarketCap | CohortWithoutRelative | CohortAddress | AddressCohortObject, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createActivitySectionMinimal({ cohort, title }) {
@@ -587,10 +587,10 @@ export function createActivitySectionMinimal({ cohort, title }) {
* @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 {(item: T | A) => AnySeriesPattern} getRaw
* @param {(item: T | A) => AnySeriesPattern} get7d
* @param {(item: T | A) => AnySeriesPattern} get30d
* @param {(name: string) => string} title
* @param {string} [prefix]
* @returns {PartialOptionsTree}
*/
@@ -600,21 +600,21 @@ function groupedSoprCharts(list, all, getRaw, get7d, get30d, title, prefix = "")
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 }),
baseline({ series: 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 }),
baseline({ series: 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 }),
baseline({ series: get30d(item), name: item.name, color: item.color, unit: Unit.ratio, base: 1 }),
),
},
];
@@ -625,11 +625,11 @@ function groupedSoprCharts(list, all, getRaw, get7d, get30d, title, prefix = "")
* @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 {(item: T | A) => AnySeriesPattern} get24h
* @param {(item: T | A) => AnySeriesPattern} get7d
* @param {(item: T | A) => AnySeriesPattern} get30d
* @param {(item: T | A) => AnySeriesPattern} get1y
* @param {(name: string) => string} title
* @param {string} [prefix]
* @returns {PartialOptionsTree}
*/
@@ -639,28 +639,28 @@ function groupedRollingSoprCharts(list, all, get24h, get7d, get30d, get1y, title
name: "24h",
title: title(`${prefix}SOPR (24h)`),
bottom: mapCohortsWithAll(list, all, (c) =>
baseline({ metric: get24h(c), name: c.name, color: c.color, unit: Unit.ratio, base: 1 }),
baseline({ series: get24h(c), name: c.name, color: c.color, unit: Unit.ratio, base: 1 }),
),
},
{
name: "7d",
title: title(`${prefix}SOPR (7d)`),
bottom: mapCohortsWithAll(list, all, (c) =>
baseline({ metric: get7d(c), name: c.name, color: c.color, unit: Unit.ratio, base: 1 }),
baseline({ series: get7d(c), name: c.name, color: c.color, unit: Unit.ratio, base: 1 }),
),
},
{
name: "30d",
title: title(`${prefix}SOPR (30d)`),
bottom: mapCohortsWithAll(list, all, (c) =>
baseline({ metric: get30d(c), name: c.name, color: c.color, unit: Unit.ratio, base: 1 }),
baseline({ series: get30d(c), name: c.name, color: c.color, unit: Unit.ratio, base: 1 }),
),
},
{
name: "1y",
title: title(`${prefix}SOPR (1y)`),
bottom: mapCohortsWithAll(list, all, (c) =>
baseline({ metric: get1y(c), name: c.name, color: c.color, unit: Unit.ratio, base: 1 }),
baseline({ series: get1y(c), name: c.name, color: c.color, unit: Unit.ratio, base: 1 }),
),
},
];
@@ -675,8 +675,8 @@ function groupedRollingSoprCharts(list, all, get24h, get7d, get30d, get1y, title
* @template {{ color: Color, name: string }} A
* @param {readonly T[]} list
* @param {A} all
* @param {readonly { name: string, getCreated: (item: T | A) => AnyMetricPattern, getDestroyed: (item: T | A) => AnyMetricPattern }[]} windows
* @param {(metric: string) => string} title
* @param {readonly { name: string, getCreated: (item: T | A) => AnySeriesPattern, getDestroyed: (item: T | A) => AnySeriesPattern }[]} windows
* @param {(name: string) => string} title
* @param {string} [prefix]
* @returns {PartialOptionsTree}
*/
@@ -688,7 +688,7 @@ function groupedRollingValueCharts(list, all, windows, title, prefix = "") {
name: w.name,
title: title(`${prefix}Value Created (${w.name})`),
bottom: mapCohortsWithAll(list, all, (item) =>
line({ metric: w.getCreated(item), name: item.name, color: item.color, unit: Unit.usd }),
line({ series: w.getCreated(item), name: item.name, color: item.color, unit: Unit.usd }),
),
})),
},
@@ -698,7 +698,7 @@ function groupedRollingValueCharts(list, all, windows, title, prefix = "") {
name: w.name,
title: title(`${prefix}Value Destroyed (${w.name})`),
bottom: mapCohortsWithAll(list, all, (item) =>
line({ metric: w.getDestroyed(item), name: item.name, color: item.color, unit: Unit.usd }),
line({ series: w.getDestroyed(item), name: item.name, color: item.color, unit: Unit.usd }),
),
})),
},
@@ -723,7 +723,7 @@ function valueWindows(list, all) {
// ============================================================================
/**
* @param {{ list: readonly CohortFull[], all: CohortAll, title: (metric: string) => string }} args
* @param {{ list: readonly CohortFull[], all: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedActivitySectionWithAdjusted({ list, all, title }) {
@@ -734,7 +734,7 @@ export function createGroupedActivitySectionWithAdjusted({ list, all, title }) {
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 }),
line({ series: tree.activity.sent.sum._24h, name, color, unit: Unit.sats }),
]),
},
{
@@ -793,10 +793,10 @@ export function createGroupedActivitySectionWithAdjusted({ list, all, title }) {
{
name: "Sell Side Risk",
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: "24h", title: title("Sell Side Risk (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: 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({ series: 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({ series: 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({ series: tree.realized.sellSideRiskRatio._1y.ratio, name, color, unit: Unit.ratio })) },
],
},
{
@@ -805,12 +805,12 @@ export function createGroupedActivitySectionWithAdjusted({ list, all, title }) {
{
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: "Distribution", title: title("Distribution Flow"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.profit.distributionFlow, name, color, unit: Unit.usd })) },
{ name: "Capitulation", title: title("Capitulation Flow"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: 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: "Created", title: title("Value Created"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sopr.valueCreated.base, name, color, unit: Unit.usd })) },
{ name: "Destroyed", title: title("Value Destroyed"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sopr.valueDestroyed.base, name, color, unit: Unit.usd })) },
{
name: "Rolling",
tree: [
@@ -840,7 +840,7 @@ export function createGroupedActivitySectionWithAdjusted({ 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 }),
line({ series: tree.activity.coindaysDestroyed.sum._24h, name, color, unit: Unit.coindays }),
]),
},
],
@@ -849,7 +849,7 @@ export function createGroupedActivitySectionWithAdjusted({ list, all, title }) {
/**
* Grouped activity for cohorts with rolling SOPR + sell side risk (LTH-like)
* @param {{ list: readonly (CohortFull | CohortLongTerm)[], all: CohortAll, title: (metric: string) => string }} args
* @param {{ list: readonly (CohortFull | CohortLongTerm)[], all: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedActivitySection({ list, all, title }) {
@@ -860,7 +860,7 @@ export function createGroupedActivitySection({ list, all, title }) {
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 }),
line({ series: tree.activity.sent.sum._24h, name, color, unit: Unit.sats }),
]),
},
{
@@ -889,10 +889,10 @@ export function createGroupedActivitySection({ list, all, title }) {
{
name: "Sell Side Risk",
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: "24h", title: title("Sell Side Risk (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: 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({ series: 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({ series: 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({ series: tree.realized.sellSideRiskRatio._1y.ratio, name, color, unit: Unit.ratio })) },
],
},
{
@@ -901,12 +901,12 @@ export function createGroupedActivitySection({ list, all, title }) {
{
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: "Distribution", title: title("Distribution Flow"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.profit.distributionFlow, name, color, unit: Unit.usd })) },
{ name: "Capitulation", title: title("Capitulation Flow"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: 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: "Created", title: title("Value Created"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sopr.valueCreated.base, name, color, unit: Unit.usd })) },
{ name: "Destroyed", title: title("Value Destroyed"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sopr.valueDestroyed.base, name, color, unit: Unit.usd })) },
{
name: "Rolling",
tree: groupedRollingValueCharts(list, all, valueWindows(list, all), title),
@@ -917,7 +917,7 @@ export function createGroupedActivitySection({ 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 }),
line({ series: tree.activity.coindaysDestroyed.sum._24h, name, color, unit: Unit.coindays }),
]),
},
],
@@ -926,7 +926,7 @@ export function createGroupedActivitySection({ list, all, title }) {
/**
* Grouped activity for cohorts with activity but basic realized (AgeRange/MaxAge)
* @param {{ list: readonly (CohortAgeRange | CohortWithAdjusted)[], all: CohortAll, title: (metric: string) => string }} args
* @param {{ list: readonly (CohortAgeRange | CohortWithAdjusted)[], all: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedActivitySectionWithActivity({ list, all, title }) {
@@ -937,28 +937,28 @@ export function createGroupedActivitySectionWithActivity({ list, all, title }) {
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 }),
line({ series: 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 }),
baseline({ series: 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: "Created", title: title("Value Created"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: tree.realized.sopr.valueCreated.base, name, color, unit: Unit.usd })) },
{ name: "Destroyed", title: title("Value Destroyed"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: 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 }),
line({ series: tree.activity.coindaysDestroyed.sum._24h, name, color, unit: Unit.coindays }),
]),
},
],
@@ -967,15 +967,15 @@ export function createGroupedActivitySectionWithActivity({ list, all, title }) {
/**
* Grouped minimal activity (value only, no activity field)
* @param {{ list: readonly (UtxoCohortObject | CohortWithoutRelative | CohortAddress | AddressCohortObject)[], all: CohortAll, title: (metric: string) => string }} args
* @param {{ list: readonly (UtxoCohortObject | CohortWithoutRelative | CohortAddress | AddressCohortObject)[], all: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
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 })) },
{ name: "Value Created", title: title("Value Created"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ series: 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({ series: tree.realized.sopr.valueDestroyed.base, name, color, unit: Unit.usd })) },
],
};
}
@@ -27,9 +27,9 @@ const ACTIVE_PCTS = new Set(["pct75", "pct50", "pct25"]);
function createCorePercentileSeries(p, n = (x) => x) {
return entries(p)
.reverse()
.map(([key, metric], i, arr) =>
.map(([key, s], i, arr) =>
price({
metric,
series: s,
name: n(key.replace("pct", "p")),
color: colors.at(i, arr.length),
...(ACTIVE_PCTS.has(key) ? {} : { defaultActive: false }),
@@ -45,28 +45,28 @@ function createSingleSummarySeries(cohort) {
const { color, tree } = cohort;
const p = tree.costBasis.percentiles;
return [
price({ metric: tree.realized.price, name: "Average", color }),
price({ series: tree.realized.price, name: "Average", color }),
price({
metric: tree.costBasis.max,
series: tree.costBasis.max,
name: "Max (p100)",
color: colors.stat.max,
defaultActive: false,
}),
price({
metric: p.pct75,
series: p.pct75,
name: "Q3 (p75)",
color: colors.stat.pct75,
defaultActive: false,
}),
price({ metric: p.pct50, name: "Median (p50)", color: colors.stat.median }),
price({ series: p.pct50, name: "Median (p50)", color: colors.stat.median }),
price({
metric: p.pct25,
series: p.pct25,
name: "Q1 (p25)",
color: colors.stat.pct25,
defaultActive: false,
}),
price({
metric: tree.costBasis.min,
series: tree.costBasis.min,
name: "Min (p0)",
color: colors.stat.min,
defaultActive: false,
@@ -81,7 +81,7 @@ function createSingleSummarySeries(cohort) {
*/
function createGroupedSummarySeries(list, all) {
return mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({ metric: tree.realized.price, name, color }),
price({ series: tree.realized.price, name, color }),
);
}
@@ -93,16 +93,16 @@ function createSingleByCoinSeries(cohort) {
const { color, tree } = cohort;
const cb = tree.costBasis;
return [
price({ metric: tree.realized.price, name: "Average", color }),
price({ series: tree.realized.price, name: "Average", color }),
price({
metric: cb.max,
series: cb.max,
name: "p100",
color: colors.stat.max,
defaultActive: false,
}),
...createCorePercentileSeries(cb.percentiles),
price({
metric: cb.min,
series: cb.min,
name: "p0",
color: colors.stat.min,
defaultActive: false,
@@ -117,7 +117,7 @@ function createSingleByCoinSeries(cohort) {
function createSingleByCapitalSeries(cohort) {
const { color, tree } = cohort;
return [
price({ metric: tree.realized.investor.price, name: "Average", color }),
price({ series: tree.realized.investor.price, name: "Average", color }),
...createCorePercentileSeries(tree.costBasis.investedCapital),
];
}
@@ -139,7 +139,7 @@ function createSingleSupplyDensitySeries(cohort) {
}
/**
* @param {{ cohort: CohortAll | CohortFull | CohortLongTerm, title: (metric: string) => string }} args
* @param {{ cohort: CohortAll | CohortFull | CohortLongTerm, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createCostBasisSectionWithPercentiles({ cohort, title }) {
@@ -171,7 +171,7 @@ export function createCostBasisSectionWithPercentiles({ cohort, title }) {
}
/**
* @param {{ list: readonly (CohortAll | CohortFull | CohortLongTerm)[], all: CohortAll, title: (metric: string) => string }} args
* @param {{ list: readonly (CohortAll | CohortFull | CohortLongTerm)[], all: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedCostBasisSectionWithPercentiles({
@@ -194,28 +194,28 @@ export function createGroupedCostBasisSectionWithPercentiles({
name: "Average",
title: title("Realized Price Comparison"),
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({ metric: tree.realized.price, name, color }),
price({ series: tree.realized.price, name, color }),
),
},
{
name: "Median",
title: title("Cost Basis Median (BTC-weighted)"),
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({ metric: tree.costBasis.percentiles.pct50, name, color }),
price({ series: tree.costBasis.percentiles.pct50, name, color }),
),
},
{
name: "Q3",
title: title("Cost Basis Q3 (BTC-weighted)"),
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({ metric: tree.costBasis.percentiles.pct75, name, color }),
price({ series: tree.costBasis.percentiles.pct75, name, color }),
),
},
{
name: "Q1",
title: title("Cost Basis Q1 (BTC-weighted)"),
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({ metric: tree.costBasis.percentiles.pct25, name, color }),
price({ series: tree.costBasis.percentiles.pct25, name, color }),
),
},
],
@@ -227,7 +227,7 @@ export function createGroupedCostBasisSectionWithPercentiles({
name: "Average",
title: title("Investor Price Comparison"),
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({ metric: tree.realized.investor.price, name, color }),
price({ series: tree.realized.investor.price, name, color }),
),
},
{
@@ -235,7 +235,7 @@ export function createGroupedCostBasisSectionWithPercentiles({
title: title("Cost Basis Median (USD-weighted)"),
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({
metric: tree.costBasis.investedCapital.pct50,
series: tree.costBasis.investedCapital.pct50,
name,
color,
}),
@@ -246,7 +246,7 @@ export function createGroupedCostBasisSectionWithPercentiles({
title: title("Cost Basis Q3 (USD-weighted)"),
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({
metric: tree.costBasis.investedCapital.pct75,
series: tree.costBasis.investedCapital.pct75,
name,
color,
}),
@@ -257,7 +257,7 @@ export function createGroupedCostBasisSectionWithPercentiles({
title: title("Cost Basis Q1 (USD-weighted)"),
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({
metric: tree.costBasis.investedCapital.pct25,
series: tree.costBasis.investedCapital.pct25,
name,
color,
}),
+3 -3
View File
@@ -19,9 +19,9 @@ const isAddressable = (key) =>
/** @type {readonly string[]} */ (ADDRESSABLE_TYPES).includes(key);
export function buildCohortData() {
const utxoCohorts = brk.metrics.cohorts.utxo;
const addressCohorts = brk.metrics.cohorts.address;
const { addresses } = brk.metrics;
const utxoCohorts = brk.series.cohorts.utxo;
const addressCohorts = brk.series.cohorts.address;
const { addresses } = brk.series;
const {
TERM_NAMES,
EPOCH_NAMES,
@@ -3,7 +3,7 @@
*
* Supply pattern capabilities by cohort type:
* - DeltaHalfInRelTotalPattern2 (STH/LTH): inProfit + inLoss + relToCirculating + relToOwn
* - MetricsTree_Cohorts_Utxo_All_Supply (All): inProfit + inLoss + relToOwn (no relToCirculating)
* - SeriesTree_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
@@ -74,40 +74,40 @@ function fullSupplySeries(supply) {
/**
* % of Own Supply series (profit/loss relative to own supply)
* @param {{ inProfit: { relToOwn: { percent: AnyMetricPattern, ratio: AnyMetricPattern } }, inLoss: { relToOwn: { percent: AnyMetricPattern, ratio: AnyMetricPattern } } }} supply
* @param {{ inProfit: { relToOwn: { percent: AnySeriesPattern, ratio: AnySeriesPattern } }, inLoss: { relToOwn: { percent: AnySeriesPattern, ratio: AnySeriesPattern } } }} supply
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function ownSupplyPctSeries(supply) {
return [
line({ metric: supply.inProfit.relToOwn.percent, name: "In Profit", color: colors.profit, unit: Unit.pctOwn }),
line({ metric: supply.inLoss.relToOwn.percent, name: "In Loss", color: colors.loss, unit: Unit.pctOwn }),
line({ metric: supply.inProfit.relToOwn.ratio, name: "In Profit", color: colors.profit, unit: Unit.ratio }),
line({ metric: supply.inLoss.relToOwn.ratio, name: "In Loss", color: colors.loss, unit: Unit.ratio }),
line({ series: supply.inProfit.relToOwn.percent, name: "In Profit", color: colors.profit, unit: Unit.pctOwn }),
line({ series: supply.inLoss.relToOwn.percent, name: "In Loss", color: colors.loss, unit: Unit.pctOwn }),
line({ series: supply.inProfit.relToOwn.ratio, name: "In Profit", color: colors.profit, unit: Unit.ratio }),
line({ series: supply.inLoss.relToOwn.ratio, name: "In Loss", color: colors.loss, unit: Unit.ratio }),
...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwn }),
];
}
/**
* % of Circulating Supply series (total, profit, loss)
* @param {{ relToCirculating: { percent: AnyMetricPattern }, inProfit: { relToCirculating: { percent: AnyMetricPattern } }, inLoss: { relToCirculating: { percent: AnyMetricPattern } } }} supply
* @param {{ relToCirculating: { percent: AnySeriesPattern }, inProfit: { relToCirculating: { percent: AnySeriesPattern } }, inLoss: { relToCirculating: { percent: AnySeriesPattern } } }} supply
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function circulatingSupplyPctSeries(supply) {
return [
line({
metric: supply.relToCirculating.percent,
series: supply.relToCirculating.percent,
name: "Total",
color: colors.default,
unit: Unit.pctSupply,
}),
line({
metric: supply.inProfit.relToCirculating.percent,
series: supply.inProfit.relToCirculating.percent,
name: "In Profit",
color: colors.profit,
unit: Unit.pctSupply,
}),
line({
metric: supply.inLoss.relToCirculating.percent,
series: supply.inLoss.relToCirculating.percent,
name: "In Loss",
color: colors.loss,
unit: Unit.pctSupply,
@@ -117,25 +117,25 @@ function circulatingSupplyPctSeries(supply) {
/**
* Ratio of Circulating Supply series (total, profit, loss)
* @param {{ relToCirculating: { ratio: AnyMetricPattern }, inProfit: { relToCirculating: { ratio: AnyMetricPattern } }, inLoss: { relToCirculating: { ratio: AnyMetricPattern } } }} supply
* @param {{ relToCirculating: { ratio: AnySeriesPattern }, inProfit: { relToCirculating: { ratio: AnySeriesPattern } }, inLoss: { relToCirculating: { ratio: AnySeriesPattern } } }} supply
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function circulatingSupplyRatioSeries(supply) {
return [
line({
metric: supply.relToCirculating.ratio,
series: supply.relToCirculating.ratio,
name: "Total",
color: colors.default,
unit: Unit.ratio,
}),
line({
metric: supply.inProfit.relToCirculating.ratio,
series: supply.inProfit.relToCirculating.ratio,
name: "In Profit",
color: colors.profit,
unit: Unit.ratio,
}),
line({
metric: supply.inLoss.relToCirculating.ratio,
series: supply.inLoss.relToCirculating.ratio,
name: "In Loss",
color: colors.loss,
unit: Unit.ratio,
@@ -146,7 +146,7 @@ function circulatingSupplyRatioSeries(supply) {
/**
* @param {readonly (UtxoCohortObject | CohortWithoutRelative)[]} list
* @param {CohortAll} all
* @param {(metric: string) => string} title
* @param {(name: string) => string} title
*/
function groupedUtxoCountChart(list, all, title) {
return {
@@ -154,7 +154,7 @@ function groupedUtxoCountChart(list, all, title) {
title: title("UTXO Count"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({
metric: tree.outputs.unspentCount.inner,
series: tree.outputs.unspentCount.inner,
name,
color,
unit: Unit.count,
@@ -164,9 +164,9 @@ function groupedUtxoCountChart(list, all, title) {
}
/**
* @param {{ absolute: { _24h: AnyMetricPattern, _1w: AnyMetricPattern, _1m: AnyMetricPattern, _1y: AnyMetricPattern }, rate: { _24h: { percent: AnyMetricPattern, ratio: AnyMetricPattern }, _1w: { percent: AnyMetricPattern, ratio: AnyMetricPattern }, _1m: { percent: AnyMetricPattern, ratio: AnyMetricPattern }, _1y: { percent: AnyMetricPattern, ratio: AnyMetricPattern } } }} delta
* @param {{ absolute: { _24h: AnySeriesPattern, _1w: AnySeriesPattern, _1m: AnySeriesPattern, _1y: AnySeriesPattern }, rate: { _24h: { percent: AnySeriesPattern, ratio: AnySeriesPattern }, _1w: { percent: AnySeriesPattern, ratio: AnySeriesPattern }, _1m: { percent: AnySeriesPattern, ratio: AnySeriesPattern }, _1y: { percent: AnySeriesPattern, ratio: AnySeriesPattern } } }} delta
* @param {Unit} unit
* @param {(metric: string) => string} title
* @param {(name: string) => string} title
* @param {string} name
* @returns {PartialOptionsGroup}
*/
@@ -187,7 +187,7 @@ function singleDeltaTree(delta, unit, title, name) {
* @param {A} all
* @param {(c: T | A) => DeltaPattern} getDelta
* @param {Unit} unit
* @param {(metric: string) => string} title
* @param {(name: string) => string} title
* @param {string} name
* @returns {PartialOptionsGroup}
*/
@@ -201,7 +201,7 @@ function groupedDeltaTree(list, all, getDelta, unit, title, name) {
name: w.name,
title: title(`${name} Change (${w.name})`),
bottom: mapCohortsWithAll(list, all, (c) =>
baseline({ metric: getDelta(c).absolute[w.key], name: c.name, color: c.color, unit }),
baseline({ series: getDelta(c).absolute[w.key], name: c.name, color: c.color, unit }),
),
})),
},
@@ -221,7 +221,7 @@ function groupedDeltaTree(list, all, getDelta, unit, title, name) {
/**
* @param {UtxoCohortObject | CohortWithoutRelative} cohort
* @param {(metric: string) => string} title
* @param {(name: string) => string} title
* @returns {PartialChartOption}
*/
function singleUtxoCountChart(cohort, title) {
@@ -230,7 +230,7 @@ function singleUtxoCountChart(cohort, title) {
title: title("UTXO Count"),
bottom: [
line({
metric: cohort.tree.outputs.unspentCount.inner,
series: cohort.tree.outputs.unspentCount.inner,
name: "UTXO Count",
color: cohort.color,
unit: Unit.count,
@@ -242,7 +242,7 @@ function singleUtxoCountChart(cohort, title) {
/**
* @param {CohortAll | CohortAddress | AddressCohortObject} cohort
* @param {(metric: string) => string} title
* @param {(name: string) => string} title
* @returns {PartialChartOption}
*/
function singleAddressCountChart(cohort, title) {
@@ -251,7 +251,7 @@ function singleAddressCountChart(cohort, title) {
title: title("Address Count"),
bottom: [
line({
metric: cohort.addressCount.inner,
series: cohort.addressCount.inner,
name: "Address Count",
color: cohort.color,
unit: Unit.count,
@@ -268,7 +268,7 @@ function singleAddressCountChart(cohort, title) {
/**
* Basic holdings (total + half only, no supply breakdown)
* For: CohortWithoutRelative, CohortBasicWithMarketCap, CohortBasicWithoutMarketCap
* @param {{ cohort: UtxoCohortObject | CohortWithoutRelative, title: (metric: string) => string }} args
* @param {{ cohort: UtxoCohortObject | CohortWithoutRelative, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSection({ cohort, title }) {
@@ -294,7 +294,7 @@ export function createHoldingsSection({ cohort, title }) {
/**
* Holdings for CohortAll (has inProfit/inLoss with relToOwn but no relToCirculating)
* @param {{ cohort: CohortAll, title: (metric: string) => string }} args
* @param {{ cohort: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSectionAll({ cohort, title }) {
@@ -325,9 +325,9 @@ export function createHoldingsSectionAll({ cohort, title }) {
}
/**
* Holdings with full relative metrics (relToCirculating + relToOwn)
* Holdings with full relative series (relToCirculating + relToOwn)
* For: CohortFull, CohortLongTerm (have DeltaHalfInRelTotalPattern2)
* @param {{ cohort: CohortFull | CohortLongTerm, title: (metric: string) => string }} args
* @param {{ cohort: CohortFull | CohortLongTerm, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSectionWithRelative({ cohort, title }) {
@@ -369,7 +369,7 @@ export function createHoldingsSectionWithRelative({ cohort, title }) {
/**
* Holdings with inProfit/inLoss + relToCirculating (no relToOwn)
* For: CohortWithAdjusted, CohortAgeRange (have DeltaHalfInRelTotalPattern)
* @param {{ cohort: CohortWithAdjusted | CohortAgeRange, title: (metric: string) => string }} args
* @param {{ cohort: CohortWithAdjusted | CohortAgeRange, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSectionWithOwnSupply({ cohort, title }) {
@@ -410,7 +410,7 @@ export function createHoldingsSectionWithOwnSupply({ cohort, title }) {
/**
* Holdings with inProfit/inLoss (no rel, no address count)
* For: CohortWithoutRelative (p2ms, unknown, empty)
* @param {{ cohort: CohortWithoutRelative, title: (metric: string) => string }} args
* @param {{ cohort: CohortWithoutRelative, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSectionWithProfitLoss({ cohort, title }) {
@@ -436,7 +436,7 @@ export function createHoldingsSectionWithProfitLoss({ cohort, title }) {
/**
* Holdings for CohortAddress (has inProfit/inLoss but no rel, plus address count)
* @param {{ cohort: CohortAddress, title: (metric: string) => string }} args
* @param {{ cohort: CohortAddress, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSectionAddress({ cohort, title }) {
@@ -464,7 +464,7 @@ export function createHoldingsSectionAddress({ cohort, title }) {
/**
* Holdings for address amount cohorts (no inProfit/inLoss, has address count)
* @param {{ cohort: AddressCohortObject, title: (metric: string) => string }} args
* @param {{ cohort: AddressCohortObject, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createHoldingsSectionAddressAmount({ cohort, title }) {
@@ -495,7 +495,7 @@ export function createHoldingsSectionAddressAmount({ cohort, title }) {
// ============================================================================
/**
* @param {{ list: readonly CohortAddress[], all: CohortAll, title: (metric: string) => string }} args
* @param {{ list: readonly CohortAddress[], all: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedHoldingsSectionAddress({ list, all, title }) {
@@ -541,7 +541,7 @@ export function createGroupedHoldingsSectionAddress({ 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 }),
line({ series: addressCount.inner, name, color, unit: Unit.count }),
),
},
{
@@ -558,7 +558,7 @@ export function createGroupedHoldingsSectionAddress({ list, all, title }) {
/**
* Grouped holdings for address amount cohorts (no inProfit/inLoss, has address count)
* @param {{ list: readonly AddressCohortObject[], all: CohortAll, title: (metric: string) => string }} args
* @param {{ list: readonly AddressCohortObject[], all: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedHoldingsSectionAddressAmount({
@@ -586,7 +586,7 @@ export function createGroupedHoldingsSectionAddressAmount({
name: "Address Count",
title: title("Address Count"),
bottom: mapCohortsWithAll(list, all, ({ name, color, addressCount }) =>
line({ metric: addressCount.inner, name, color, unit: Unit.count }),
line({ series: addressCount.inner, name, color, unit: Unit.count }),
),
},
{
@@ -603,7 +603,7 @@ export function createGroupedHoldingsSectionAddressAmount({
/**
* Basic grouped holdings (total + half only)
* @param {{ list: readonly (UtxoCohortObject | CohortWithoutRelative)[], all: CohortAll, title: (metric: string) => string }} args
* @param {{ list: readonly (UtxoCohortObject | CohortWithoutRelative)[], all: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedHoldingsSection({ list, all, title }) {
@@ -637,7 +637,7 @@ export function createGroupedHoldingsSection({ list, all, title }) {
/**
* Grouped holdings with inProfit/inLoss (no rel, no address count)
* For: CohortWithoutRelative (p2ms, unknown, empty)
* @param {{ list: readonly CohortWithoutRelative[], all: CohortAll, title: (metric: string) => string }} args
* @param {{ list: readonly CohortWithoutRelative[], all: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedHoldingsSectionWithProfitLoss({
@@ -697,7 +697,7 @@ export function createGroupedHoldingsSectionWithProfitLoss({
/**
* Grouped holdings with inProfit/inLoss + relToCirculating (no relToOwn)
* For: CohortWithAdjusted, CohortAgeRange
* @param {{ list: readonly (CohortWithAdjusted | CohortAgeRange)[], all: CohortAll, title: (metric: string) => string }} args
* @param {{ list: readonly (CohortWithAdjusted | CohortAgeRange)[], all: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedHoldingsSectionWithOwnSupply({
@@ -720,7 +720,7 @@ export function createGroupedHoldingsSectionWithOwnSupply({
),
...mapCohorts(list, ({ name, color, tree }) =>
line({
metric: tree.supply.relToCirculating.percent,
series: tree.supply.relToCirculating.percent,
name,
color,
unit: Unit.pctSupply,
@@ -741,7 +741,7 @@ export function createGroupedHoldingsSectionWithOwnSupply({
),
...mapCohorts(list, ({ name, color, tree }) =>
line({
metric: tree.supply.inProfit.relToCirculating.percent,
series: tree.supply.inProfit.relToCirculating.percent,
name,
color,
unit: Unit.pctSupply,
@@ -762,7 +762,7 @@ export function createGroupedHoldingsSectionWithOwnSupply({
),
...mapCohorts(list, ({ name, color, tree }) =>
line({
metric: tree.supply.inLoss.relToCirculating.percent,
series: tree.supply.inLoss.relToCirculating.percent,
name,
color,
unit: Unit.pctSupply,
@@ -785,9 +785,9 @@ export function createGroupedHoldingsSectionWithOwnSupply({
}
/**
* Grouped holdings with full relative metrics (relToCirculating + relToOwn)
* Grouped holdings with full relative series (relToCirculating + relToOwn)
* For: CohortFull, CohortLongTerm
* @param {{ list: readonly (CohortFull | CohortLongTerm)[], all: CohortAll, title: (metric: string) => string }} args
* @param {{ list: readonly (CohortFull | CohortLongTerm)[], all: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedHoldingsSectionWithRelative({ list, all, title }) {
@@ -806,7 +806,7 @@ export function createGroupedHoldingsSectionWithRelative({ list, all, title }) {
),
...mapCohorts(list, ({ name, color, tree }) =>
line({
metric: tree.supply.relToCirculating.percent,
series: tree.supply.relToCirculating.percent,
name,
color,
unit: Unit.pctSupply,
@@ -827,7 +827,7 @@ export function createGroupedHoldingsSectionWithRelative({ list, all, title }) {
),
...mapCohorts(list, ({ name, color, tree }) =>
line({
metric: tree.supply.inProfit.relToCirculating.percent,
series: tree.supply.inProfit.relToCirculating.percent,
name,
color,
unit: Unit.pctSupply,
@@ -835,7 +835,7 @@ export function createGroupedHoldingsSectionWithRelative({ list, all, title }) {
),
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({
metric: tree.supply.inProfit.relToOwn.percent,
series: tree.supply.inProfit.relToOwn.percent,
name,
color,
unit: Unit.pctOwn,
@@ -857,7 +857,7 @@ export function createGroupedHoldingsSectionWithRelative({ list, all, title }) {
),
...mapCohorts(list, ({ name, color, tree }) =>
line({
metric: tree.supply.inLoss.relToCirculating.percent,
series: tree.supply.inLoss.relToCirculating.percent,
name,
color,
unit: Unit.pctSupply,
@@ -865,7 +865,7 @@ export function createGroupedHoldingsSectionWithRelative({ list, all, title }) {
),
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({
metric: tree.supply.inLoss.relToOwn.percent,
series: tree.supply.inLoss.relToOwn.percent,
name,
color,
unit: Unit.pctOwn,
+16 -24
View File
@@ -13,6 +13,7 @@
import { formatCohortTitle, satsBtcUsd, satsBtcUsdFullTree } from "../shared.js";
import { ROLLING_WINDOWS, line, baseline, percentRatio, rollingWindowsTree, rollingPercentRatioTree } from "../series.js";
import { Unit } from "../../utils/units.js";
import { colors } from "../../utils/colors.js";
// Section builders
import {
@@ -602,14 +603,12 @@ function singleBucketFolder({ name, color, pattern }) {
name: "Supply",
tree: [
{
name: "All",
name: "Value",
title: `${name}: Supply`,
bottom: satsBtcUsd({ pattern: pattern.supply.all, name, color }),
},
{
name: "STH",
title: `${name}: STH Supply`,
bottom: satsBtcUsd({ pattern: pattern.supply.sth, name, color }),
bottom: [
...satsBtcUsd({ pattern: pattern.supply.all, name: "Total" }),
...satsBtcUsd({ pattern: pattern.supply.sth, name: "STH", color: colors.term.short }),
],
},
{
name: "Change",
@@ -622,23 +621,16 @@ function singleBucketFolder({ name, color, pattern }) {
},
{
name: "Realized Cap",
tree: [
{
name: "All",
title: `${name}: Realized Cap`,
bottom: [line({ metric: pattern.realizedCap.all, name, color, unit: Unit.usd })],
},
{
name: "STH",
title: `${name}: STH Realized Cap`,
bottom: [line({ metric: pattern.realizedCap.sth, name, color, unit: Unit.usd })],
},
title: `${name}: Realized Cap`,
bottom: [
line({ series: pattern.realizedCap.all, name: "Total", unit: Unit.usd }),
line({ series: pattern.realizedCap.sth, name: "STH", color: colors.term.short, unit: Unit.usd }),
],
},
{
name: "NUPL",
title: `${name}: NUPL`,
bottom: [line({ metric: pattern.nupl.ratio, name, color, unit: Unit.ratio })],
bottom: [line({ series: pattern.nupl.ratio, name, color, unit: Unit.ratio })],
},
],
};
@@ -679,7 +671,7 @@ function groupedBucketCharts(list, titlePrefix) {
title: `${titlePrefix}: Supply Change`,
bottom: ROLLING_WINDOWS.flatMap((w) =>
list.map(({ name, color, pattern }) =>
baseline({ metric: pattern.supply.all.delta.absolute[w.key], name: `${name} ${w.name}`, color, unit: Unit.sats }),
baseline({ series: pattern.supply.all.delta.absolute[w.key], name: `${name} ${w.name}`, color, unit: Unit.sats }),
),
),
},
@@ -687,7 +679,7 @@ function groupedBucketCharts(list, titlePrefix) {
name: w.name,
title: `${titlePrefix}: Supply Change ${w.name}`,
bottom: list.map(({ name, color, pattern }) =>
baseline({ metric: pattern.supply.all.delta.absolute[w.key], name, color, unit: Unit.sats }),
baseline({ series: pattern.supply.all.delta.absolute[w.key], name, color, unit: Unit.sats }),
),
})),
],
@@ -724,14 +716,14 @@ function groupedBucketCharts(list, titlePrefix) {
name: "All",
title: `${titlePrefix}: Realized Cap`,
bottom: list.map(({ name, color, pattern }) =>
line({ metric: pattern.realizedCap.all, name, color, unit: Unit.usd }),
line({ series: pattern.realizedCap.all, name, color, unit: Unit.usd }),
),
},
{
name: "STH",
title: `${titlePrefix}: STH Realized Cap`,
bottom: list.map(({ name, color, pattern }) =>
line({ metric: pattern.realizedCap.sth, name, color, unit: Unit.usd }),
line({ series: pattern.realizedCap.sth, name, color, unit: Unit.usd }),
),
},
],
@@ -740,7 +732,7 @@ function groupedBucketCharts(list, titlePrefix) {
name: "NUPL",
title: `${titlePrefix}: NUPL`,
bottom: list.map(({ name, color, pattern }) =>
line({ metric: pattern.nupl.ratio, name, color, unit: Unit.ratio }),
line({ series: pattern.nupl.ratio, name, color, unit: Unit.ratio }),
),
},
];
+12 -12
View File
@@ -21,7 +21,7 @@ import { Unit } from "../../utils/units.js";
/**
* Create prices section for cohorts with full ratio patterns
* (CohortAll, CohortFull, CohortLongTerm)
* @param {{ cohort: CohortAll | CohortFull | CohortLongTerm, title: (metric: string) => string }} args
* @param {{ cohort: CohortAll | CohortFull | CohortLongTerm, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createPricesSectionFull({ cohort, title }) {
@@ -33,10 +33,10 @@ export function createPricesSectionFull({ cohort, 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 }),
price({ series: tree.realized.price, name: "Realized", color: colors.realized }),
price({ series: tree.realized.investor.price, name: "Investor", color: colors.investor }),
price({ series: tree.realized.investor.upperPriceBand, name: "I²/R", color: colors.stat.max, style: 2, defaultActive: false }),
price({ series: tree.realized.investor.lowerPriceBand, name: "R²/I", color: colors.stat.min, style: 2, defaultActive: false }),
],
},
{
@@ -67,7 +67,7 @@ export function createPricesSectionFull({ 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
* @param {{ cohort: CohortWithAdjusted | CohortBasic | CohortAddress | CohortWithoutRelative | CohortAgeRange, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createPricesSectionBasic({ cohort, title }) {
@@ -81,14 +81,14 @@ export function createPricesSectionBasic({ cohort, title }) {
{
name: "Price",
title: title("Realized Price"),
top: [price({ metric: tree.realized.price, name: "Realized", color })],
top: [price({ series: tree.realized.price, name: "Realized", color })],
},
{
name: "MVRV",
title: title("MVRV"),
bottom: [
baseline({
metric: tree.realized.mvrv,
series: tree.realized.mvrv,
name: "MVRV",
unit: Unit.ratio,
base: 1,
@@ -100,7 +100,7 @@ export function createPricesSectionBasic({ cohort, title }) {
title: title("Realized Price Ratio"),
bottom: [
baseline({
metric: tree.realized.price.ratio,
series: tree.realized.price.ratio,
name: "Price Ratio",
unit: Unit.ratio,
base: 1,
@@ -115,7 +115,7 @@ export function createPricesSectionBasic({ cohort, title }) {
/**
* Create prices section for grouped cohorts
* @param {{ list: readonly CohortObject[], all: CohortAll, title: (metric: string) => string }} args
* @param {{ list: readonly CohortObject[], all: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedPricesSection({ list, all, title }) {
@@ -129,7 +129,7 @@ export function createGroupedPricesSection({ list, all, title }) {
name: "Price",
title: title("Realized Price"),
top: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
price({ metric: tree.realized.price, name, color }),
price({ series: tree.realized.price, name, color }),
),
},
{
@@ -137,7 +137,7 @@ export function createGroupedPricesSection({ list, all, title }) {
title: title("MVRV"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({
metric: tree.realized.mvrv,
series: tree.realized.mvrv,
name,
color,
unit: Unit.ratio,
File diff suppressed because it is too large Load Diff
@@ -22,7 +22,7 @@ function createSingleRealizedCapSeries(cohort) {
const { color, tree } = cohort;
return [
line({
metric: tree.realized.cap.usd,
series: tree.realized.cap.usd,
name: "Realized Cap",
color,
unit: Unit.usd,
@@ -33,7 +33,7 @@ function createSingleRealizedCapSeries(cohort) {
/**
* Create valuation section for cohorts with full ratio patterns
* (CohortAll, CohortFull, CohortWithPercentiles)
* @param {{ cohort: CohortAll | CohortFull | CohortLongTerm, title: (metric: string) => string }} args
* @param {{ cohort: CohortAll | CohortFull | CohortLongTerm, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createValuationSectionFull({ cohort, title }) {
@@ -77,7 +77,7 @@ export function createValuationSectionFull({ cohort, title }) {
/**
* Create valuation section for cohorts with basic ratio patterns
* (CohortWithAdjusted, CohortBasic, CohortAddress, CohortWithoutRelative)
* @param {{ cohort: CohortWithAdjusted | CohortBasic | CohortAddress | CohortWithoutRelative, title: (metric: string) => string }} args
* @param {{ cohort: CohortWithAdjusted | CohortBasic | CohortAddress | CohortWithoutRelative, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createValuationSection({ cohort, title }) {
@@ -102,7 +102,7 @@ export function createValuationSection({ cohort, title }) {
title: title("MVRV"),
bottom: [
baseline({
metric: tree.realized.mvrv,
series: tree.realized.mvrv,
name: "MVRV",
unit: Unit.ratio,
base: 1,
@@ -114,7 +114,7 @@ export function createValuationSection({ cohort, title }) {
}
/**
* @param {{ list: readonly (UtxoCohortObject | CohortWithoutRelative)[], all: CohortAll, title: (metric: string) => string }} args
* @param {{ list: readonly (UtxoCohortObject | CohortWithoutRelative)[], all: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedValuationSection({ list, all, title }) {
@@ -126,7 +126,7 @@ export function createGroupedValuationSection({ list, all, title }) {
title: title("Realized Cap"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({
metric: tree.realized.cap.usd,
series: tree.realized.cap.usd,
name,
color,
unit: Unit.usd,
@@ -142,7 +142,7 @@ export function createGroupedValuationSection({ list, all, title }) {
name: w.name,
title: title(`Realized Cap Change (${w.name})`),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({ metric: tree.realized.cap.delta.absolute[w.key].usd, name, color, unit: Unit.usd }),
baseline({ series: tree.realized.cap.delta.absolute[w.key].usd, name, color, unit: Unit.usd }),
),
})),
},
@@ -163,7 +163,7 @@ export function createGroupedValuationSection({ list, all, title }) {
title: title("MVRV"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({
metric: tree.realized.mvrv,
series: tree.realized.mvrv,
name,
color,
unit: Unit.ratio,
@@ -176,7 +176,7 @@ export function createGroupedValuationSection({ list, all, title }) {
}
/**
* @param {{ list: readonly (CohortAll | CohortFull | CohortLongTerm)[], all: CohortAll, title: (metric: string) => string }} args
* @param {{ list: readonly (CohortAll | CohortFull | CohortLongTerm)[], all: CohortAll, title: (name: string) => string }} args
* @returns {PartialOptionsGroup}
*/
export function createGroupedValuationSectionWithOwnMarketCap({
@@ -194,7 +194,7 @@ export function createGroupedValuationSectionWithOwnMarketCap({
name: "USD",
title: title("Realized Cap"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
line({ metric: tree.realized.cap.usd, name, color, unit: Unit.usd }),
line({ series: tree.realized.cap.usd, name, color, unit: Unit.usd }),
),
},
{
@@ -215,7 +215,7 @@ export function createGroupedValuationSectionWithOwnMarketCap({
name: w.name,
title: title(`Realized Cap Change (${w.name})`),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({ metric: tree.realized.cap.delta.absolute[w.key].usd, name, color, unit: Unit.usd }),
baseline({ series: tree.realized.cap.delta.absolute[w.key].usd, name, color, unit: Unit.usd }),
),
})),
},
@@ -236,7 +236,7 @@ export function createGroupedValuationSectionWithOwnMarketCap({
title: title("MVRV"),
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
baseline({
metric: tree.realized.mvrv,
series: tree.realized.mvrv,
name,
color,
unit: Unit.ratio,
+17 -17
View File
@@ -122,32 +122,32 @@ export function initOptions() {
for (let i = 0; i < arr.length; i++) {
const blueprint = arr[i];
// Check for undefined metric
if (!blueprint.metric) {
throw new Error(`Blueprint has undefined metric: ${blueprint.title}`);
// Check for undefined series
if (!blueprint.series) {
throw new Error(`Blueprint has undefined series: ${blueprint.title}`);
}
// Check for price pattern blueprint (has usd/sats sub-metrics)
// Check for price pattern blueprint (has usd/sats sub-series)
// Use unknown cast for safe property access check
const maybePriceMetric =
/** @type {{ usd?: AnyMetricPattern, sats?: AnyMetricPattern }} */ (
/** @type {unknown} */ (blueprint.metric)
const maybePriceSeries =
/** @type {{ usd?: AnySeriesPattern, sats?: AnySeriesPattern }} */ (
/** @type {unknown} */ (blueprint.series)
);
if (maybePriceMetric.usd?.by && maybePriceMetric.sats?.by) {
const { usd, sats } = maybePriceMetric;
if (maybePriceSeries.usd?.by && maybePriceSeries.sats?.by) {
const { usd, sats } = maybePriceSeries;
if (!usdArr) map.set(Unit.usd, (usdArr = []));
usdArr.push({ ...blueprint, metric: usd, unit: Unit.usd });
usdArr.push({ ...blueprint, series: usd, unit: Unit.usd });
if (!satsArr) map.set(Unit.sats, (satsArr = []));
satsArr.push({ ...blueprint, metric: sats, unit: Unit.sats });
satsArr.push({ ...blueprint, series: sats, unit: Unit.sats });
continue;
}
// After continue, we know this is a regular metric blueprint
// After continue, we know this is a regular series blueprint
const regularBlueprint = /** @type {AnyFetchedSeriesBlueprint} */ (
blueprint
);
const metric = regularBlueprint.metric;
const s = regularBlueprint.series;
const unit = regularBlueprint.unit;
if (!unit) continue;
@@ -163,7 +163,7 @@ export function initOptions() {
priceSet.add(regularBlueprint.options?.baseValue?.price ?? 0);
} else if (!type || type === "Line") {
// Check if manual price line - avoid Object.values() array allocation
const by = metric.by;
const by = s.by;
for (const k in by) {
if (by[/** @type {Index} */ (k)]?.path?.includes("constant_")) {
priceLines.get(unit)?.delete(parseFloat(regularBlueprint.title));
@@ -178,9 +178,9 @@ export function initOptions() {
const arr = map.get(unit);
if (!arr) continue;
for (const baseValue of values) {
const metric = getConstant(brk.metrics.constants, baseValue);
const s = getConstant(brk.series.constants, baseValue);
arr.push({
metric,
series: s,
title: `${baseValue}`,
color: colors.gray,
unit,
@@ -371,7 +371,7 @@ export function initOptions() {
return { nodes, count: totalCount };
}
logUnused(brk.metrics, partialOptions);
logUnused(brk.series, partialOptions);
const { nodes: processedTree } = processPartialTree(partialOptions);
/**
+8 -8
View File
@@ -47,7 +47,7 @@ const YEARS_2010S = /** @type {const} */ ([2019, 2018, 2017, 2016, 2015]);
const periodName = (key) => periodIdToName(key.slice(1), true);
/**
* @typedef {{ percent: AnyMetricPattern, ratio: AnyMetricPattern }} PercentRatioPattern
* @typedef {{ percent: AnySeriesPattern, ratio: AnySeriesPattern }} PercentRatioPattern
*/
/**
@@ -55,8 +55,8 @@ const periodName = (key) => periodIdToName(key.slice(1), true);
* @typedef {Object} BaseEntryItem
* @property {string} name - Display name
* @property {Color} color - Item color
* @property {AnyPricePattern} costBasis - Cost basis metric
* @property {PercentRatioPattern} returns - Returns metric
* @property {AnyPricePattern} costBasis - Cost basis series
* @property {PercentRatioPattern} returns - Returns series
* @property {AnyValuePattern} stack - Stack pattern
*/
@@ -90,7 +90,7 @@ function buildYearEntry(dca, year, i) {
* @returns {PartialOptionsGroup}
*/
export function createInvestingSection() {
const { market } = brk.metrics;
const { market } = brk.series;
const { dca, lookback, returns } = market;
return {
@@ -111,7 +111,7 @@ export function createInvestingSection() {
*/
function createCompareFolder(context, items) {
const topPane = items.map(({ name, color, costBasis }) =>
price({ metric: costBasis, name, color }),
price({ series: costBasis, name, color }),
);
return {
name: "Compare",
@@ -152,7 +152,7 @@ function createCompareFolder(context, items) {
*/
function createSingleEntryTree(item, returnsBottom) {
const { name, titlePrefix = name, color, costBasis, stack } = item;
const top = [price({ metric: costBasis, name: "Cost Basis", color })];
const top = [price({ series: costBasis, name: "Cost Basis", color })];
return {
name,
tree: [
@@ -203,11 +203,11 @@ export function createDcaVsLumpSumSection({ dca, lookback, returns }) {
/** @param {AllPeriodKey} key */
const topPane = (key) => [
price({
metric: dca.period.costBasis[key],
series: dca.period.costBasis[key],
name: "DCA",
color: colors.profit,
}),
price({ metric: lookback[key], name: "Lump Sum", color: colors.bitcoin }),
price({ series: lookback[key], name: "Lump Sum", color: colors.bitcoin }),
];
/** @param {string} name @param {AllPeriodKey} key */
+58 -58
View File
@@ -21,13 +21,13 @@ import { periodIdToName } from "./utils.js";
* @typedef {Object} Period
* @property {string} id
* @property {Color} color
* @property {{ percent: AnyMetricPattern, ratio: AnyMetricPattern }} returns
* @property {{ percent: AnySeriesPattern, ratio: AnySeriesPattern }} returns
* @property {AnyPricePattern} lookback
* @property {boolean} [defaultActive]
*/
/**
* @typedef {Period & { cagr: { percent: AnyMetricPattern, ratio: AnyMetricPattern } }} PeriodWithCagr
* @typedef {Period & { cagr: { percent: AnySeriesPattern, ratio: AnySeriesPattern } }} PeriodWithCagr
*/
/**
@@ -39,13 +39,13 @@ import { periodIdToName } from "./utils.js";
/**
* Create index (percent) + ratio line pair from a BpsPercentRatioPattern
* @param {{ pattern: { percent: AnyMetricPattern, ratio: AnyMetricPattern }, name: string, color?: Color, defaultActive?: boolean }} args
* @param {{ pattern: { percent: AnySeriesPattern, ratio: AnySeriesPattern }, name: string, color?: Color, defaultActive?: boolean }} args
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function indexRatio({ pattern, name, color, defaultActive }) {
return [
line({ metric: pattern.percent, name, color, defaultActive, unit: Unit.index }),
line({ metric: pattern.ratio, name, color, defaultActive, unit: Unit.ratio }),
line({ series: pattern.percent, name, color, defaultActive, unit: Unit.index }),
line({ series: pattern.ratio, name, color, defaultActive, unit: Unit.ratio }),
];
}
@@ -84,7 +84,7 @@ function createMaSubSection(label, averages) {
name: "Compare",
title: `Price ${label}s`,
top: averages.map((a) =>
price({ metric: a.ratio, name: a.id, color: a.color }),
price({ series: a.ratio, name: a.id, color: a.color }),
),
},
...common.map(toFolder),
@@ -97,16 +97,16 @@ function createMaSubSection(label, averages) {
* @param {string} name
* @param {string} title
* @param {Unit} unit
* @param {{ _1w: AnyMetricPattern, _1m: AnyMetricPattern, _1y: AnyMetricPattern }} metrics
* @param {{ _1w: AnySeriesPattern, _1m: AnySeriesPattern, _1y: AnySeriesPattern }} patterns
*/
function volatilityChart(name, title, unit, metrics) {
function volatilityChart(name, title, unit, patterns) {
return {
name,
title,
bottom: [
line({ metric: metrics._1w, name: "1w", color: colors.time._1w, unit }),
line({ metric: metrics._1m, name: "1m", color: colors.time._1m, unit }),
line({ metric: metrics._1y, name: "1y", color: colors.time._1y, unit }),
line({ series: patterns._1w, name: "1w", color: colors.time._1w, unit }),
line({ series: patterns._1m, name: "1m", color: colors.time._1m, unit }),
line({ series: patterns._1y, name: "1y", color: colors.time._1y, unit }),
],
};
}
@@ -182,13 +182,13 @@ function historicalSubSection(name, periods) {
name: "Compare",
title: `${name} Historical`,
top: periods.map((p) =>
price({ metric: p.lookback, name: p.id, color: p.color }),
price({ series: p.lookback, name: p.id, color: p.color }),
),
},
...periods.map((p) => ({
name: periodIdToName(p.id, true),
title: `${periodIdToName(p.id, true)} Ago`,
top: [price({ metric: p.lookback, name: "Price" })],
top: [price({ series: p.lookback, name: "Price" })],
})),
],
};
@@ -199,7 +199,7 @@ function historicalSubSection(name, periods) {
* @returns {PartialOptionsGroup}
*/
export function createMarketSection() {
const { market, supply, cohorts, prices, indicators } = brk.metrics;
const { market, supply, cohorts, prices, indicators } = brk.series;
const {
movingAverage: ma,
ath,
@@ -385,7 +385,7 @@ export function createMarketSection() {
title: "Sats per Dollar",
bottom: [
line({
metric: prices.spot.sats,
series: prices.spot.sats,
name: "Sats/$",
unit: Unit.sats,
}),
@@ -400,7 +400,7 @@ export function createMarketSection() {
title: "Market Capitalization",
bottom: [
line({
metric: supply.marketCap.usd,
series: supply.marketCap.usd,
name: "Market Cap",
unit: Unit.usd,
}),
@@ -411,7 +411,7 @@ export function createMarketSection() {
title: "Realized Capitalization",
bottom: [
line({
metric: cohorts.utxo.all.realized.cap.usd,
series: cohorts.utxo.all.realized.cap.usd,
name: "Realized Cap",
color: colors.realized,
unit: Unit.usd,
@@ -428,7 +428,7 @@ export function createMarketSection() {
color: colors.bitcoin,
}),
baseline({
metric: supply.marketMinusRealizedCapGrowthRate._24h,
series: supply.marketMinusRealizedCapGrowthRate._24h,
name: "Market - Realized",
unit: Unit.percentage,
}),
@@ -443,7 +443,7 @@ export function createMarketSection() {
{
name: "Drawdown",
title: "ATH Drawdown",
top: [price({ metric: ath.high, name: "ATH" })],
top: [price({ series: ath.high, name: "ATH" })],
bottom: percentRatio({
pattern: ath.drawdown,
name: "Drawdown",
@@ -453,26 +453,26 @@ export function createMarketSection() {
{
name: "Time Since",
title: "Time Since ATH",
top: [price({ metric: ath.high, name: "ATH" })],
top: [price({ series: ath.high, name: "ATH" })],
bottom: [
line({
metric: ath.daysSince,
series: ath.daysSince,
name: "Since",
unit: Unit.days,
}),
line({
metric: ath.yearsSince,
series: ath.yearsSince,
name: "Since",
unit: Unit.years,
}),
line({
metric: ath.maxDaysBetween,
series: ath.maxDaysBetween,
name: "Max",
color: colors.loss,
unit: Unit.days,
}),
line({
metric: ath.maxYearsBetween,
series: ath.maxYearsBetween,
name: "Max",
color: colors.loss,
unit: Unit.years,
@@ -515,13 +515,13 @@ export function createMarketSection() {
title: "True Range",
bottom: [
line({
metric: range.trueRange,
series: range.trueRange,
name: "Daily",
color: colors.time._24h,
unit: Unit.usd,
}),
line({
metric: range.trueRangeSum2w,
series: range.trueRangeSum2w,
name: "2w Sum",
color: colors.time._1w,
unit: Unit.usd,
@@ -555,12 +555,12 @@ export function createMarketSection() {
title: "SMA vs EMA Comparison",
top: smaVsEma.flatMap((p) => [
price({
metric: p.sma,
series: p.sma,
name: `${p.id} SMA`,
color: p.color,
}),
price({
metric: p.ema,
series: p.ema,
name: `${p.id} EMA`,
color: p.color,
style: 1,
@@ -571,9 +571,9 @@ export function createMarketSection() {
name: p.name,
title: `${p.name} SMA vs EMA`,
top: [
price({ metric: p.sma, name: "SMA", color: p.color }),
price({ series: p.sma, name: "SMA", color: p.color }),
price({
metric: p.ema,
series: p.ema,
name: "EMA",
color: p.color,
style: 1,
@@ -622,13 +622,13 @@ export function createMarketSection() {
title: `${p.name} MinMax`,
top: [
price({
metric: p.max,
series: p.max,
name: "Max",
key: "price-max",
color: colors.stat.max,
}),
price({
metric: p.min,
series: p.min,
name: "Min",
key: "price-min",
color: colors.stat.min,
@@ -641,17 +641,17 @@ export function createMarketSection() {
title: "Mayer Multiple",
top: [
price({
metric: ma.sma._200d,
series: ma.sma._200d,
name: "200d SMA",
color: colors.indicator.main,
}),
price({
metric: ma.sma._200d.x24,
series: ma.sma._200d.x24,
name: "200d SMA x2.4",
color: colors.indicator.upper,
}),
price({
metric: ma.sma._200d.x08,
series: ma.sma._200d.x08,
name: "200d SMA x0.8",
color: colors.indicator.lower,
}),
@@ -698,10 +698,10 @@ export function createMarketSection() {
name: "Components",
title: `RSI Components (${w.name})`,
bottom: [
line({ metric: rsi.averageGain, name: "Avg Gain", color: colors.profit, unit: Unit.usd }),
line({ metric: rsi.averageLoss, name: "Avg Loss", color: colors.loss, unit: Unit.usd }),
line({ metric: rsi.gains, name: "Gains", color: colors.profit, defaultActive: false, unit: Unit.usd }),
line({ metric: rsi.losses, name: "Losses", color: colors.loss, defaultActive: false, unit: Unit.usd }),
line({ series: rsi.averageGain, name: "Avg Gain", color: colors.profit, unit: Unit.usd }),
line({ series: rsi.averageLoss, name: "Avg Loss", color: colors.loss, unit: Unit.usd }),
line({ series: rsi.gains, name: "Gains", color: colors.profit, defaultActive: false, unit: Unit.usd }),
line({ series: rsi.losses, name: "Losses", color: colors.loss, defaultActive: false, unit: Unit.usd }),
],
},
],
@@ -753,16 +753,16 @@ export function createMarketSection() {
name: "Compare",
title: "MACD Comparison",
bottom: ROLLING_WINDOWS.map((w) =>
line({ metric: technical.macd[w.key].line, name: w.name, color: w.color, unit: Unit.usd }),
line({ series: technical.macd[w.key].line, name: w.name, color: w.color, unit: Unit.usd }),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `MACD (${w.name})`,
bottom: [
line({ metric: technical.macd[w.key].line, name: "MACD", color: colors.indicator.fast, unit: Unit.usd }),
line({ metric: technical.macd[w.key].signal, name: "Signal", color: colors.indicator.slow, unit: Unit.usd }),
histogram({ metric: technical.macd[w.key].histogram, name: "Histogram", unit: Unit.usd }),
line({ series: technical.macd[w.key].line, name: "MACD", color: colors.indicator.fast, unit: Unit.usd }),
line({ series: technical.macd[w.key].signal, name: "Signal", color: colors.indicator.slow, unit: Unit.usd }),
histogram({ series: technical.macd[w.key].histogram, name: "Histogram", unit: Unit.usd }),
],
})),
],
@@ -778,7 +778,7 @@ export function createMarketSection() {
title: "Historical Comparison",
top: [...shortPeriods, ...longPeriods].map((p) =>
price({
metric: p.lookback,
series: p.lookback,
name: p.id,
color: p.color,
defaultActive: p.defaultActive,
@@ -795,7 +795,7 @@ export function createMarketSection() {
title: "Dollar Cost Average Sats/Day",
bottom: [
line({
metric: dca.satsPerDay,
series: dca.satsPerDay,
name: "Sats/Day",
unit: Unit.sats,
}),
@@ -810,19 +810,19 @@ export function createMarketSection() {
title: "Pi Cycle",
top: [
price({
metric: ma.sma._111d,
series: ma.sma._111d,
name: "111d SMA",
color: colors.indicator.upper,
}),
price({
metric: ma.sma._350d.x2,
series: ma.sma._350d.x2,
name: "350d SMA x2",
color: colors.indicator.lower,
}),
],
bottom: [
baseline({
metric: technical.piCycle.ratio,
series: technical.piCycle.ratio,
name: "Pi Cycle",
unit: Unit.ratio,
base: 1,
@@ -834,7 +834,7 @@ export function createMarketSection() {
title: "Puell Multiple",
bottom: [
line({
metric: indicators.puellMultiple.ratio,
series: indicators.puellMultiple.ratio,
name: "Puell",
color: colors.usd,
unit: Unit.ratio,
@@ -846,7 +846,7 @@ export function createMarketSection() {
title: "NVT Ratio",
bottom: [
line({
metric: indicators.nvt.ratio,
series: indicators.nvt.ratio,
name: "NVT",
color: colors.bitcoin,
unit: Unit.ratio,
@@ -867,7 +867,7 @@ export function createMarketSection() {
title: "RHODL Ratio",
bottom: [
line({
metric: indicators.rhodlRatio.ratio,
series: indicators.rhodlRatio.ratio,
name: "RHODL",
color: colors.bitcoin,
unit: Unit.ratio,
@@ -879,7 +879,7 @@ export function createMarketSection() {
title: "Thermocap Multiple",
bottom: [
line({
metric: indicators.thermocapMultiple.ratio,
series: indicators.thermocapMultiple.ratio,
name: "Thermocap",
color: colors.bitcoin,
unit: Unit.ratio,
@@ -891,7 +891,7 @@ export function createMarketSection() {
title: "Stock-to-Flow",
bottom: [
line({
metric: indicators.stockToFlow,
series: indicators.stockToFlow,
name: "S2F",
color: colors.bitcoin,
unit: Unit.ratio,
@@ -903,13 +903,13 @@ export function createMarketSection() {
title: "Dormancy",
bottom: [
line({
metric: indicators.dormancy.supplyAdjusted,
series: indicators.dormancy.supplyAdjusted,
name: "Supply Adjusted",
color: colors.bitcoin,
unit: Unit.ratio,
}),
line({
metric: indicators.dormancy.flow,
series: indicators.dormancy.flow,
name: "Flow",
color: colors.usd,
unit: Unit.ratio,
@@ -922,7 +922,7 @@ export function createMarketSection() {
title: "Seller Exhaustion Constant",
bottom: [
line({
metric: indicators.sellerExhaustionConstant,
series: indicators.sellerExhaustionConstant,
name: "SEC",
color: colors.bitcoin,
unit: Unit.ratio,
@@ -934,7 +934,7 @@ export function createMarketSection() {
title: "Coindays Destroyed (Supply Adjusted)",
bottom: [
line({
metric: indicators.coindaysDestroyedSupplyAdjusted,
series: indicators.coindaysDestroyedSupplyAdjusted,
name: "CDD SA",
color: colors.bitcoin,
unit: Unit.ratio,
@@ -946,7 +946,7 @@ export function createMarketSection() {
title: "Coinyears Destroyed (Supply Adjusted)",
bottom: [
line({
metric: indicators.coinyearsDestroyedSupplyAdjusted,
series: indicators.coinyearsDestroyedSupplyAdjusted,
name: "CYD SA",
color: colors.bitcoin,
unit: Unit.ratio,
+35 -35
View File
@@ -56,7 +56,7 @@ const ANTPOOL_AND_FRIENDS_IDS = /** @type {const} */ ([
* @returns {PartialOptionsGroup}
*/
export function createMiningSection() {
const { blocks, pools, mining } = brk.metrics;
const { blocks, pools, mining } = brk.series;
// Pre-compute pool entries with resolved names
const majorPoolData = entries(pools.major).map(([id, pool]) => ({
@@ -101,7 +101,7 @@ export function createMiningSection() {
title: `Blocks Mined: ${name}`,
bottom: [
line({
metric: pool.blocksMined.base,
series: pool.blocksMined.base,
name: "base",
unit: Unit.count,
}),
@@ -113,7 +113,7 @@ export function createMiningSection() {
title: `Blocks Mined: ${name} (Total)`,
bottom: [
line({
metric: pool.blocksMined.cumulative,
series: pool.blocksMined.cumulative,
name: "all-time",
unit: Unit.count,
}),
@@ -180,7 +180,7 @@ export function createMiningSection() {
title: `Blocks Mined: ${name}`,
bottom: [
line({
metric: pool.blocksMined.base,
series: pool.blocksMined.base,
name: "base",
unit: Unit.count,
}),
@@ -192,7 +192,7 @@ export function createMiningSection() {
title: `Blocks Mined: ${name} (Total)`,
bottom: [
line({
metric: pool.blocksMined.cumulative,
series: pool.blocksMined.cumulative,
name: "all-time",
unit: Unit.count,
}),
@@ -215,46 +215,46 @@ export function createMiningSection() {
title: "Network Hashrate",
bottom: [
dots({
metric: mining.hashrate.rate.base,
series: mining.hashrate.rate.base,
name: "Hashrate",
unit: Unit.hashRate,
}),
line({
metric: mining.hashrate.rate.sma._1w,
series: mining.hashrate.rate.sma._1w,
name: "1w SMA",
color: colors.time._1w,
unit: Unit.hashRate,
defaultActive: false,
}),
line({
metric: mining.hashrate.rate.sma._1m,
series: mining.hashrate.rate.sma._1m,
name: "1m SMA",
color: colors.time._1m,
unit: Unit.hashRate,
defaultActive: false,
}),
line({
metric: mining.hashrate.rate.sma._2m,
series: mining.hashrate.rate.sma._2m,
name: "2m SMA",
color: colors.indicator.main,
unit: Unit.hashRate,
defaultActive: false,
}),
line({
metric: mining.hashrate.rate.sma._1y,
series: mining.hashrate.rate.sma._1y,
name: "1y SMA",
color: colors.time._1y,
unit: Unit.hashRate,
defaultActive: false,
}),
dotted({
metric: blocks.difficulty.asHash,
series: blocks.difficulty.asHash,
name: "Difficulty",
color: colors.default,
unit: Unit.hashRate,
}),
line({
metric: mining.hashrate.rate.ath,
series: mining.hashrate.rate.ath,
name: "ATH",
color: colors.loss,
unit: Unit.hashRate,
@@ -267,13 +267,13 @@ export function createMiningSection() {
title: "Network Hashrate ATH",
bottom: [
line({
metric: mining.hashrate.rate.ath,
series: mining.hashrate.rate.ath,
name: "ATH",
color: colors.loss,
unit: Unit.hashRate,
}),
dots({
metric: mining.hashrate.rate.base,
series: mining.hashrate.rate.base,
name: "Hashrate",
color: colors.bitcoin,
unit: Unit.hashRate,
@@ -301,7 +301,7 @@ export function createMiningSection() {
title: "Mining Difficulty",
bottom: [
line({
metric: blocks.difficulty.value,
series: blocks.difficulty.value,
name: "Difficulty",
unit: Unit.difficulty,
}),
@@ -312,7 +312,7 @@ export function createMiningSection() {
title: "Difficulty Epoch",
bottom: [
line({
metric: blocks.difficulty.epoch,
series: blocks.difficulty.epoch,
name: "Epoch",
unit: Unit.epoch,
}),
@@ -323,7 +323,7 @@ export function createMiningSection() {
title: "Difficulty Adjustment",
bottom: [
baseline({
metric: blocks.difficulty.adjustment.percent,
series: blocks.difficulty.adjustment.percent,
name: "Change",
unit: Unit.percentage,
}),
@@ -334,12 +334,12 @@ export function createMiningSection() {
title: "Next Difficulty Adjustment",
bottom: [
line({
metric: blocks.difficulty.blocksBeforeNext,
series: blocks.difficulty.blocksBeforeNext,
name: "Remaining",
unit: Unit.blocks,
}),
line({
metric: blocks.difficulty.daysBeforeNext,
series: blocks.difficulty.daysBeforeNext,
name: "Remaining",
unit: Unit.days,
}),
@@ -480,7 +480,7 @@ export function createMiningSection() {
name: "sum",
}),
line({
metric: mining.rewards.subsidy.sma1y.usd,
series: mining.rewards.subsidy.sma1y.usd,
name: "1y SMA",
color: colors.time._1y,
unit: Unit.usd,
@@ -650,13 +650,13 @@ export function createMiningSection() {
name: "Compare",
title: "Fee-to-Subsidy Ratio",
bottom: ROLLING_WINDOWS.map((w) =>
line({ metric: mining.rewards.fees.ratioMultiple[w.key].ratio, name: w.name, color: w.color, unit: Unit.ratio }),
line({ series: mining.rewards.fees.ratioMultiple[w.key].ratio, name: w.name, color: w.color, unit: Unit.ratio }),
),
},
...ROLLING_WINDOWS.map((w) => ({
name: w.name,
title: `Fee-to-Subsidy Ratio (${w.name})`,
bottom: [line({ metric: mining.rewards.fees.ratioMultiple[w.key].ratio, name: w.name, color: w.color, unit: Unit.ratio })],
bottom: [line({ series: mining.rewards.fees.ratioMultiple[w.key].ratio, name: w.name, color: w.color, unit: Unit.ratio })],
})),
],
},
@@ -712,25 +712,25 @@ export function createMiningSection() {
title: "Hash Price",
bottom: [
line({
metric: mining.hashrate.price.ths,
series: mining.hashrate.price.ths,
name: "TH/s",
color: colors.usd,
unit: Unit.usdPerThsPerDay,
}),
line({
metric: mining.hashrate.price.phs,
series: mining.hashrate.price.phs,
name: "PH/s",
color: colors.usd,
unit: Unit.usdPerPhsPerDay,
}),
dotted({
metric: mining.hashrate.price.thsMin,
series: mining.hashrate.price.thsMin,
name: "TH/s Min",
color: colors.stat.min,
unit: Unit.usdPerThsPerDay,
}),
dotted({
metric: mining.hashrate.price.phsMin,
series: mining.hashrate.price.phsMin,
name: "PH/s Min",
color: colors.stat.min,
unit: Unit.usdPerPhsPerDay,
@@ -742,25 +742,25 @@ export function createMiningSection() {
title: "Hash Value",
bottom: [
line({
metric: mining.hashrate.value.ths,
series: mining.hashrate.value.ths,
name: "TH/s",
color: colors.bitcoin,
unit: Unit.satsPerThsPerDay,
}),
line({
metric: mining.hashrate.value.phs,
series: mining.hashrate.value.phs,
name: "PH/s",
color: colors.bitcoin,
unit: Unit.satsPerPhsPerDay,
}),
dotted({
metric: mining.hashrate.value.thsMin,
series: mining.hashrate.value.thsMin,
name: "TH/s Min",
color: colors.stat.min,
unit: Unit.satsPerThsPerDay,
}),
dotted({
metric: mining.hashrate.value.phsMin,
series: mining.hashrate.value.phsMin,
name: "PH/s Min",
color: colors.stat.min,
unit: Unit.satsPerPhsPerDay,
@@ -787,12 +787,12 @@ export function createMiningSection() {
title: "Next Halving",
bottom: [
line({
metric: blocks.halving.blocksBeforeNext,
series: blocks.halving.blocksBeforeNext,
name: "Remaining",
unit: Unit.blocks,
}),
line({
metric: blocks.halving.daysBeforeNext,
series: blocks.halving.daysBeforeNext,
name: "Remaining",
unit: Unit.days,
}),
@@ -803,7 +803,7 @@ export function createMiningSection() {
title: "Halving Epoch",
bottom: [
line({
metric: blocks.halving.epoch,
series: blocks.halving.epoch,
name: "Epoch",
unit: Unit.epoch,
}),
@@ -836,7 +836,7 @@ export function createMiningSection() {
title: "Blocks Mined: Major Pools (1m)",
bottom: featuredPools.map((p, i) =>
line({
metric: p.pool.blocksMined.sum._1m,
series: p.pool.blocksMined.sum._1m,
name: p.name,
color: colors.at(i, featuredPools.length),
unit: Unit.count,
@@ -877,7 +877,7 @@ export function createMiningSection() {
title: "Blocks Mined: AntPool & Friends (1m)",
bottom: antpoolFriends.map((p, i) =>
line({
metric: p.pool.blocksMined.sum._1m,
series: p.pool.blocksMined.sum._1m,
name: p.name,
color: colors.at(i, antpoolFriends.length),
unit: Unit.count,
+72 -72
View File
@@ -38,7 +38,7 @@ export function createNetworkSection() {
supply,
addresses,
cohorts,
} = brk.metrics;
} = brk.series;
const st = colors.scriptType;
@@ -123,46 +123,46 @@ export function createNetworkSection() {
name: "Funded",
title: "Address Count by Type",
/** @param {AddressableType} t */
getMetric: (t) => addresses.funded[t],
getSeries: (t) => addresses.funded[t],
},
{
name: "Empty",
title: "Empty Address Count by Type",
/** @param {AddressableType} t */
getMetric: (t) => addresses.empty[t],
getSeries: (t) => addresses.empty[t],
},
{
name: "Total",
title: "Total Address Count by Type",
/** @param {AddressableType} t */
getMetric: (t) => addresses.total[t],
getSeries: (t) => addresses.total[t],
},
]);
/**
* Create address metrics tree for a given type key
* Create address series tree for a given type key
* @param {AddressableType | "all"} key
* @param {string} titlePrefix
*/
const createAddressMetricsTree = (key, titlePrefix) => [
const createAddressSeriesTree = (key, titlePrefix) => [
{
name: "Count",
title: `${titlePrefix}Address Count`,
bottom: [
line({
metric: addresses.funded[key],
series: addresses.funded[key],
name: "Funded",
unit: Unit.count,
}),
line({
metric: addresses.empty[key],
series: addresses.empty[key],
name: "Empty",
color: colors.gray,
unit: Unit.count,
defaultActive: false,
}),
line({
metric: addresses.total[key],
series: addresses.total[key],
name: "Total",
color: colors.default,
unit: Unit.count,
@@ -189,7 +189,7 @@ export function createNetworkSection() {
name: "Sum",
title: t,
bottom: [
line({ metric: p.base, name: "base", unit: Unit.count }),
line({ series: p.base, name: "base", unit: Unit.count }),
],
},
rollingWindowsTree({
@@ -202,7 +202,7 @@ export function createNetworkSection() {
title: `${t} (Total)`,
bottom: [
line({
metric: p.cumulative,
series: p.cumulative,
name: "all-time",
unit: Unit.count,
}),
@@ -219,12 +219,12 @@ export function createNetworkSection() {
title: `${titlePrefix}Reactivated Addresses per Block`,
bottom: [
dots({
metric: addresses.activity[key].reactivated.height,
series: addresses.activity[key].reactivated.height,
name: "base",
unit: Unit.count,
}),
line({
metric: addresses.activity[key].reactivated._24h,
series: addresses.activity[key].reactivated._24h,
name: "24h avg",
color: colors.stat.avg,
unit: Unit.count,
@@ -254,12 +254,12 @@ export function createNetworkSection() {
title: `${titlePrefix}${t.title}`,
bottom: [
dots({
metric: addresses.activity[key][t.key].height,
series: addresses.activity[key][t.key].height,
name: "base",
unit: Unit.count,
}),
line({
metric: addresses.activity[key][t.key]._24h,
series: addresses.activity[key][t.key]._24h,
name: "24h avg",
color: colors.stat.avg,
unit: Unit.count,
@@ -292,7 +292,7 @@ export function createNetworkSection() {
title: `${groupName} ${c.title}`,
bottom: types.map((t) =>
line({
metric: c.getMetric(t.key),
series: c.getSeries(t.key),
name: t.name,
color: t.color,
unit: Unit.count,
@@ -305,13 +305,13 @@ export function createNetworkSection() {
title: `${groupName} New Address Count`,
bottom: types.flatMap((t) => [
line({
metric: addresses.new[t.key].base,
series: addresses.new[t.key].base,
name: t.name,
color: t.color,
unit: Unit.count,
}),
line({
metric: addresses.new[t.key].sum._24h,
series: addresses.new[t.key].sum._24h,
name: t.name,
color: t.color,
unit: Unit.count,
@@ -326,7 +326,7 @@ export function createNetworkSection() {
title: `${groupName} Reactivated Addresses per Block`,
bottom: types.map((t) =>
line({
metric: addresses.activity[t.key].reactivated.height,
series: addresses.activity[t.key].reactivated.height,
name: t.name,
color: t.color,
unit: Unit.count,
@@ -338,7 +338,7 @@ export function createNetworkSection() {
title: `${groupName} Reactivated Addresses (${w.name})`,
bottom: types.map((t) =>
line({
metric: addresses.activity[t.key].reactivated[w.key],
series: addresses.activity[t.key].reactivated[w.key],
name: t.name,
color: t.color,
unit: Unit.count,
@@ -371,7 +371,7 @@ export function createNetworkSection() {
title: `${groupName} ${tr.compareTitle}`,
bottom: types.map((t) =>
line({
metric: addresses.activity[t.key][tr.key].height,
series: addresses.activity[t.key][tr.key].height,
name: t.name,
color: t.color,
unit: Unit.count,
@@ -383,7 +383,7 @@ export function createNetworkSection() {
title: `${groupName} ${tr.compareTitle} (${w.name})`,
bottom: types.map((t) =>
line({
metric: addresses.activity[t.key][tr.key][w.key],
series: addresses.activity[t.key][tr.key][w.key],
name: t.name,
color: t.color,
unit: Unit.count,
@@ -423,7 +423,7 @@ export function createNetworkSection() {
title: `${groupName} Output Count`,
bottom: types.map((t) =>
line({
metric: /** @type {CountPattern<number>} */ (scripts.count[t.key])
series: /** @type {CountPattern<number>} */ (scripts.count[t.key])
.sum._24h,
name: t.name,
color: t.color,
@@ -436,7 +436,7 @@ export function createNetworkSection() {
title: `${groupName} Output Count (Total)`,
bottom: types.map((t) =>
line({
metric: /** @type {CountPattern<number>} */ (scripts.count[t.key])
series: /** @type {CountPattern<number>} */ (scripts.count[t.key])
.cumulative,
name: t.name,
color: t.color,
@@ -478,7 +478,7 @@ export function createNetworkSection() {
title: "Market Cap",
bottom: [
line({
metric: supply.marketCap.usd,
series: supply.marketCap.usd,
name: "Market Cap",
unit: Unit.usd,
}),
@@ -758,7 +758,7 @@ export function createNetworkSection() {
bottom: entries(transactions.versions).map(
([v, data], i, arr) =>
line({
metric: data.base,
series: data.base,
name: v,
color: colors.at(i, arr.length),
unit: Unit.count,
@@ -775,7 +775,7 @@ export function createNetworkSection() {
([v, data], i, arr) =>
ROLLING_WINDOWS.map((w) =>
line({
metric: data.sum[w.key],
series: data.sum[w.key],
name: `${v} ${w.name}`,
color: colors.at(i, arr.length),
unit: Unit.count,
@@ -789,7 +789,7 @@ export function createNetworkSection() {
bottom: entries(transactions.versions).map(
([v, data], i, arr) =>
line({
metric: data.sum[w.key],
series: data.sum[w.key],
name: v,
color: colors.at(i, arr.length),
unit: Unit.count,
@@ -804,7 +804,7 @@ export function createNetworkSection() {
bottom: entries(transactions.versions).map(
([v, data], i, arr) =>
line({
metric: data.cumulative,
series: data.cumulative,
name: v,
color: colors.at(i, arr.length),
unit: Unit.count,
@@ -818,12 +818,12 @@ export function createNetworkSection() {
title: "Transaction Velocity",
bottom: [
line({
metric: supply.velocity.native,
series: supply.velocity.native,
name: "BTC",
unit: Unit.ratio,
}),
line({
metric: supply.velocity.fiat,
series: supply.velocity.fiat,
name: "USD",
color: colors.usd,
unit: Unit.ratio,
@@ -845,12 +845,12 @@ export function createNetworkSection() {
title: "Block Count",
bottom: [
line({
metric: blocks.count.total.base,
series: blocks.count.total.base,
name: "base",
unit: Unit.count,
}),
line({
metric: blocks.count.target,
series: blocks.count.target,
name: "Target",
color: colors.gray,
unit: Unit.count,
@@ -868,7 +868,7 @@ export function createNetworkSection() {
title: "Block Count (Total)",
bottom: [
line({
metric: blocks.count.total.cumulative,
series: blocks.count.total.cumulative,
name: "all-time",
unit: Unit.count,
}),
@@ -884,12 +884,12 @@ export function createNetworkSection() {
title: "Block Interval",
bottom: [
dots({
metric: blocks.interval.height,
series: blocks.interval.height,
name: "base",
unit: Unit.secs,
}),
line({
metric: blocks.interval._24h,
series: blocks.interval._24h,
name: "24h avg",
color: colors.stat.avg,
unit: Unit.secs,
@@ -912,7 +912,7 @@ export function createNetworkSection() {
title: "Block Size",
bottom: [
line({
metric: blocks.size.total,
series: blocks.size.total,
name: "base",
unit: Unit.bytes,
}),
@@ -934,7 +934,7 @@ export function createNetworkSection() {
title: "Block Size (Total)",
bottom: [
line({
metric: blocks.size.cumulative,
series: blocks.size.cumulative,
name: "all-time",
unit: Unit.bytes,
}),
@@ -950,7 +950,7 @@ export function createNetworkSection() {
title: "Block Weight",
bottom: [
line({
metric: blocks.weight.raw,
series: blocks.weight.raw,
name: "base",
unit: Unit.wu,
}),
@@ -972,7 +972,7 @@ export function createNetworkSection() {
title: "Block Weight (Total)",
bottom: [
line({
metric: blocks.weight.cumulative,
series: blocks.weight.cumulative,
name: "all-time",
unit: Unit.wu,
}),
@@ -988,7 +988,7 @@ export function createNetworkSection() {
title: "Block vBytes",
bottom: [
line({
metric: blocks.vbytes.base,
series: blocks.vbytes.base,
name: "base",
unit: Unit.vb,
}),
@@ -1010,7 +1010,7 @@ export function createNetworkSection() {
title: "Block vBytes (Total)",
bottom: [
line({
metric: blocks.vbytes.cumulative,
series: blocks.vbytes.cumulative,
name: "all-time",
unit: Unit.vb,
}),
@@ -1034,7 +1034,7 @@ export function createNetworkSection() {
title: "Mining Difficulty",
bottom: [
line({
metric: blocks.difficulty.value,
series: blocks.difficulty.value,
name: "Difficulty",
unit: Unit.count,
}),
@@ -1062,7 +1062,7 @@ export function createNetworkSection() {
title: "UTXO Count",
bottom: [
line({
metric: outputs.count.unspent,
series: outputs.count.unspent,
name: "Count",
unit: Unit.count,
}),
@@ -1073,7 +1073,7 @@ export function createNetworkSection() {
title: "UTXO Count 30d Change",
bottom: [
baseline({
metric:
series:
cohorts.utxo.all.outputs.unspentCount.delta.absolute._1m,
name: "30d Change",
unit: Unit.count,
@@ -1085,13 +1085,13 @@ export function createNetworkSection() {
title: "UTXO Flow",
bottom: [
line({
metric: outputs.count.total.sum,
series: outputs.count.total.sum,
name: "Created",
color: colors.entity.output,
unit: Unit.count,
}),
line({
metric: inputs.count.sum,
series: inputs.count.sum,
name: "Spent",
color: colors.entity.input,
unit: Unit.count,
@@ -1121,19 +1121,19 @@ export function createNetworkSection() {
title: "Activity Rate",
bottom: [
dots({
metric: transactions.volume.txPerSec,
series: transactions.volume.txPerSec,
name: "TX/sec",
color: colors.entity.tx,
unit: Unit.perSec,
}),
dots({
metric: transactions.volume.inputsPerSec,
series: transactions.volume.inputsPerSec,
name: "Inputs/sec",
color: colors.entity.input,
unit: Unit.perSec,
}),
dots({
metric: transactions.volume.outputsPerSec,
series: transactions.volume.outputsPerSec,
name: "Outputs/sec",
color: colors.entity.output,
unit: Unit.perSec,
@@ -1145,8 +1145,8 @@ export function createNetworkSection() {
{
name: "Addresses",
tree: [
// Overview - global metrics for all addresses
{ name: "Overview", tree: createAddressMetricsTree("all", "") },
// Overview - global series for all addresses
{ name: "Overview", tree: createAddressSeriesTree("all", "") },
// Top-level Compare - all types
{
@@ -1159,7 +1159,7 @@ export function createNetworkSection() {
title: c.title,
bottom: addressTypes.map((t) =>
line({
metric: c.getMetric(t.key),
series: c.getSeries(t.key),
name: t.name,
color: t.color,
unit: Unit.count,
@@ -1173,14 +1173,14 @@ export function createNetworkSection() {
title: "New Address Count by Type",
bottom: addressTypes.flatMap((t) => [
line({
metric: addresses.new[t.key].base,
series: addresses.new[t.key].base,
name: t.name,
color: t.color,
unit: Unit.count,
defaultActive: t.defaultActive,
}),
line({
metric: addresses.new[t.key].sum._24h,
series: addresses.new[t.key].sum._24h,
name: t.name,
color: t.color,
unit: Unit.count,
@@ -1196,7 +1196,7 @@ export function createNetworkSection() {
title: "Reactivated Addresses per Block by Type",
bottom: addressTypes.map((t) =>
line({
metric: addresses.activity[t.key].reactivated.height,
series: addresses.activity[t.key].reactivated.height,
name: t.name,
color: t.color,
unit: Unit.count,
@@ -1209,7 +1209,7 @@ export function createNetworkSection() {
title: `Reactivated Addresses by Type (${w.name})`,
bottom: addressTypes.map((t) =>
line({
metric: addresses.activity[t.key].reactivated[w.key],
series: addresses.activity[t.key].reactivated[w.key],
name: t.name,
color: t.color,
unit: Unit.count,
@@ -1244,7 +1244,7 @@ export function createNetworkSection() {
title: tr.compareTitle,
bottom: addressTypes.map((t) =>
line({
metric: addresses.activity[t.key][tr.key].height,
series: addresses.activity[t.key][tr.key].height,
name: t.name,
color: t.color,
unit: Unit.count,
@@ -1257,7 +1257,7 @@ export function createNetworkSection() {
title: `${tr.compareTitle} (${w.name})`,
bottom: addressTypes.map((t) =>
line({
metric: addresses.activity[t.key][tr.key][w.key],
series: addresses.activity[t.key][tr.key][w.key],
name: t.name,
color: t.color,
unit: Unit.count,
@@ -1278,7 +1278,7 @@ export function createNetworkSection() {
createAddressCompare("Legacy", legacyAddresses),
...legacyAddresses.map((t) => ({
name: t.name,
tree: createAddressMetricsTree(t.key, `${t.name} `),
tree: createAddressSeriesTree(t.key, `${t.name} `),
})),
],
},
@@ -1290,7 +1290,7 @@ export function createNetworkSection() {
createAddressCompare("SegWit", segwitAddresses),
...segwitAddresses.map((t) => ({
name: t.name,
tree: createAddressMetricsTree(t.key, `${t.name} `),
tree: createAddressSeriesTree(t.key, `${t.name} `),
})),
],
},
@@ -1302,7 +1302,7 @@ export function createNetworkSection() {
createAddressCompare("Taproot", taprootAddresses),
...taprootAddresses.map((t) => ({
name: t.name,
tree: createAddressMetricsTree(t.key, `${t.name} `),
tree: createAddressSeriesTree(t.key, `${t.name} `),
})),
],
},
@@ -1325,7 +1325,7 @@ export function createNetworkSection() {
title: "Output Count by Script Type",
bottom: scriptTypes.map((t) =>
line({
metric: /** @type {CountPattern<number>} */ (
series: /** @type {CountPattern<number>} */ (
scripts.count[t.key]
).sum._24h,
name: t.name,
@@ -1340,7 +1340,7 @@ export function createNetworkSection() {
title: "Output Count by Script Type (Total)",
bottom: scriptTypes.map((t) =>
line({
metric: scripts.count[t.key].cumulative,
series: scripts.count[t.key].cumulative,
name: t.name,
color: t.color,
unit: Unit.count,
@@ -1440,25 +1440,25 @@ export function createNetworkSection() {
title: "Script Adoption",
bottom: [
line({
metric: scripts.adoption.segwit.percent,
series: scripts.adoption.segwit.percent,
name: "SegWit",
color: colors.segwit,
unit: Unit.percentage,
}),
line({
metric: scripts.adoption.segwit.ratio,
series: scripts.adoption.segwit.ratio,
name: "SegWit",
color: colors.segwit,
unit: Unit.ratio,
}),
line({
metric: scripts.adoption.taproot.percent,
series: scripts.adoption.taproot.percent,
name: "Taproot",
color: taprootAddresses[1].color,
unit: Unit.percentage,
}),
line({
metric: scripts.adoption.taproot.ratio,
series: scripts.adoption.taproot.ratio,
name: "Taproot",
color: taprootAddresses[1].color,
unit: Unit.ratio,
@@ -1470,12 +1470,12 @@ export function createNetworkSection() {
title: "SegWit Adoption",
bottom: [
line({
metric: scripts.adoption.segwit.percent,
series: scripts.adoption.segwit.percent,
name: "Adoption",
unit: Unit.percentage,
}),
line({
metric: scripts.adoption.segwit.ratio,
series: scripts.adoption.segwit.ratio,
name: "Adoption",
unit: Unit.ratio,
}),
@@ -1486,12 +1486,12 @@ export function createNetworkSection() {
title: "Taproot Adoption",
bottom: [
line({
metric: scripts.adoption.taproot.percent,
series: scripts.adoption.taproot.percent,
name: "Adoption",
unit: Unit.percentage,
}),
line({
metric: scripts.adoption.taproot.ratio,
series: scripts.adoption.taproot.ratio,
name: "Adoption",
unit: Unit.ratio,
}),
+80 -80
View File
@@ -18,11 +18,11 @@ export const ROLLING_WINDOWS = [
];
/**
* Extract a metric from each rolling window via a mapping function
* Extract a series 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 }}
* @param {(v: T) => AnySeriesPattern} extract
* @returns {{ _24h: AnySeriesPattern, _1w: AnySeriesPattern, _1m: AnySeriesPattern, _1y: AnySeriesPattern }}
*/
export function mapWindows(windows, extract) {
return {
@@ -40,7 +40,7 @@ export function mapWindows(windows, extract) {
/**
* Create a price series for the top pane (auto-expands to USD + sats versions)
* @param {Object} args
* @param {AnyPricePattern} args.metric - Price pattern with usd and sats
* @param {AnyPricePattern} args.series - Price pattern with usd and sats
* @param {string} args.name
* @param {string} [args.key]
* @param {LineStyle} [args.style]
@@ -50,7 +50,7 @@ export function mapWindows(windows, extract) {
* @returns {FetchedPriceSeriesBlueprint}
*/
export function price({
metric,
series,
name,
key,
style,
@@ -59,7 +59,7 @@ export function price({
options,
}) {
return {
metric,
series,
title: name,
key,
color,
@@ -86,49 +86,49 @@ function percentileSeries(pattern, unit, title) {
const { stat } = colors;
return [
dots({
metric: pattern.max,
series: pattern.max,
name: `${title} max`.trim(),
color: stat.max,
unit,
defaultActive: false,
}),
dots({
metric: pattern.min,
series: pattern.min,
name: `${title} min`.trim(),
color: stat.min,
unit,
defaultActive: false,
}),
dots({
metric: pattern.median,
series: pattern.median,
name: `${title} median`.trim(),
color: stat.median,
unit,
defaultActive: false,
}),
dots({
metric: pattern.pct75,
series: pattern.pct75,
name: `${title} pct75`.trim(),
color: stat.pct75,
unit,
defaultActive: false,
}),
dots({
metric: pattern.pct25,
series: pattern.pct25,
name: `${title} pct25`.trim(),
color: stat.pct25,
unit,
defaultActive: false,
}),
dots({
metric: pattern.pct90,
series: pattern.pct90,
name: `${title} pct90`.trim(),
color: stat.pct90,
unit,
defaultActive: false,
}),
dots({
metric: pattern.pct10,
series: pattern.pct10,
name: `${title} pct10`.trim(),
color: stat.pct10,
unit,
@@ -140,7 +140,7 @@ function percentileSeries(pattern, unit, title) {
/**
* Create a Line series
* @param {Object} args
* @param {AnyMetricPattern} args.metric
* @param {AnySeriesPattern} args.series
* @param {string} args.name
* @param {Unit} args.unit
* @param {string} [args.key] - Optional key for persistence (derived from name if not provided)
@@ -151,7 +151,7 @@ function percentileSeries(pattern, unit, title) {
* @returns {FetchedLineSeriesBlueprint}
*/
export function line({
metric,
series,
name,
key,
style,
@@ -161,7 +161,7 @@ export function line({
options,
}) {
return {
metric,
series,
title: name,
key,
color,
@@ -195,7 +195,7 @@ export function sparseDotted(args) {
/**
* Create a Dots series (line with only point markers visible)
* @param {Object} args
* @param {AnyMetricPattern} args.metric
* @param {AnySeriesPattern} args.series
* @param {string} args.name
* @param {Unit} args.unit
* @param {string} [args.key] - Optional key for persistence (derived from name if not provided)
@@ -205,7 +205,7 @@ export function sparseDotted(args) {
* @returns {FetchedDotsSeriesBlueprint}
*/
export function dots({
metric,
series,
name,
key,
color,
@@ -215,7 +215,7 @@ export function dots({
}) {
return {
type: /** @type {const} */ ("Dots"),
metric,
series,
title: name,
key,
color,
@@ -228,7 +228,7 @@ export function dots({
/**
* Create a Candlestick series
* @param {Object} args
* @param {AnyMetricPattern} args.metric
* @param {AnySeriesPattern} args.series
* @param {string} args.name
* @param {Unit} args.unit
* @param {string} [args.key] - Optional key for persistence (derived from name if not provided)
@@ -238,7 +238,7 @@ export function dots({
* @returns {FetchedCandlestickSeriesBlueprint}
*/
export function candlestick({
metric,
series,
name,
key,
defaultActive,
@@ -247,7 +247,7 @@ export function candlestick({
}) {
return {
type: /** @type {const} */ ("Candlestick"),
metric,
series,
title: name,
key,
unit,
@@ -259,7 +259,7 @@ export function candlestick({
/**
* Create a Baseline series
* @param {Object} args
* @param {AnyMetricPattern} args.metric
* @param {AnySeriesPattern} args.series
* @param {string} args.name
* @param {Unit} args.unit
* @param {string} [args.key] - Optional key for persistence (derived from name if not provided)
@@ -271,7 +271,7 @@ export function candlestick({
* @returns {FetchedBaselineSeriesBlueprint}
*/
export function baseline({
metric,
series,
name,
key,
color,
@@ -284,7 +284,7 @@ export function baseline({
const isTuple = Array.isArray(color);
return {
type: /** @type {const} */ ("Baseline"),
metric,
series,
title: name,
key,
color: isTuple ? undefined : color,
@@ -313,7 +313,7 @@ export function dottedBaseline(args) {
/**
* Baseline series rendered as dots (points only, no line)
* @param {Object} args
* @param {AnyMetricPattern} args.metric
* @param {AnySeriesPattern} args.series
* @param {string} args.name
* @param {Unit} args.unit
* @param {string} [args.key]
@@ -324,7 +324,7 @@ export function dottedBaseline(args) {
* @returns {FetchedDotsBaselineSeriesBlueprint}
*/
export function dotsBaseline({
metric,
series,
name,
key,
color,
@@ -336,7 +336,7 @@ export function dotsBaseline({
const isTuple = Array.isArray(color);
return {
type: /** @type {const} */ ("DotsBaseline"),
metric,
series,
title: name,
key,
color: isTuple ? undefined : color,
@@ -355,7 +355,7 @@ export function dotsBaseline({
/**
* Create a Histogram series
* @param {Object} args
* @param {AnyMetricPattern} args.metric
* @param {AnySeriesPattern} args.series
* @param {string} args.name
* @param {Unit} args.unit
* @param {string} [args.key] - Optional key for persistence (derived from name if not provided)
@@ -365,7 +365,7 @@ export function dotsBaseline({
* @returns {FetchedHistogramSeriesBlueprint}
*/
export function histogram({
metric,
series,
name,
key,
color,
@@ -375,7 +375,7 @@ export function histogram({
}) {
return {
type: /** @type {const} */ ("Histogram"),
metric,
series,
title: name,
key,
color,
@@ -388,7 +388,7 @@ export function histogram({
/**
* Create series from an AverageHeightMaxMedianMinP10P25P75P90Pattern (height + rolling stats)
* @param {Object} args
* @param {{ height: AnyMetricPattern } & Record<string, any>} args.pattern - Pattern with .height and rolling stats (p10/p25/p75/p90 as _1y24h30d7dPattern)
* @param {{ height: AnySeriesPattern } & Record<string, any>} args.pattern - Pattern with .height and rolling stats (p10/p25/p75/p90 as _1y24h30d7dPattern)
* @param {string} args.window - Rolling window key (e.g., '_24h', '_7d', '_30d', '_1y')
* @param {Unit} args.unit
* @param {string} [args.title]
@@ -408,13 +408,13 @@ export function fromBaseStatsPattern({
const stats = statsAtWindow(pattern, window);
return [
dots({
metric: pattern.height,
series: pattern.height,
name: title || "base",
color: baseColor,
unit,
}),
dots({
metric: stats.average,
series: stats.average,
name: `${title} avg`.trim(),
color: stat.avg,
unit,
@@ -425,10 +425,10 @@ export function fromBaseStatsPattern({
}
/**
* Create series from a flat stats pattern (average + pct percentiles as single metrics)
* Create series from a flat stats pattern (average + pct percentiles as single series)
* Use statsAtWindow() to extract from patterns with _1y24h30d7dPattern stats
* @param {Object} args
* @param {{ average: AnyMetricPattern, median: AnyMetricPattern, max: AnyMetricPattern, min: AnyMetricPattern, pct75: AnyMetricPattern, pct25: AnyMetricPattern, pct90: AnyMetricPattern, pct10: AnyMetricPattern }} args.pattern
* @param {{ average: AnySeriesPattern, median: AnySeriesPattern, max: AnySeriesPattern, min: AnySeriesPattern, pct75: AnySeriesPattern, pct25: AnySeriesPattern, pct90: AnySeriesPattern, pct10: AnySeriesPattern }} args.pattern
* @param {Unit} args.unit
* @param {string} [args.title]
* @returns {AnyFetchedSeriesBlueprint[]}
@@ -437,7 +437,7 @@ export function fromStatsPattern({ pattern, unit, title = "" }) {
return [
{
type: "Dots",
metric: pattern.average,
series: pattern.average,
title: `${title} avg`.trim(),
unit,
},
@@ -466,10 +466,10 @@ export function statsAtWindow(pattern, window) {
/**
* Create a Rolling folder tree from a _1m1w1y24hPattern (4 rolling windows)
* @param {Object} args
* @param {{ _24h: AnyMetricPattern, _1w: AnyMetricPattern, _1m: AnyMetricPattern, _1y: AnyMetricPattern }} args.windows
* @param {{ _24h: AnySeriesPattern, _1w: AnySeriesPattern, _1m: AnySeriesPattern, _1y: AnySeriesPattern }} args.windows
* @param {string} args.title
* @param {Unit} args.unit
* @param {(args: {metric: AnyMetricPattern, name: string, color: Color, unit: Unit}) => AnyFetchedSeriesBlueprint} [args.series]
* @param {(args: {series: AnySeriesPattern, name: string, color: Color, unit: Unit}) => AnyFetchedSeriesBlueprint} [args.series]
* @returns {PartialOptionsGroup}
*/
export function rollingWindowsTree({ windows, title, unit, series = line }) {
@@ -481,7 +481,7 @@ export function rollingWindowsTree({ windows, title, unit, series = line }) {
title: `${title} Rolling`,
bottom: ROLLING_WINDOWS.map((w) =>
series({
metric: windows[w.key],
series: windows[w.key],
name: w.name,
color: w.color,
unit,
@@ -493,7 +493,7 @@ export function rollingWindowsTree({ windows, title, unit, series = line }) {
title: `${title} ${w.name}`,
bottom: [
series({
metric: windows[w.key],
series: windows[w.key],
name: w.name,
color: w.color,
unit,
@@ -508,7 +508,7 @@ export function rollingWindowsTree({ windows, title, unit, series = line }) {
* Create a Distribution folder tree with stats at each rolling window (24h/7d/30d/1y)
* @param {Object} args
* @param {Record<string, any>} 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 {AnySeriesPattern} [args.base] - Optional base series to show as dots on each chart
* @param {string} args.title
* @param {Unit} args.unit
* @returns {PartialOptionsGroup}
@@ -520,7 +520,7 @@ export function distributionWindowsTree({ pattern, base, title, unit }) {
name: w.name,
title: `${title} Distribution (${w.name})`,
bottom: [
...(base ? [line({ metric: base, name: "base", unit })] : []),
...(base ? [line({ series: base, name: "base", unit })] : []),
...fromStatsPattern({
pattern: statsAtWindow(pattern, w.key),
unit,
@@ -579,19 +579,19 @@ export const distributionBtcSatsUsd = (slot) => [
export function fromSupplyPattern({ pattern, title, color }) {
return [
{
metric: pattern.btc,
series: pattern.btc,
title,
color,
unit: Unit.btc,
},
{
metric: pattern.sats,
series: pattern.sats,
title,
color,
unit: Unit.sats,
},
{
metric: pattern.usd,
series: pattern.usd,
title,
color,
unit: Unit.usd,
@@ -606,7 +606,7 @@ export function fromSupplyPattern({ pattern, title, color }) {
/**
* Create percent + ratio series from a BpsPercentRatioPattern
* @param {Object} args
* @param {{ percent: AnyMetricPattern, ratio: AnyMetricPattern }} args.pattern
* @param {{ percent: AnySeriesPattern, ratio: AnySeriesPattern }} args.pattern
* @param {string} args.name
* @param {Color} [args.color]
* @param {boolean} [args.defaultActive]
@@ -614,15 +614,15 @@ export function fromSupplyPattern({ pattern, title, color }) {
*/
export function percentRatio({ pattern, name, color, defaultActive }) {
return [
line({ metric: pattern.percent, name, color, defaultActive, unit: Unit.percentage }),
line({ metric: pattern.ratio, name, color, defaultActive, unit: Unit.ratio }),
line({ series: pattern.percent, name, color, defaultActive, unit: Unit.percentage }),
line({ series: pattern.ratio, name, color, defaultActive, unit: Unit.ratio }),
];
}
/**
* Create percent + ratio dots series from a BpsPercentRatioPattern
* @param {Object} args
* @param {{ percent: AnyMetricPattern, ratio: AnyMetricPattern }} args.pattern
* @param {{ percent: AnySeriesPattern, ratio: AnySeriesPattern }} args.pattern
* @param {string} args.name
* @param {Color} [args.color]
* @param {boolean} [args.defaultActive]
@@ -630,15 +630,15 @@ export function percentRatio({ pattern, name, color, defaultActive }) {
*/
export function percentRatioDots({ pattern, name, color, defaultActive }) {
return [
dots({ metric: pattern.percent, name, color, defaultActive, unit: Unit.percentage }),
dots({ metric: pattern.ratio, name, color, defaultActive, unit: Unit.ratio }),
dots({ series: pattern.percent, name, color, defaultActive, unit: Unit.percentage }),
dots({ series: pattern.ratio, name, color, defaultActive, unit: Unit.ratio }),
];
}
/**
* Create percent + ratio baseline series from a BpsPercentRatioPattern
* @param {Object} args
* @param {{ percent: AnyMetricPattern, ratio: AnyMetricPattern }} args.pattern
* @param {{ percent: AnySeriesPattern, ratio: AnySeriesPattern }} args.pattern
* @param {string} args.name
* @param {Color | [Color, Color]} [args.color]
* @param {boolean} [args.defaultActive]
@@ -646,17 +646,17 @@ export function percentRatioDots({ pattern, name, color, defaultActive }) {
*/
export function percentRatioBaseline({ pattern, name, color, defaultActive }) {
return [
baseline({ metric: pattern.percent, name, color, defaultActive, unit: Unit.percentage }),
baseline({ metric: pattern.ratio, name, color, defaultActive, unit: Unit.ratio }),
baseline({ series: pattern.percent, name, color, defaultActive, unit: Unit.percentage }),
baseline({ series: pattern.ratio, name, color, defaultActive, unit: Unit.ratio }),
];
}
/**
* Create a Rolling folder tree where each window is a BpsPercentRatioPattern (percent + ratio)
* @param {Object} args
* @param {{ _24h: { percent: AnyMetricPattern, ratio: AnyMetricPattern }, _1w: { percent: AnyMetricPattern, ratio: AnyMetricPattern }, _1m: { percent: AnyMetricPattern, ratio: AnyMetricPattern }, _1y: { percent: AnyMetricPattern, ratio: AnyMetricPattern } }} args.windows
* @param {{ _24h: { percent: AnySeriesPattern, ratio: AnySeriesPattern }, _1w: { percent: AnySeriesPattern, ratio: AnySeriesPattern }, _1m: { percent: AnySeriesPattern, ratio: AnySeriesPattern }, _1y: { percent: AnySeriesPattern, ratio: AnySeriesPattern } }} args.windows
* @param {string} args.title
* @param {(args: {pattern: { percent: AnyMetricPattern, ratio: AnyMetricPattern }, name: string, color: Color}) => AnyFetchedSeriesBlueprint[]} [args.series]
* @param {(args: {pattern: { percent: AnySeriesPattern, ratio: AnySeriesPattern }, name: string, color: Color}) => AnyFetchedSeriesBlueprint[]} [args.series]
* @returns {PartialOptionsGroup}
*/
export function rollingPercentRatioTree({ windows, title, series = percentRatio }) {
@@ -693,51 +693,51 @@ export function rollingPercentRatioTree({ windows, title, series = percentRatio
function distributionSeries(pattern, unit) {
const { stat } = colors;
return [
dots({ metric: pattern.average, name: "avg", color: stat.avg, unit }),
dots({ series: pattern.average, name: "avg", color: stat.avg, unit }),
dots({
metric: pattern.median,
series: pattern.median,
name: "median",
color: stat.median,
unit,
defaultActive: false,
}),
dots({
metric: pattern.max,
series: pattern.max,
name: "max",
color: stat.max,
unit,
defaultActive: false,
}),
dots({
metric: pattern.min,
series: pattern.min,
name: "min",
color: stat.min,
unit,
defaultActive: false,
}),
dots({
metric: pattern.pct75,
series: pattern.pct75,
name: "pct75",
color: stat.pct75,
unit,
defaultActive: false,
}),
dots({
metric: pattern.pct25,
series: pattern.pct25,
name: "pct25",
color: stat.pct25,
unit,
defaultActive: false,
}),
dots({
metric: pattern.pct90,
series: pattern.pct90,
name: "pct90",
color: stat.pct90,
unit,
defaultActive: false,
}),
dots({
metric: pattern.pct10,
series: pattern.pct10,
name: "pct10",
color: stat.pct10,
unit,
@@ -747,32 +747,32 @@ function distributionSeries(pattern, unit) {
}
/**
* Create btc/sats/usd series from metrics
* Create btc/sats/usd series from patterns
* @param {Object} args
* @param {{ btc: AnyMetricPattern, sats: AnyMetricPattern, usd: AnyMetricPattern }} args.metrics
* @param {{ btc: AnySeriesPattern, sats: AnySeriesPattern, usd: AnySeriesPattern }} args.patterns
* @param {string} args.name
* @param {Color} [args.color]
* @param {boolean} [args.defaultActive]
* @returns {AnyFetchedSeriesBlueprint[]}
*/
function btcSatsUsdSeries({ metrics, name, color, defaultActive }) {
function btcSatsUsdSeries({ patterns, name, color, defaultActive }) {
return [
{
metric: metrics.btc,
series: patterns.btc,
title: name,
color,
unit: Unit.btc,
defaultActive,
},
{
metric: metrics.sats,
series: patterns.sats,
title: name,
color,
unit: Unit.sats,
defaultActive,
},
{
metric: metrics.usd,
series: patterns.usd,
title: name,
color,
unit: Unit.usd,
@@ -804,14 +804,14 @@ export function chartsFromFull({
{
name: "Sum",
title,
bottom: [{ metric: pattern.base, title: "base", unit }],
bottom: [{ series: pattern.base, title: "base", unit }],
},
rollingWindowsTree({ windows: pattern.sum, title, unit }),
distributionWindowsTree({ pattern, title: distTitle, unit }),
{
name: "Cumulative",
title: `${title} (Total)`,
bottom: [{ metric: pattern.cumulative, title: "all-time", unit }],
bottom: [{ series: pattern.cumulative, title: "all-time", unit }],
},
];
}
@@ -850,7 +850,7 @@ export function chartsFromSum({
{
name: "Sum",
title,
bottom: [{ metric: pattern.sum, title: "sum", color: stat.sum, unit }],
bottom: [{ series: pattern.sum, title: "sum", color: stat.sum, unit }],
},
rollingWindowsTree({ windows: pattern.rolling.sum, title, unit }),
distributionWindowsTree({ pattern: pattern.rolling, title: distTitle, unit }),
@@ -862,7 +862,7 @@ export function chartsFromSum({
{
name: "Cumulative",
title: `${title} (Total)`,
bottom: [{ metric: pattern.cumulative, title: "all-time", unit }],
bottom: [{ series: pattern.cumulative, title: "all-time", unit }],
},
];
}
@@ -915,13 +915,13 @@ export function chartsFromCount({ pattern, title, unit, color }) {
{
name: "Base",
title,
bottom: [{ metric: pattern.base, title: "base", color, unit }],
bottom: [{ series: pattern.base, title: "base", color, unit }],
},
rollingWindowsTree({ windows: pattern.sum, title, unit }),
{
name: "Cumulative",
title: `${title} (Total)`,
bottom: [{ metric: pattern.cumulative, title: "all-time", color, unit }],
bottom: [{ series: pattern.cumulative, title: "all-time", color, unit }],
},
];
}
@@ -939,9 +939,9 @@ export function chartsFromValueFull({ pattern, title }) {
name: "Sum",
title,
bottom: [
...btcSatsUsdSeries({ metrics: pattern.base, name: "sum" }),
...btcSatsUsdSeries({ patterns: pattern.base, name: "sum" }),
...btcSatsUsdSeries({
metrics: pattern.sum._24h,
patterns: pattern.sum._24h,
name: "24h sum",
defaultActive: false,
}),
@@ -951,7 +951,7 @@ export function chartsFromValueFull({ pattern, title }) {
name: "Cumulative",
title: `${title} (Total)`,
bottom: btcSatsUsdSeries({
metrics: pattern.cumulative,
patterns: pattern.cumulative,
name: "all-time",
}),
},
+46 -46
View File
@@ -72,10 +72,10 @@ export function flatMapCohortsWithAll(list, all, fn) {
/**
* Create a title formatter for chart titles
* @param {string} [cohortTitle]
* @returns {(metric: string) => string}
* @returns {(name: string) => string}
*/
export const formatCohortTitle = (cohortTitle) => (metric) =>
cohortTitle ? `${metric}: ${cohortTitle}` : metric;
export const formatCohortTitle = (cohortTitle) => (name) =>
cohortTitle ? `${name}: ${cohortTitle}` : name;
/**
* Create sats/btc/usd line series from a pattern with .sats/.btc/.usd
@@ -90,7 +90,7 @@ export const formatCohortTitle = (cohortTitle) => (metric) =>
export function satsBtcUsd({ pattern, name, color, defaultActive, style }) {
return [
line({
metric: pattern.btc,
series: pattern.btc,
name,
color,
unit: Unit.btc,
@@ -98,7 +98,7 @@ export function satsBtcUsd({ pattern, name, color, defaultActive, style }) {
style,
}),
line({
metric: pattern.sats,
series: pattern.sats,
name,
color,
unit: Unit.sats,
@@ -106,7 +106,7 @@ export function satsBtcUsd({ pattern, name, color, defaultActive, style }) {
style,
}),
line({
metric: pattern.usd,
series: pattern.usd,
name,
color,
unit: Unit.usd,
@@ -119,7 +119,7 @@ export function satsBtcUsd({ pattern, name, color, defaultActive, style }) {
/**
* Create sats/btc/usd baseline series from a value pattern
* @param {Object} args
* @param {{ btc: AnyMetricPattern, sats: AnyMetricPattern, usd: AnyMetricPattern }} args.pattern
* @param {{ btc: AnySeriesPattern, sats: AnySeriesPattern, usd: AnySeriesPattern }} args.pattern
* @param {string} args.name
* @param {Color} [args.color]
* @param {boolean} [args.defaultActive]
@@ -128,21 +128,21 @@ export function satsBtcUsd({ pattern, name, color, defaultActive, style }) {
export function satsBtcUsdBaseline({ pattern, name, color, defaultActive }) {
return [
baseline({
metric: pattern.btc,
series: pattern.btc,
name,
color,
unit: Unit.btc,
defaultActive,
}),
baseline({
metric: pattern.sats,
series: pattern.sats,
name,
color,
unit: Unit.sats,
defaultActive,
}),
baseline({
metric: pattern.usd,
series: pattern.usd,
name,
color,
unit: Unit.usd,
@@ -271,7 +271,7 @@ export function satsBtcUsdFullTree({ pattern, name, title, color }) {
/**
* Create Price + Ratio charts from a simple price pattern (BpsCentsRatioSatsUsdPattern)
* @param {Object} args
* @param {AnyPricePattern & { ratio: AnyMetricPattern }} args.pattern
* @param {AnyPricePattern & { ratio: AnySeriesPattern }} args.pattern
* @param {string} args.title
* @param {string} args.legend
* @param {Color} [args.color]
@@ -282,15 +282,15 @@ export function simplePriceRatioTree({ pattern, title, legend, color }) {
{
name: "Price",
title,
top: [price({ metric: pattern, name: legend, color })],
top: [price({ series: pattern, name: legend, color })],
},
{
name: "Ratio",
title: `${title} Ratio`,
top: [price({ metric: pattern, name: legend, color })],
top: [price({ series: pattern, name: legend, color })],
bottom: [
baseline({
metric: pattern.ratio,
series: pattern.ratio,
name: "Ratio",
unit: Unit.ratio,
base: 1,
@@ -339,11 +339,11 @@ export function priceRatioPercentilesTree({
name: "Price",
title,
top: [
price({ metric: pattern, name: legend, color }),
price({ series: pattern, name: legend, color }),
...(priceReferences ?? []),
...pctUsd.map(({ name, prop, color }) =>
price({
metric: prop,
series: prop,
name,
color,
defaultActive: false,
@@ -356,10 +356,10 @@ export function priceRatioPercentilesTree({
name: "Ratio",
title: `${title} Ratio`,
top: [
price({ metric: pattern, name: legend, color }),
price({ series: pattern, name: legend, color }),
...pctUsd.map(({ name, prop, color }) =>
price({
metric: prop,
series: prop,
name,
color,
defaultActive: false,
@@ -369,14 +369,14 @@ export function priceRatioPercentilesTree({
],
bottom: [
baseline({
metric: pattern.ratio,
series: pattern.ratio,
name: "Ratio",
unit: Unit.ratio,
base: 1,
}),
...pctRatio.map(({ name, prop, color }) =>
line({
metric: prop,
series: prop,
name,
color,
defaultActive: false,
@@ -507,7 +507,7 @@ export function sdBandsUsd(sd) {
/**
* Build SD band mappings (ratio) from an SD pattern
* @param {Ratio1ySdPattern} sd
* @param {AnyMetricPattern} smaRatio
* @param {AnySeriesPattern} smaRatio
*/
export function sdBandsRatio(sd, smaRatio) {
return /** @type {const} */ ([
@@ -533,19 +533,19 @@ export function sdBandsRatio(sd, smaRatio) {
*/
export function ratioSmas(ratio) {
return [
{ 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 },
{ name: "1w SMA", series: ratio.sma._1w.ratio },
{ name: "1m SMA", series: ratio.sma._1m.ratio },
{ name: "1y SMA", series: ratio.sma._1y.ratio },
{ name: "2y SMA", series: ratio.sma._2y.ratio },
{ name: "4y SMA", series: ratio.sma._4y.ratio },
{ name: "All SMA", series: ratio.sma.all.ratio, color: colors.time.all },
].map((s, i, arr) => ({ color: colors.at(i, arr.length), ...s }));
}
/**
* Create ratio chart from ActivePriceRatioPattern
* @param {Object} args
* @param {(metric: string) => string} args.title
* @param {(name: string) => string} args.title
* @param {AnyPricePattern} args.pricePattern - The price pattern to show in top pane
* @param {AnyRatioPattern} args.ratio - The ratio pattern
* @param {Color} args.color
@@ -557,10 +557,10 @@ export function createRatioChart({ title, pricePattern, ratio, color, name }) {
name: name ?? "ratio",
title: title(name ?? "Ratio"),
top: [
price({ metric: pricePattern, name: "Price", color }),
price({ series: pricePattern, name: "Price", color }),
...percentileUsdMap(ratio).map(({ name, prop, color }) =>
price({
metric: prop,
series: prop,
name,
color,
defaultActive: false,
@@ -570,17 +570,17 @@ export function createRatioChart({ title, pricePattern, ratio, color, name }) {
],
bottom: [
baseline({
metric: ratio.ratio,
series: ratio.ratio,
name: "Ratio",
unit: Unit.ratio,
base: 1,
}),
...ratioSmas(ratio).map(({ name, metric, color }) =>
line({ metric, name, color, unit: Unit.ratio, defaultActive: false }),
...ratioSmas(ratio).map(({ name, series, color }) =>
line({ series, name, color, unit: Unit.ratio, defaultActive: false }),
),
...percentileMap(ratio).map(({ name, prop, color }) =>
line({
metric: prop,
series: prop,
name,
color,
defaultActive: false,
@@ -595,7 +595,7 @@ export function createRatioChart({ title, pricePattern, ratio, color, name }) {
/**
* Create ZScores folder from ActivePriceRatioPattern
* @param {Object} args
* @param {(suffix: string) => string} args.formatTitle - Function that takes metric suffix and returns full title
* @param {(suffix: string) => string} args.formatTitle - Function that takes series suffix and returns full title
* @param {string} args.legend
* @param {AnyPricePattern} args.pricePattern - The price pattern to show in top pane
* @param {AnyRatioPattern} args.ratio - The ratio pattern
@@ -625,10 +625,10 @@ export function createZScoresFolder({
name: "Compare",
title: formatTitle("Z-Scores"),
top: [
price({ metric: pricePattern, name: legend, color }),
price({ series: pricePattern, name: legend, color }),
...zscorePeriods.map((p) =>
price({
metric: p.sd._0sd,
series: p.sd._0sd,
name: `${p.name} 0σ`,
color: p.color,
defaultActive: false,
@@ -638,7 +638,7 @@ export function createZScoresFolder({
bottom: [
...zscorePeriods.reverse().map((p) =>
line({
metric: p.sd.zscore,
series: p.sd.zscore,
name: p.name,
color: p.color,
unit: Unit.sd,
@@ -653,7 +653,7 @@ export function createZScoresFolder({
},
...sdPats.map(({ nameAddon, titleAddon, sd, smaRatio }) => {
const prefix = titleAddon ? `${titleAddon} ` : "";
const topPrice = price({ metric: pricePattern, name: legend, color });
const topPrice = price({ series: pricePattern, name: legend, color });
return {
name: nameAddon,
tree: [
@@ -665,7 +665,7 @@ export function createZScoresFolder({
...sdBandsUsd(sd).map(
({ name: bandName, prop, color: bandColor }) =>
price({
metric: prop,
series: prop,
name: bandName,
color: bandColor,
defaultActive: false,
@@ -674,7 +674,7 @@ export function createZScoresFolder({
],
bottom: [
baseline({
metric: sd.zscore,
series: sd.zscore,
name: "Z-Score",
unit: Unit.sd,
}),
@@ -694,7 +694,7 @@ export function createZScoresFolder({
top: [topPrice],
bottom: [
baseline({
metric: ratio.ratio,
series: ratio.ratio,
name: "Ratio",
unit: Unit.ratio,
base: 1,
@@ -702,7 +702,7 @@ export function createZScoresFolder({
...sdBandsRatio(sd, smaRatio).map(
({ name: bandName, prop, color: bandColor }) =>
line({
metric: prop,
series: prop,
name: bandName,
color: bandColor,
unit: Unit.ratio,
@@ -717,7 +717,7 @@ export function createZScoresFolder({
top: [topPrice],
bottom: [
line({
metric: sd.sd,
series: sd.sd,
name: "Volatility",
color: colors.gray,
unit: Unit.percentage,
@@ -733,7 +733,7 @@ export function createZScoresFolder({
/**
* Create price + ratio + z-scores charts - flat array
* Unified helper for averages, distribution, and other price-based metrics
* Unified helper for averages, distribution, and other price-based series
* @param {Object} args
* @param {string} args.context - Context string for ratio/z-scores titles (e.g., "1 Week SMA", "STH")
* @param {string} args.legend - Legend name for the price series
@@ -761,7 +761,7 @@ export function createPriceRatioCharts({
name: "Price",
title: priceTitle ?? context,
top: [
price({ metric: pricePattern, name: legend, color }),
price({ series: pricePattern, name: legend, color }),
...(priceReferences ?? []),
],
},
+7 -7
View File
@@ -44,7 +44,7 @@
*
* @typedef {Object} PriceSeriesBlueprintSpecific
* @property {"Price"} type
* @property {AnyMetricPattern} ohlcMetric - OHLC metric for candlestick (>= 1h indexes)
* @property {AnySeriesPattern} ohlcSeries - OHLC series for candlestick (>= 1h indexes)
* @property {[Color, Color]} [colors]
* @property {CandlestickSeriesPartialOptions} [options]
* @typedef {BaseSeriesBlueprint & PriceSeriesBlueprintSpecific} PriceSeriesBlueprint
@@ -53,7 +53,7 @@
*
* @typedef {AnySeriesBlueprint["type"]} SeriesType
*
* @typedef {{ metric: AnyMetricPattern, unit?: Unit }} FetchedAnySeriesOptions
* @typedef {{ series: AnySeriesPattern, unit?: Unit }} FetchedAnySeriesOptions
*
* @typedef {BaselineSeriesBlueprint & FetchedAnySeriesOptions} FetchedBaselineSeriesBlueprint
* @typedef {CandlestickSeriesBlueprint & FetchedAnySeriesOptions} FetchedCandlestickSeriesBlueprint
@@ -63,14 +63,14 @@
* @typedef {DotsBaselineSeriesBlueprint & FetchedAnySeriesOptions} FetchedDotsBaselineSeriesBlueprint
* @typedef {AnySeriesBlueprint & FetchedAnySeriesOptions} AnyFetchedSeriesBlueprint
*
* Any pattern with usd and sats sub-metrics (auto-expands to USD + sats)
* @typedef {{ usd: AnyMetricPattern, sats: AnyMetricPattern }} AnyPricePattern
* Any pattern with usd and sats sub-series (auto-expands to USD + sats)
* @typedef {{ usd: AnySeriesPattern, sats: AnySeriesPattern }} AnyPricePattern
*
* Any pattern with sats, btc, and usd sub-metrics (value patterns like stack)
* @typedef {{ sats: AnyMetricPattern, btc: AnyMetricPattern, usd: AnyMetricPattern }} AnyValuePattern
* Any pattern with sats, btc, and usd sub-series (value patterns like stack)
* @typedef {{ sats: AnySeriesPattern, btc: AnySeriesPattern, usd: AnySeriesPattern }} AnyValuePattern
*
* Top pane price series - requires a price pattern with usd/sats, auto-expands to USD + sats
* @typedef {{ metric: AnyPricePattern }} FetchedPriceSeriesOptions
* @typedef {{ series: AnyPricePattern }} FetchedPriceSeriesOptions
* @typedef {LineSeriesBlueprint & FetchedPriceSeriesOptions} FetchedPriceSeriesBlueprint
*
* @typedef {Object} PartialOption
+30 -30
View File
@@ -2,8 +2,8 @@ import { localhost } from "../utils/env.js";
import { INDEX_LABEL } from "../utils/serde.js";
/**
* Check if a metric pattern has at least one chartable index
* @param {AnyMetricPattern} node
* Check if a series pattern has at least one chartable index
* @param {AnySeriesPattern} node
* @returns {boolean}
*/
function hasChartableIndex(node) {
@@ -12,16 +12,16 @@ function hasChartableIndex(node) {
}
/**
* Walk a metrics tree and collect all chartable metric patterns
* Walk a series tree and collect all chartable series patterns
* @param {TreeNode | null | undefined} node
* @param {Map<AnyMetricPattern, string[]>} map
* @param {Map<AnySeriesPattern, string[]>} map
* @param {string[]} path
*/
function walkMetrics(node, map, path) {
function walkSeries(node, map, path) {
if (node && "by" in node) {
const metricNode = /** @type {AnyMetricPattern} */ (node);
if (!hasChartableIndex(metricNode)) return;
map.set(metricNode, path);
const seriesNode = /** @type {AnySeriesPattern} */ (node);
if (!hasChartableIndex(seriesNode)) return;
map.set(seriesNode, path);
} else if (node && typeof node === "object") {
for (const [key, value] of Object.entries(node)) {
const kn = key.toLowerCase();
@@ -56,7 +56,7 @@ function walkMetrics(node, map, path) {
kn.endsWith("indexes")
)
continue;
walkMetrics(/** @type {TreeNode | null | undefined} */ (value), map, [
walkSeries(/** @type {TreeNode | null | undefined} */ (value), map, [
...path,
key,
]);
@@ -65,9 +65,9 @@ function walkMetrics(node, map, path) {
}
/**
* Walk partial options tree and delete referenced metrics from the map
* Walk partial options tree and delete referenced series from the map
* @param {PartialOptionsTree} options
* @param {Map<AnyMetricPattern, string[]>} map
* @param {Map<AnySeriesPattern, string[]>} map
*/
function walkOptions(options, map) {
for (const node of options) {
@@ -82,40 +82,40 @@ function walkOptions(options, map) {
}
/**
* @param {Map<AnyMetricPattern, string[]>} map
* @param {Map<AnySeriesPattern, string[]>} map
* @param {(AnyFetchedSeriesBlueprint | FetchedPriceSeriesBlueprint)[]} [arr]
*/
function markUsedBlueprints(map, arr) {
if (!arr) return;
for (let i = 0; i < arr.length; i++) {
const metric = arr[i].metric;
if (!metric) continue;
const maybePriceMetric =
/** @type {{ usd?: AnyMetricPattern, sats?: AnyMetricPattern }} */ (
/** @type {unknown} */ (metric)
const s = arr[i].series;
if (!s) continue;
const maybePriceSeries =
/** @type {{ usd?: AnySeriesPattern, sats?: AnySeriesPattern }} */ (
/** @type {unknown} */ (s)
);
if (maybePriceMetric.usd?.by && maybePriceMetric.sats?.by) {
map.delete(maybePriceMetric.usd);
map.delete(maybePriceMetric.sats);
if (maybePriceSeries.usd?.by && maybePriceSeries.sats?.by) {
map.delete(maybePriceSeries.usd);
map.delete(maybePriceSeries.sats);
} else {
map.delete(/** @type {AnyMetricPattern} */ (metric));
map.delete(/** @type {AnySeriesPattern} */ (s));
}
}
}
/**
* Log unused metrics to console (localhost only)
* @param {TreeNode} metricsTree
* Log unused series to console (localhost only)
* @param {TreeNode} seriesTree
* @param {PartialOptionsTree} partialOptions
*/
export function logUnused(metricsTree, partialOptions) {
export function logUnused(seriesTree, partialOptions) {
if (!localhost) return;
console.log(extractTreeStructure(partialOptions));
/** @type {Map<AnyMetricPattern, string[]>} */
/** @type {Map<AnySeriesPattern, string[]>} */
const all = new Map();
walkMetrics(metricsTree, all, []);
walkSeries(seriesTree, all, []);
walkOptions(partialOptions, all);
if (!all.size) return;
@@ -135,7 +135,7 @@ export function logUnused(metricsTree, partialOptions) {
}
}
console.log("Unused metrics:", { count: all.size, tree });
console.log("Unused series:", { count: all.size, tree });
}
/**
@@ -154,10 +154,10 @@ export function extractTreeStructure(options) {
/** @type {Record<string, string[]>} */
const grouped = {};
for (const s of series) {
const metric = /** @type {AnyMetricPattern | AnyPricePattern} */ (
s.metric
const pattern = /** @type {AnySeriesPattern | AnyPricePattern} */ (
s.series
);
if (isTop && "usd" in metric && "sats" in metric) {
if (isTop && "usd" in pattern && "sats" in pattern) {
const title = s.title || s.key || "unnamed";
(grouped["USD"] ??= []).push(title);
(grouped["sats"] ??= []).push(title);
+35 -35
View File
@@ -3,7 +3,7 @@
// import { randomFromArray } from "../utils/array.js";
// import { createButtonElement, createHeader, createSelect } from "../utils/dom.js";
// import { tableElement } from "../utils/elements.js";
// import { serdeMetrics, serdeString } from "../utils/serde.js";
// import { serdeSeries, serdeString } from "../utils/serde.js";
// import { resetParams } from "../utils/url.js";
// export function init() {
@@ -45,7 +45,7 @@
// // * @param {Resources} args.resources
// // */
// // function createTable({ brk, signals, option, resources }) {
// // const indexToMetrics = createIndexToMetrics(metricToIndexes);
// // const indexToSeries = createIndexToMetrics(seriesToIndexes);
// // const serializedIndexes = createSerializedIndexes();
// // /** @type {SerializedIndex} */
@@ -78,17 +78,17 @@
// // resetParams(option);
// // }
// // const possibleMetrics = indexToMetrics[index];
// // const possibleSeries = indexToSeries[index];
// // const columns = signals.createSignal(/** @type {Metric[]} */ ([]), {
// // equals: false,
// // save: {
// // ...serdeMetrics,
// // ...serdeSeries,
// // keyPrefix: `table-${serializedIndex()}`,
// // key: `columns`,
// // },
// // });
// // columns.set((l) => l.filter((id) => possibleMetrics.includes(id)));
// // columns.set((l) => l.filter((id) => possibleSeries.includes(id)));
// // signals.createEffect(columns, (columns) => {
// // console.log(columns);
@@ -204,35 +204,35 @@
// // const owner = signals.getOwner();
// // /**
// // * @param {Metric} metric
// // * @param {Series} s
// // * @param {number} [_colIndex]
// // */
// // function addCol(metric, _colIndex = columns().length) {
// // function addCol(s, _colIndex = columns().length) {
// // signals.runWithOwner(owner, () => {
// // /** @type {VoidFunction | undefined} */
// // let dispose;
// // signals.createRoot((_dispose) => {
// // dispose = _dispose;
// // const metricOption = signals.createSignal({
// // name: metric,
// // value: metric,
// // const seriesOption = signals.createSignal({
// // name: s,
// // value: s,
// // });
// // const { select } = createSelect({
// // list: possibleMetrics.map((metric) => ({
// // name: metric,
// // value: metric,
// // list: possibleSeries.map((s) => ({
// // name: s,
// // value: s,
// // })),
// // signal: metricOption,
// // signal: seriesOption,
// // });
// // signals.createEffect(metricOption, (metricOption) => {
// // select.style.width = `${21 + 7.25 * metricOption.name.length}px`;
// // signals.createEffect(seriesOption, (seriesOption) => {
// // select.style.width = `${21 + 7.25 * seriesOption.name.length}px`;
// // });
// // if (_colIndex === columns().length) {
// // columns.set((l) => {
// // l.push(metric);
// // l.push(s);
// // return l;
// // });
// // }
@@ -282,7 +282,7 @@
// // const th = addThCol({
// // select,
// // unit: serdeUnit.deserialize(metric),
// // unit: serdeUnit.deserialize(s),
// // onLeft: createMoveColumnFunction(false),
// // onRight: createMoveColumnFunction(true),
// // onRemove: () => {
@@ -314,23 +314,23 @@
// // }
// // signals.createEffect(
// // () => metricOption().name,
// // (metric, prevMetric) => {
// // const unit = serdeUnit.deserialize(metric);
// // () => seriesOption().name,
// // (s, prevSeries) => {
// // const unit = serdeUnit.deserialize(s);
// // th.setUnit(unit);
// // const vec = resources.getOrCreate(index, metric);
// // const vec = resources.getOrCreate(index, s);
// // vec.fetch({ from, to });
// // const fetchedKey = resources.genFetchedKey({ from, to });
// // columns.set((l) => {
// // const i = l.indexOf(prevMetric ?? metric);
// // const i = l.indexOf(prevSeries ?? s);
// // if (i === -1) {
// // l.push(metric);
// // l.push(s);
// // } else {
// // l[i] = metric;
// // l[i] = s;
// // }
// // return l;
// // });
@@ -355,7 +355,7 @@
// // },
// // );
// // return () => metric;
// // return () => s;
// // },
// // );
// // });
@@ -367,10 +367,10 @@
// // });
// // }
// // columns().forEach((metric, colIndex) => addCol(metric, colIndex));
// // columns().forEach((s, colIndex) => addCol(s, colIndex));
// // obj.addRandomCol = function () {
// // addCol(randomFromArray(possibleMetrics));
// // addCol(randomFromArray(possibleSeries));
// // };
// // return () => index;
@@ -380,24 +380,24 @@
// // }
// /**
// * @param {MetricToIndexes} metricToIndexes
// * @param {SeriesToIndexes} seriesToIndexes
// */
// function createIndexToMetrics(metricToIndexes) {
// // const indexToMetrics = Object.entries(metricToIndexes).reduce(
// function createIndexToMetrics(seriesToIndexes) {
// // const indexToSeries = Object.entries(seriesToIndexes).reduce(
// // (arr, [_id, indexes]) => {
// // const id = /** @type {Metric} */ (_id);
// // const id = /** @type {Series} */ (_id);
// // indexes.forEach((i) => {
// // arr[i] ??= [];
// // arr[i].push(id);
// // });
// // return arr;
// // },
// // /** @type {Metric[][]} */ (Array.from({ length: 24 })),
// // /** @type {Series[][]} */ (Array.from({ length: 24 })),
// // );
// // indexToMetrics.forEach((arr) => {
// // indexToSeries.forEach((arr) => {
// // arr.sort();
// // });
// // return indexToMetrics;
// // return indexToSeries;
// }
// /**
+7 -7
View File
@@ -40,14 +40,14 @@ export function init() {
/** @type {Map<Unit, AnyFetchedSeriesBlueprint[]>} */
const result = new Map();
const { ohlc, spot } = brk.metrics.prices;
const { ohlc, spot } = brk.series.prices;
result.set(Unit.usd, [
/** @type {AnyFetchedSeriesBlueprint} */ ({
type: "Price",
title: "Price",
metric: spot.usd,
ohlcMetric: ohlc.usd,
series: spot.usd,
ohlcSeries: ohlc.usd,
}),
...(optionTop.get(Unit.usd) ?? []),
]);
@@ -56,8 +56,8 @@ export function init() {
/** @type {AnyFetchedSeriesBlueprint} */ ({
type: "Price",
title: "Price",
metric: spot.sats,
ohlcMetric: ohlc.sats,
series: spot.sats,
ohlcSeries: ohlc.sats,
colors: /** @type {const} */ ([colors.bi.p1[1], colors.bi.p1[0]]),
}),
...(optionTop.get(Unit.sats) ?? []),
@@ -143,10 +143,10 @@ function computeChoices(opt) {
[Array.from(opt.top().values()), Array.from(opt.bottom().values())]
.flat(2)
.filter((blueprint) => {
const path = Object.values(blueprint.metric.by)[0]?.path ?? "";
const path = Object.values(blueprint.series.by)[0]?.path ?? "";
return !path.includes("constant_");
})
.flatMap((blueprint) => blueprint.metric.indexes()),
.flatMap((blueprint) => blueprint.series.indexes()),
);
const groups = ALL_GROUPS
+24 -24
View File
@@ -2,7 +2,7 @@
* @import { IChartApi, ISeriesApi as _ISeriesApi, SeriesDefinition, SingleValueData as _SingleValueData, CandlestickData as _CandlestickData, BaselineData as _BaselineData, HistogramData as _HistogramData, SeriesType as LCSeriesType, IPaneApi, LineSeriesPartialOptions as _LineSeriesPartialOptions, HistogramSeriesPartialOptions as _HistogramSeriesPartialOptions, BaselineSeriesPartialOptions as _BaselineSeriesPartialOptions, CandlestickSeriesPartialOptions as _CandlestickSeriesPartialOptions, WhitespaceData, DeepPartial, ChartOptions, Time, LineData as _LineData, createChart as CreateLCChart, LineStyle, createSeriesMarkers as CreateSeriesMarkers, SeriesMarker, ISeriesMarkersPluginApi } from './modules/lightweight-charts/5.1.0/dist/typings.js'
*
* @import * as Brk from "./modules/brk-client/index.js"
* @import { BrkClient, Index, Metric, MetricData } from "./modules/brk-client/index.js"
* @import { BrkClient, Index, Series as BrkSeries, SeriesData } from "./modules/brk-client/index.js"
*
* @import { Options } from './options/full.js'
*
@@ -30,17 +30,17 @@
* @typedef {SeriesMarker<Time>} TimeSeriesMarker
*
* Brk tree types (stable across regenerations)
* @typedef {Brk.MetricsTree_Cohorts_Utxo} UtxoCohortTree
* @typedef {Brk.MetricsTree_Cohorts_Address} AddressCohortTree
* @typedef {Brk.MetricsTree_Cohorts_Utxo_All} AllUtxoPattern
* @typedef {Brk.MetricsTree_Cohorts_Utxo_Sth} ShortTermPattern
* @typedef {Brk.MetricsTree_Cohorts_Utxo_Lth} LongTermPattern
* @typedef {Brk.MetricsTree_Cohorts_Utxo_All_Unrealized} AllRelativePattern
* @typedef {Brk.SeriesTree_Cohorts_Utxo} UtxoCohortTree
* @typedef {Brk.SeriesTree_Cohorts_Address} AddressCohortTree
* @typedef {Brk.SeriesTree_Cohorts_Utxo_All} AllUtxoPattern
* @typedef {Brk.SeriesTree_Cohorts_Utxo_Sth} ShortTermPattern
* @typedef {Brk.SeriesTree_Cohorts_Utxo_Lth} LongTermPattern
* @typedef {Brk.SeriesTree_Cohorts_Utxo_All_Unrealized} AllRelativePattern
* @typedef {keyof Brk.BtcCentsSatsUsdPattern} BtcSatsUsdKey
* @typedef {Brk.BtcCentsSatsUsdPattern} SupplyPattern
* @typedef {Brk.AverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern} BlockSizePattern
* @typedef {keyof Brk.MetricsTree_Cohorts_Utxo_Type} SpendableType
* @typedef {keyof Brk.MetricsTree_Addresses_Raw} AddressableType
* @typedef {keyof Brk.SeriesTree_Cohorts_Utxo_Type} SpendableType
* @typedef {keyof Brk.SeriesTree_Addresses_Raw} AddressableType
*
* Brk pattern types (using new pattern names)
* @typedef {Brk.ActivityOutputsRealizedSupplyUnrealizedPattern} MaxAgePattern
@@ -68,15 +68,15 @@
* @typedef {Brk.AverageMaxMedianMinPct10Pct25Pct75Pct90Pattern} RollingWindowSlot
* AnyValuePatternType: union of all value pattern types
* @typedef {Brk.BaseCumulativeSumPattern4 | Brk.BaseCumulativeSumPattern<number> | Brk.BaseCumulativeRelPattern} AnyValuePatternType
* @typedef {Brk.AnyMetricPattern} AnyMetricPattern
* @typedef {Brk.AnySeriesPattern} AnySeriesPattern
* @typedef {Brk.CentsSatsUsdPattern} ActivePricePattern
* @typedef {Brk.AnyMetricEndpointBuilder} AnyMetricEndpoint
* @typedef {Brk.AnyMetricData} AnyMetricData
* @typedef {Brk.AnySeriesEndpointBuilder} AnySeriesEndpoint
* @typedef {Brk.AnySeriesData} AnySeriesData
* @typedef {Brk.AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern3} AddrCountPattern
* Relative patterns by capability:
* - BasicRelativePattern: minimal relative (investedCapitalIn*Pct, supplyIn*RelToOwnSupply only)
* - GlobalRelativePattern: has RelToMarketCap metrics (netUnrealizedPnlRelToMarketCap, etc)
* - OwnRelativePattern: has RelToOwnMarketCap metrics (netUnrealizedPnlRelToOwnMarketCap, etc)
* - GlobalRelativePattern: has RelToMarketCap series (netUnrealizedPnlRelToMarketCap, etc)
* - OwnRelativePattern: has RelToOwnMarketCap series (netUnrealizedPnlRelToOwnMarketCap, etc)
* - FullRelativePattern: has BOTH RelToMarketCap AND RelToOwnMarketCap
* @typedef {Brk.LossNetNuplProfitPattern} BasicRelativePattern
* @typedef {Brk.LossNetNuplProfitPattern} GlobalRelativePattern
@@ -96,7 +96,7 @@
/**
* @template T
* @typedef {Brk.MetricEndpointBuilder<T>} MetricEndpoint
* @typedef {Brk.SeriesEndpointBuilder<T>} SeriesEndpoint
*/
/**
* Stats pattern: average, min, max, percentiles (height-only indexes, NO base)
@@ -132,8 +132,8 @@
* @typedef {FullStatsPattern | BtcFullStatsPattern} AnyStatsPattern
*/
/**
* Distribution stats: 8 metric fields (average, min, max, median, pct10/25/75/90)
* @typedef {{ average: AnyMetricPattern, min: AnyMetricPattern, max: AnyMetricPattern, median: AnyMetricPattern, pct10: AnyMetricPattern, pct25: AnyMetricPattern, pct75: AnyMetricPattern, pct90: AnyMetricPattern }} DistributionStats
* Distribution stats: 8 series fields (average, min, max, median, pct10/25/75/90)
* @typedef {{ average: AnySeriesPattern, min: AnySeriesPattern, max: AnySeriesPattern, median: AnySeriesPattern, pct10: AnySeriesPattern, pct25: AnySeriesPattern, pct75: AnySeriesPattern, pct90: AnySeriesPattern }} DistributionStats
*/
/**
@@ -144,9 +144,9 @@
* @typedef {keyof PoolIdToPoolName} PoolId
*
* Tree branch types
* @typedef {Brk.MetricsTree_Market} Market
* @typedef {Brk.MetricsTree_Market_MovingAverage} MarketMovingAverage
* @typedef {Brk.MetricsTree_Market_Dca} MarketDca
* @typedef {Brk.SeriesTree_Market} Market
* @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
@@ -199,14 +199,14 @@
* Cohorts with RealizedWithExtras (realizedCapRelToOwnMarketCap + realizedProfitToLossRatio)
* @typedef {CohortAll | CohortFull | CohortWithPercentiles} CohortWithRealizedExtras
*
* Cohorts with circulating supply relative metrics (supplyRelToCirculatingSupply etc.)
* Cohorts with circulating supply relative series (supplyRelToCirculatingSupply etc.)
* These have GlobalRelativePattern or FullRelativePattern (same as RelativeWithMarketCap/RelativeWithNupl)
* @typedef {CohortFull | CohortLongTerm | CohortWithAdjusted | CohortBasicWithMarketCap} UtxoCohortWithCirculatingSupplyRelative
*
* Address cohorts with circulating supply relative metrics (all address amount cohorts have these)
* Address cohorts with circulating supply relative series (all address amount cohorts have these)
* @typedef {AddressCohortObject} AddressCohortWithCirculatingSupplyRelative
*
* All cohorts with circulating supply relative metrics
* All cohorts with circulating supply relative series
* @typedef {UtxoCohortWithCirculatingSupplyRelative | AddressCohortWithCirculatingSupplyRelative} CohortWithCirculatingSupplyRelative
*
* Delta patterns with absolute + rate rolling windows
@@ -218,6 +218,6 @@
* @typedef {Brk.BpsPriceRatioPattern} InvestorPercentileEntry
*
* Generic tree node type for walking
* @typedef {AnyMetricPattern | Record<string, unknown>} TreeNode
* @typedef {AnySeriesPattern | Record<string, unknown>} TreeNode
*
*/