mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 06:01:57 -07:00
global: metrics -> series rename
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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")),
|
||||
]);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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('}')))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")`
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
)?;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
|
||||
|
||||
@@ -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,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(¶ms)?;
|
||||
|
||||
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 {
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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>);
|
||||
|
||||
+165
-139
@@ -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>()
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
|
||||
+3
-3
@@ -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)
|
||||
+6
-6
@@ -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
|
||||
+4
-4
@@ -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)
|
||||
@@ -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(¶ms.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(
|
||||
¶ms.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()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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::*;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,6 +1,6 @@
|
||||
use crate::Format;
|
||||
|
||||
/// Metric data output format
|
||||
/// Series data output format
|
||||
#[derive(Debug)]
|
||||
pub enum Output {
|
||||
Json(Vec<u8>),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
+5
-5
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+1850
-1820
File diff suppressed because it is too large
Load Diff
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }),
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -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 ?? []),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
|
||||
// /**
|
||||
|
||||
@@ -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
@@ -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
|
||||
*
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user