mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-24 06:39:58 -07:00
clients: snapshot
This commit is contained in:
@@ -123,6 +123,9 @@ pub struct PatternBaseResult {
|
||||
/// Whether this instance uses suffix mode (common prefix) or prefix mode (common suffix).
|
||||
/// Used to check compatibility with the pattern's mode.
|
||||
pub is_suffix_mode: bool,
|
||||
/// The field parts (suffix in suffix mode, prefix in prefix mode) for each field.
|
||||
/// Used to check if instance field parts match the pattern's field parts.
|
||||
pub field_parts: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Get the metric base for a pattern instance by analyzing direct children.
|
||||
@@ -141,6 +144,7 @@ pub fn get_pattern_instance_base(node: &TreeNode) -> PatternBaseResult {
|
||||
base: String::new(),
|
||||
has_outlier: false,
|
||||
is_suffix_mode: true, // default
|
||||
field_parts: HashMap::new(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -150,6 +154,7 @@ pub fn get_pattern_instance_base(node: &TreeNode) -> PatternBaseResult {
|
||||
base: result.base,
|
||||
has_outlier: result.has_outlier,
|
||||
is_suffix_mode: result.is_suffix_mode,
|
||||
field_parts: result.field_parts,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -168,6 +173,7 @@ pub fn get_pattern_instance_base(node: &TreeNode) -> PatternBaseResult {
|
||||
base: result.base,
|
||||
has_outlier: true,
|
||||
is_suffix_mode: result.is_suffix_mode,
|
||||
field_parts: result.field_parts,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -179,14 +185,16 @@ pub fn get_pattern_instance_base(node: &TreeNode) -> PatternBaseResult {
|
||||
base: String::new(),
|
||||
has_outlier: false,
|
||||
is_suffix_mode: true, // default
|
||||
field_parts: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of try_find_base: base name, has_outlier flag, and is_suffix_mode flag.
|
||||
/// Result of try_find_base: base name, has_outlier flag, is_suffix_mode flag, and field_parts.
|
||||
struct FindBaseResult {
|
||||
base: String,
|
||||
has_outlier: bool,
|
||||
is_suffix_mode: bool,
|
||||
field_parts: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Try to find a common base from child names using prefix/suffix detection.
|
||||
@@ -197,20 +205,52 @@ fn try_find_base(child_names: &[(String, String)], is_outlier_attempt: bool) ->
|
||||
// Try common prefix first (suffix mode)
|
||||
if let Some(prefix) = find_common_prefix(&leaf_names) {
|
||||
let base = prefix.trim_end_matches('_').to_string();
|
||||
let mut field_parts = HashMap::new();
|
||||
for (field_name, leaf_name) in child_names {
|
||||
// Compute the suffix part for this field
|
||||
let suffix = if leaf_name == &base {
|
||||
String::new()
|
||||
} else {
|
||||
leaf_name
|
||||
.strip_prefix(&prefix)
|
||||
.unwrap_or(leaf_name)
|
||||
.to_string()
|
||||
};
|
||||
field_parts.insert(field_name.clone(), suffix);
|
||||
}
|
||||
return Some(FindBaseResult {
|
||||
base,
|
||||
has_outlier: is_outlier_attempt,
|
||||
is_suffix_mode: true,
|
||||
field_parts,
|
||||
});
|
||||
}
|
||||
|
||||
// Try common suffix (prefix mode)
|
||||
if let Some(suffix) = find_common_suffix(&leaf_names) {
|
||||
let base = suffix.trim_start_matches('_').to_string();
|
||||
let mut field_parts = HashMap::new();
|
||||
for (field_name, leaf_name) in child_names {
|
||||
// Compute the prefix part for this field
|
||||
let prefix_part = leaf_name
|
||||
.strip_suffix(&suffix)
|
||||
.map(|s| {
|
||||
if s.is_empty() {
|
||||
String::new()
|
||||
} else if s.ends_with('_') {
|
||||
s.to_string()
|
||||
} else {
|
||||
format!("{}_", s)
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
field_parts.insert(field_name.clone(), prefix_part);
|
||||
}
|
||||
return Some(FindBaseResult {
|
||||
base,
|
||||
has_outlier: is_outlier_attempt,
|
||||
is_suffix_mode: false,
|
||||
field_parts,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -55,17 +55,20 @@ pub fn prepare_tree_node<'a>(
|
||||
.map(|(f, _)| f.clone())
|
||||
.collect();
|
||||
|
||||
// Skip if this matches a parameterizable pattern AND has no outlier AND mode matches
|
||||
// Skip if this matches a parameterizable pattern AND has no outlier AND field parts match
|
||||
let base_result = get_pattern_instance_base(node);
|
||||
let mode_matches = pattern_lookup
|
||||
let pattern_fully_matches = pattern_lookup
|
||||
.get(&fields)
|
||||
.and_then(|name| metadata.find_pattern(name))
|
||||
.is_none_or(|p| p.is_suffix_mode() == base_result.is_suffix_mode);
|
||||
.is_some_and(|p| {
|
||||
p.is_suffix_mode() == base_result.is_suffix_mode
|
||||
&& p.field_parts_match(&base_result.field_parts)
|
||||
});
|
||||
if let Some(pattern_name) = pattern_lookup.get(&fields)
|
||||
&& pattern_name != name
|
||||
&& metadata.is_parameterizable(pattern_name)
|
||||
&& !base_result.has_outlier
|
||||
&& mode_matches
|
||||
&& pattern_fully_matches
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -89,16 +92,23 @@ pub fn prepare_tree_node<'a>(
|
||||
.as_ref()
|
||||
.is_some_and(|cf| metadata.matches_pattern(cf));
|
||||
|
||||
// Check if the pattern mode matches the instance mode
|
||||
let mode_matches = child_fields
|
||||
// Check if the pattern mode AND field parts match the instance
|
||||
let pattern_fully_matches = child_fields
|
||||
.as_ref()
|
||||
.and_then(|cf| metadata.find_pattern_by_fields(cf))
|
||||
.is_none_or(|p| p.is_suffix_mode() == base_result.is_suffix_mode);
|
||||
.is_some_and(|p| {
|
||||
// Mode must match (suffix vs prefix)
|
||||
if p.is_suffix_mode() != base_result.is_suffix_mode {
|
||||
return false;
|
||||
}
|
||||
// Field parts must also match
|
||||
p.field_parts_match(&base_result.field_parts)
|
||||
});
|
||||
|
||||
// should_inline determines if we generate an inline struct type
|
||||
// We inline if: it's a branch AND (doesn't match any pattern OR mode doesn't match OR has outlier)
|
||||
// We inline if: it's a branch AND (doesn't match any pattern OR pattern doesn't fully match OR has outlier)
|
||||
let should_inline =
|
||||
!is_leaf && (!matches_any_pattern || !mode_matches || base_result.has_outlier);
|
||||
!is_leaf && (!matches_any_pattern || !pattern_fully_matches || base_result.has_outlier);
|
||||
|
||||
// Inline type name (only used when should_inline is true)
|
||||
let inline_type_name = if should_inline {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Structural pattern and field types.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
|
||||
use brk_types::Index;
|
||||
|
||||
@@ -47,6 +47,30 @@ impl StructuralPattern {
|
||||
pub fn is_suffix_mode(&self) -> bool {
|
||||
matches!(&self.mode, Some(PatternMode::Suffix { .. }))
|
||||
}
|
||||
|
||||
/// Check if the given instance field parts match this pattern's field parts.
|
||||
/// Returns true if all field parts in the pattern match the instance's field parts.
|
||||
pub fn field_parts_match(&self, instance_field_parts: &HashMap<String, String>) -> bool {
|
||||
match &self.mode {
|
||||
Some(PatternMode::Suffix { relatives }) => {
|
||||
// For each field in the pattern, check if the instance has the same suffix
|
||||
relatives.iter().all(|(field_name, pattern_suffix)| {
|
||||
instance_field_parts
|
||||
.get(field_name)
|
||||
.is_some_and(|instance_suffix| instance_suffix == pattern_suffix)
|
||||
})
|
||||
}
|
||||
Some(PatternMode::Prefix { prefixes }) => {
|
||||
// For each field in the pattern, check if the instance has the same prefix
|
||||
prefixes.iter().all(|(field_name, pattern_prefix)| {
|
||||
instance_field_parts
|
||||
.get(field_name)
|
||||
.is_some_and(|instance_prefix| instance_prefix == pattern_prefix)
|
||||
})
|
||||
}
|
||||
None => false, // Non-parameterizable patterns don't use field parts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A field in a structural pattern.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -379,11 +379,11 @@ export function createChartElement({
|
||||
if (metric) {
|
||||
signals.createEffect(index, (index) => {
|
||||
// Get timestamp metric from tree based on index type
|
||||
// timestampFixed has height only, timestamp has date-based indexes
|
||||
// timestampMonotonic has height only, timestamp has date-based indexes
|
||||
/** @type {AnyMetricPattern} */
|
||||
const timeMetric =
|
||||
index === "height"
|
||||
? brk.metrics.blocks.time.timestampFixed
|
||||
? brk.metrics.blocks.time.timestampMonotonic
|
||||
: brk.metrics.blocks.time.timestamp;
|
||||
/** @type {AnyMetricPattern} */
|
||||
const valuesMetric = metric;
|
||||
|
||||
@@ -379,34 +379,34 @@ export function createCostBasisPercentilesSeries(ctx, list, useGroupName) {
|
||||
const percentiles = tree.costBasis.percentiles;
|
||||
return [
|
||||
line({
|
||||
metric: percentiles.costBasisPct10,
|
||||
metric: percentiles.pct10,
|
||||
name: useGroupName ? `${name} p10` : "p10",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: percentiles.costBasisPct25,
|
||||
metric: percentiles.pct25,
|
||||
name: useGroupName ? `${name} p25` : "p25",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: percentiles.costBasisPct50,
|
||||
metric: percentiles.pct50,
|
||||
name: useGroupName ? `${name} p50` : "p50",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
line({
|
||||
metric: percentiles.costBasisPct75,
|
||||
metric: percentiles.pct75,
|
||||
name: useGroupName ? `${name} p75` : "p75",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
defaultActive: false,
|
||||
}),
|
||||
line({
|
||||
metric: percentiles.costBasisPct90,
|
||||
metric: percentiles.pct90,
|
||||
name: useGroupName ? `${name} p90` : "p90",
|
||||
color,
|
||||
unit: Unit.usd,
|
||||
|
||||
@@ -64,7 +64,7 @@ export function createInvestingSection(ctx, { dca, lookback, returns }) {
|
||||
["10y", "_10y"],
|
||||
]).map(([id, key]) => {
|
||||
const name = periodIdToName(id, true);
|
||||
const priceAgo = lookback.priceAgo[key];
|
||||
const priceAgo = lookback[key];
|
||||
const priceReturns = returns.priceReturns[key];
|
||||
const dcaCostBasis = dca.periodAveragePrice[key];
|
||||
const dcaReturns = dca.periodReturns[key];
|
||||
@@ -181,14 +181,15 @@ export function createInvestingSection(ctx, { dca, lookback, returns }) {
|
||||
{
|
||||
name: "Returns",
|
||||
title: "DCA Returns by Year",
|
||||
bottom: dcaClasses.map(({ year, color, defaultActive, returns }) =>
|
||||
baseline({
|
||||
metric: returns,
|
||||
name: `${year}`,
|
||||
color,
|
||||
defaultActive,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
bottom: dcaClasses.map(
|
||||
({ year, color, defaultActive, returns }) =>
|
||||
baseline({
|
||||
metric: returns,
|
||||
name: `${year}`,
|
||||
color,
|
||||
defaultActive,
|
||||
unit: Unit.percentage,
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -118,18 +118,6 @@ export function fromBitcoin(colors, pattern, title, color) {
|
||||
return [
|
||||
{ metric: pattern.base, title, color: color ?? colors.default },
|
||||
{ metric: pattern.average, title: "Average", defaultActive: false },
|
||||
{
|
||||
metric: pattern.sum,
|
||||
title: `${title} (sum)`,
|
||||
color: colors.red,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.cumulative,
|
||||
title: `${title} (cum.)`,
|
||||
color: colors.cyan,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.max,
|
||||
title: "Max",
|
||||
@@ -333,20 +321,6 @@ export function fromFullnessPattern(colors, pattern, title, unit) {
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.sum,
|
||||
title: `${title} sum`,
|
||||
color: colors.blue,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.cumulative,
|
||||
title: `${title} cumulative`,
|
||||
color: colors.indigo,
|
||||
unit,
|
||||
defaultActive: false,
|
||||
},
|
||||
{
|
||||
metric: pattern.min,
|
||||
title: `${title} min`,
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"],
|
||||
"skipLibCheck": true,
|
||||
},
|
||||
"exclude": ["assets", "./scripts/modules", "../../modules"],
|
||||
"exclude": ["assets", "./scripts/modules"],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user