diff --git a/crates/brk_bindgen/src/generators/javascript/client.rs b/crates/brk_bindgen/src/generators/javascript/client.rs index 2c6d271c9..ca9d9ba1d 100644 --- a/crates/brk_bindgen/src/generators/javascript/client.rs +++ b/crates/brk_bindgen/src/generators/javascript/client.rs @@ -128,20 +128,14 @@ function dateToIndex(index, d) {{ * Wrap raw metric data with helper methods. * @template T * @param {{MetricData}} raw - Raw JSON response - * @returns {{MetricData}} + * @returns {{DateMetricData}} */ function _wrapMetricData(raw) {{ const {{ index, start, end, data }} = raw; const _dateBased = _DATE_INDEXES.has(index); - return /** @type {{MetricData}} */ ({{ + return /** @type {{DateMetricData}} */ ({{ ...raw, isDateBased: _dateBased, - dates() {{ - /** @type {{globalThis.Date[]}} */ - const result = []; - for (let i = start; i < end; i++) result.push(indexToDate(index, i)); - return result; - }}, indexes() {{ /** @type {{number[]}} */ const result = []; @@ -149,41 +143,48 @@ function _wrapMetricData(raw) {{ return result; }}, keys() {{ - return _dateBased ? this.dates() : this.indexes(); + return this.indexes(); }}, entries() {{ - /** @type {{Array<[globalThis.Date | number, T]>}} */ + /** @type {{Array<[number, T]>}} */ const result = []; - if (_dateBased) {{ - for (let i = 0; i < data.length; i++) result.push([indexToDate(index, start + i), data[i]]); - }} else {{ - for (let i = 0; i < data.length; i++) result.push([start + i, data[i]]); - }} + for (let i = 0; i < data.length; i++) result.push([start + i, data[i]]); return result; }}, toMap() {{ - /** @type {{Map}} */ + /** @type {{Map}} */ const map = new Map(); - if (_dateBased) {{ - for (let i = 0; i < data.length; i++) map.set(indexToDate(index, start + i), data[i]); - }} else {{ - for (let i = 0; i < data.length; i++) map.set(start + i, data[i]); - }} + for (let i = 0; i < data.length; i++) map.set(start + i, data[i]); return map; }}, *[Symbol.iterator]() {{ - if (_dateBased) {{ - for (let i = 0; i < data.length; i++) yield [indexToDate(index, start + i), data[i]]; - }} else {{ - for (let i = 0; i < data.length; i++) yield [start + i, data[i]]; - }} + for (let i = 0; i < data.length; i++) yield /** @type {{[number, T]}} */ ([start + i, data[i]]); + }}, + // DateMetricData methods (only meaningful for date-based indexes) + dates() {{ + /** @type {{globalThis.Date[]}} */ + const result = []; + for (let i = start; i < end; i++) result.push(indexToDate(index, i)); + return result; + }}, + dateEntries() {{ + /** @type {{Array<[globalThis.Date, T]>}} */ + const result = []; + for (let i = 0; i < data.length; i++) result.push([indexToDate(index, start + i), data[i]]); + return result; + }}, + toDateMap() {{ + /** @type {{Map}} */ + const map = new Map(); + for (let i = 0; i < data.length; i++) map.set(indexToDate(index, start + i), data[i]); + return map; }}, }}); }} /** * @template T - * @typedef {{Object}} MetricData + * @typedef {{Object}} MetricDataBase * @property {{number}} version - Version of the metric data * @property {{Index}} index - The index type used for this query * @property {{number}} total - Total number of data points @@ -192,26 +193,33 @@ function _wrapMetricData(raw) {{ * @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 {{() => (globalThis.Date[] | number[])}} keys - Get keys (dates for date-based, index numbers otherwise) - * @property {{() => Array<[globalThis.Date | number, T]>}} entries - Get [key, value] pairs (dates for date-based, index numbers otherwise) - * @property {{() => Map}} toMap - Return data as Map (dates for date-based, index numbers otherwise) - * @property {{() => globalThis.Date[]}} dates - Get dates (date-based indexes only, throws otherwise) * @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}} toMap - Convert to Map */ + +/** @template T @typedef {{MetricDataBase & Iterable<[number, T]>}} MetricData */ + +/** + * @template T + * @typedef {{Object}} DateMetricDataExtras + * @property {{() => globalThis.Date[]}} dates - Get dates for each data point + * @property {{() => Array<[globalThis.Date, T]>}} dateEntries - Get [date, value] pairs + * @property {{() => Map}} toDateMap - Convert to Map + */ + +/** @template T @typedef {{MetricData & DateMetricDataExtras}} DateMetricData */ /** @typedef {{MetricData}} AnyMetricData */ -/** - * Thenable interface for await support. - * @template T - * @typedef {{(onfulfilled?: (value: MetricData) => MetricData, onrejected?: (reason: Error) => never) => Promise>}} Thenable - */ +/** @template T @typedef {{(onfulfilled?: (value: MetricData) => any, onrejected?: (reason: Error) => never) => Promise>}} Thenable */ +/** @template T @typedef {{(onfulfilled?: (value: DateMetricData) => any, onrejected?: (reason: Error) => never) => Promise>}} DateThenable */ /** - * Metric endpoint builder. Callable (returns itself) so both .by.day1 and .by.day1() work. * @template T * @typedef {{Object}} MetricEndpointBuilder * @property {{(index: number) => SingleItemBuilder}} get - Get single item at index - * @property {{(start?: number | globalThis.Date, end?: number | globalThis.Date) => RangeBuilder}} slice - Slice by index or Date + * @property {{(start?: number, end?: number) => RangeBuilder}} slice - Slice by index * @property {{(n: number) => RangeBuilder}} first - Get first n items * @property {{(n: number) => RangeBuilder}} last - Get last n items * @property {{(n: number) => SkippedBuilder}} skip - Skip first n items, chain with take() @@ -220,38 +228,66 @@ function _wrapMetricData(raw) {{ * @property {{Thenable}} then - Thenable (await endpoint) * @property {{string}} path - The endpoint path */ -/** @typedef {{MetricEndpointBuilder}} AnyMetricEndpointBuilder */ /** * @template T - * @typedef {{Object}} SingleItemBuilder + * @typedef {{Object}} DateMetricEndpointBuilder + * @property {{(index: number | globalThis.Date) => DateSingleItemBuilder}} get - Get single item at index or Date + * @property {{(start?: number | globalThis.Date, end?: number | globalThis.Date) => DateRangeBuilder}} slice - Slice by index or Date + * @property {{(n: number) => DateRangeBuilder}} first - Get first n items + * @property {{(n: number) => DateRangeBuilder}} last - Get last n items + * @property {{(n: number) => DateSkippedBuilder}} skip - Skip first n items, chain with take() + * @property {{(onUpdate?: (value: DateMetricData) => void) => Promise>}} fetch - Fetch all data + * @property {{() => Promise}} fetchCsv - Fetch all data as CSV + * @property {{DateThenable}} then - Thenable (await endpoint) + * @property {{string}} path - The endpoint path + */ + +/** @typedef {{MetricEndpointBuilder}} AnyMetricEndpointBuilder */ + +/** @template T @typedef {{Object}} SingleItemBuilder * @property {{(onUpdate?: (value: MetricData) => void) => Promise>}} fetch - Fetch the item * @property {{() => Promise}} fetchCsv - Fetch as CSV * @property {{Thenable}} then - Thenable */ -/** - * @template T - * @typedef {{Object}} SkippedBuilder +/** @template T @typedef {{Object}} DateSingleItemBuilder + * @property {{(onUpdate?: (value: DateMetricData) => void) => Promise>}} fetch - Fetch the item + * @property {{() => Promise}} fetchCsv - Fetch as CSV + * @property {{DateThenable}} then - Thenable + */ + +/** @template T @typedef {{Object}} SkippedBuilder * @property {{(n: number) => RangeBuilder}} take - Take n items after skipped position * @property {{(onUpdate?: (value: MetricData) => void) => Promise>}} fetch - Fetch from skipped position to end * @property {{() => Promise}} fetchCsv - Fetch as CSV * @property {{Thenable}} then - Thenable */ -/** - * @template T - * @typedef {{Object}} RangeBuilder +/** @template T @typedef {{Object}} DateSkippedBuilder + * @property {{(n: number) => DateRangeBuilder}} take - Take n items after skipped position + * @property {{(onUpdate?: (value: DateMetricData) => void) => Promise>}} fetch - Fetch from skipped position to end + * @property {{() => Promise}} fetchCsv - Fetch as CSV + * @property {{DateThenable}} then - Thenable + */ + +/** @template T @typedef {{Object}} RangeBuilder * @property {{(onUpdate?: (value: MetricData) => void) => Promise>}} fetch - Fetch the range * @property {{() => Promise}} fetchCsv - Fetch as CSV * @property {{Thenable}} then - Thenable */ +/** @template T @typedef {{Object}} DateRangeBuilder + * @property {{(onUpdate?: (value: DateMetricData) => void) => Promise>}} fetch - Fetch the range + * @property {{() => Promise}} fetchCsv - Fetch as CSV + * @property {{DateThenable}} then - Thenable + */ + /** * @template T * @typedef {{Object}} MetricPattern * @property {{string}} name - The metric name - * @property {{Readonly>>>}} by - Index endpoints as lazy getters. Access via .by.day1 or .by['day1'] + * @property {{Readonly>>>}} by - Index endpoints as lazy getters * @property {{() => readonly Index[]}} indexes - Get the list of available indexes * @property {{(index: Index) => MetricEndpointBuilder|undefined}} get - Get an endpoint for a specific index */ @@ -264,7 +300,7 @@ function _wrapMetricData(raw) {{ * @param {{BrkClientBase}} client * @param {{string}} name - The metric vec name * @param {{Index}} index - The index name - * @returns {{MetricEndpointBuilder}} + * @returns {{DateMetricEndpointBuilder}} */ function _endpoint(client, name, index) {{ const p = `/api/metric/${{name}}/${{index}}`; @@ -287,7 +323,7 @@ function _endpoint(client, name, index) {{ /** * @param {{number}} [start] * @param {{number}} [end] - * @returns {{RangeBuilder}} + * @returns {{DateRangeBuilder}} */ const rangeBuilder = (start, end) => ({{ fetch(onUpdate) {{ return client._fetchMetricData(buildPath(start, end), onUpdate); }}, @@ -297,7 +333,7 @@ function _endpoint(client, name, index) {{ /** * @param {{number}} idx - * @returns {{SingleItemBuilder}} + * @returns {{DateSingleItemBuilder}} */ const singleItemBuilder = (idx) => ({{ fetch(onUpdate) {{ return client._fetchMetricData(buildPath(idx, idx + 1), onUpdate); }}, @@ -307,7 +343,7 @@ function _endpoint(client, name, index) {{ /** * @param {{number}} start - * @returns {{SkippedBuilder}} + * @returns {{DateSkippedBuilder}} */ const skippedBuilder = (start) => ({{ take(n) {{ return rangeBuilder(start, start + n); }}, @@ -316,9 +352,9 @@ function _endpoint(client, name, index) {{ then(resolve, reject) {{ return this.fetch().then(resolve, reject); }}, }}); - /** @type {{MetricEndpointBuilder}} */ + /** @type {{DateMetricEndpointBuilder}} */ const endpoint = {{ - get(idx) {{ return singleItemBuilder(idx); }}, + get(idx) {{ if (idx instanceof Date) idx = dateToIndex(index, idx); return singleItemBuilder(idx); }}, slice(start, end) {{ if (start instanceof Date) start = dateToIndex(index, start); if (end instanceof Date) end = dateToIndex(index, end); @@ -345,7 +381,8 @@ class BrkClientBase {{ */ constructor(options) {{ const isString = typeof options === 'string'; - this.baseUrl = isString ? options : options.baseUrl; + const rawUrl = isString ? options : options.baseUrl; + this.baseUrl = rawUrl.endsWith('/') ? rawUrl.slice(0, -1) : rawUrl; this.timeout = isString ? 5000 : (options.timeout ?? 5000); /** @type {{Promise}} */ this._cachePromise = _openCache(isString ? undefined : options.cache); @@ -359,8 +396,7 @@ class BrkClientBase {{ * @returns {{Promise}} */ async get(path) {{ - const base = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl; - const url = `${{base}}${{path}}`; + const url = `${{this.baseUrl}}${{path}}`; const res = await fetch(url, {{ signal: AbortSignal.timeout(this.timeout) }}); if (!res.ok) throw new BrkError(`HTTP ${{res.status}}: ${{url}}`, res.status); return res; @@ -374,8 +410,7 @@ class BrkClientBase {{ * @returns {{Promise}} */ async getJson(path, onUpdate) {{ - const base = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl; - const url = `${{base}}${{path}}`; + const url = `${{this.baseUrl}}${{path}}`; const cache = this._cache ?? await this._cachePromise; let resolved = false; @@ -437,8 +472,8 @@ class BrkClientBase {{ * Fetch metric data and wrap with helper methods (internal) * @template T * @param {{string}} path - * @param {{(value: MetricData) => void}} [onUpdate] - * @returns {{Promise>}} + * @param {{(value: DateMetricData) => void}} [onUpdate] + * @returns {{Promise>}} */ async _fetchMetricData(path, onUpdate) {{ const wrappedOnUpdate = onUpdate ? (/** @type {{MetricData}} */ raw) => onUpdate(_wrapMetricData(raw)) : undefined; @@ -566,7 +601,7 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern * @param {{readonly Index[]}} indexes - The supported indexes */ function _mp(client, name, indexes) {{ - const by = /** @type {{any}} */ ({{}}); + const by = {{}}; for (const idx of indexes) {{ Object.defineProperty(by, idx, {{ get() {{ return _endpoint(client, name, idx); }}, @@ -577,8 +612,9 @@ function _mp(client, name, indexes) {{ return {{ name, by, + /** @returns {{readonly Index[]}} */ indexes() {{ return indexes; }}, - /** @param {{Index}} index */ + /** @param {{Index}} index @returns {{MetricEndpointBuilder|undefined}} */ get(index) {{ return indexes.includes(index) ? _endpoint(client, name, index) : undefined; }} }}; }} @@ -593,10 +629,12 @@ function _mp(client, name, indexes) {{ .indexes .iter() .map(|idx| { - format!( - "readonly {}: MetricEndpointBuilder", - idx.name() - ) + let builder = if idx.is_date_based() { + "DateMetricEndpointBuilder" + } else { + "MetricEndpointBuilder" + }; + format!("readonly {}: {}", idx.name(), builder) }) .collect(); let by_type = format!("{{ {} }}", by_fields.join(", ")); @@ -617,7 +655,8 @@ function _mp(client, name, indexes) {{ .unwrap(); writeln!( output, - "function create{}(client, name) {{ return _mp(client, name, _i{}); }}", + "function create{}(client, name) {{ return /** @type {{{}}} */ (_mp(client, name, _i{})); }}", + pattern.name, pattern.name, i + 1 ) diff --git a/crates/brk_bindgen/src/generators/python/client.rs b/crates/brk_bindgen/src/generators/python/client.rs index 62383cf28..9fc7148f5 100644 --- a/crates/brk_bindgen/src/generators/python/client.rs +++ b/crates/brk_bindgen/src/generators/python/client.rs @@ -99,16 +99,16 @@ class BrkClientBase: """Make a GET request and return text.""" return self.get(path).decode() - def close(self): + def close(self) -> None: """Close the HTTP client.""" if self._conn: self._conn.close() self._conn = None - def __enter__(self): + def __enter__(self) -> BrkClientBase: return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, exc_type: Optional[type], exc_val: Optional[BaseException], exc_tb: Optional[Any]) -> None: self.close() @@ -221,7 +221,7 @@ def _date_to_index(index: str, d: Union[date, datetime]) -> int: @dataclass class MetricData(Generic[T]): - """Metric data with range information.""" + """Metric data with range information. Always int-indexed.""" version: int index: Index total: int @@ -235,63 +235,96 @@ class MetricData(Generic[T]): """Whether this metric uses a date-based index.""" return self.index in _DATE_INDEXES - def dates(self) -> list: - """Get dates for the index range. Date-based indexes only, throws otherwise.""" - return [_index_to_date(self.index, i) for i in range(self.start, self.end)] - def indexes(self) -> List[int]: """Get raw index numbers.""" return list(range(self.start, self.end)) - def keys(self) -> list: - """Get keys: dates for date-based indexes, index numbers otherwise.""" - return self.dates() if self.is_date_based else self.indexes() + def keys(self) -> List[int]: + """Get keys as index numbers.""" + return self.indexes() - def items(self) -> list: - """Get (key, value) pairs: keys are dates for date-based, numbers otherwise.""" - return list(zip(self.keys(), self.data)) + def items(self) -> List[Tuple[int, T]]: + """Get (index, value) pairs.""" + return list(zip(self.indexes(), self.data)) - def to_dict(self) -> dict: - """Return {{key: value}} dict: keys are dates for date-based, numbers otherwise.""" - return dict(zip(self.keys(), self.data)) + def to_dict(self) -> Dict[int, T]: + """Return {{index: value}} dict.""" + return dict(zip(self.indexes(), self.data)) - def __iter__(self): - """Iterate over (key, value) pairs. Keys are dates for date-based, numbers otherwise.""" - return iter(zip(self.keys(), self.data)) + def __iter__(self) -> Iterator[Tuple[int, T]]: + """Iterate over (index, value) pairs.""" + return iter(zip(self.indexes(), self.data)) + + def __len__(self) -> int: + return len(self.data) + + def to_polars(self) -> pl.DataFrame: + """Convert to Polars DataFrame with 'index' and 'value' columns.""" + try: + import polars as pl # type: ignore[import-not-found] + except ImportError: + raise ImportError("polars is required: pip install polars") + return pl.DataFrame({{"index": self.indexes(), "value": self.data}}) + + def to_pandas(self) -> pd.DataFrame: + """Convert to Pandas DataFrame with 'index' and 'value' columns.""" + try: + import pandas as pd # type: ignore[import-not-found] + except ImportError: + raise ImportError("pandas is required: pip install pandas") + return pd.DataFrame({{"index": self.indexes(), "value": self.data}}) + + +@dataclass +class DateMetricData(MetricData[T]): + """Metric data with date-based index. Extends MetricData 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+.""" + return [_index_to_date(self.index, i) for i in range(self.start, self.end)] + + def date_items(self) -> List[Tuple[Union[date, datetime], T]]: + """Get (date, value) pairs.""" + return list(zip(self.dates(), self.data)) + + def to_date_dict(self) -> Dict[Union[date, datetime], T]: + """Return {{date: value}} dict.""" + return dict(zip(self.dates(), self.data)) def to_polars(self, with_dates: bool = True) -> pl.DataFrame: - """Convert to Polars DataFrame. Requires polars to be installed. + """Convert to Polars DataFrame. Returns a DataFrame with columns: - - 'date' and 'value' if with_dates=True and index is date-based + - 'date' and 'value' if with_dates=True (default) - 'index' and 'value' otherwise """ try: import polars as pl # type: ignore[import-not-found] except ImportError: raise ImportError("polars is required: pip install polars") - if with_dates and self.is_date_based: + if with_dates: return pl.DataFrame({{"date": self.dates(), "value": self.data}}) return pl.DataFrame({{"index": self.indexes(), "value": self.data}}) def to_pandas(self, with_dates: bool = True) -> pd.DataFrame: - """Convert to Pandas DataFrame. Requires pandas to be installed. + """Convert to Pandas DataFrame. Returns a DataFrame with columns: - - 'date' and 'value' if with_dates=True and index is date-based + - 'date' and 'value' if with_dates=True (default) - 'index' and 'value' otherwise """ try: import pandas as pd # type: ignore[import-not-found] except ImportError: raise ImportError("pandas is required: pip install pandas") - if with_dates and self.is_date_based: + if with_dates: return pd.DataFrame({{"date": self.dates(), "value": self.data}}) return pd.DataFrame({{"index": self.indexes(), "value": self.data}}) -# Type alias for non-generic usage +# Type aliases for non-generic usage AnyMetricData = MetricData[Any] +AnyDateMetricData = DateMetricData[Any] class _EndpointConfig: @@ -325,9 +358,15 @@ class _EndpointConfig: p = self.path() return f"{{p}}?{{query}}" if query else p - def get_metric(self) -> MetricData: + 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_date_metric(self) -> DateMetricData[Any]: + return DateMetricData(**self.client.get_json(self._build_path())) + def get_csv(self) -> str: return self.client.get_text(self._build_path(format='csv')) @@ -371,10 +410,7 @@ class SkippedBuilder(Generic[T]): def take(self, n: int) -> RangeBuilder[T]: """Take n items after the skipped position.""" start = self._config.start or 0 - return RangeBuilder(_EndpointConfig( - self._config.client, self._config.name, self._config.index, - start, start + n - )) + return RangeBuilder(self._config._new(start, start + n)) def fetch(self) -> MetricData[T]: """Fetch from skipped position to end.""" @@ -385,29 +421,35 @@ class SkippedBuilder(Generic[T]): return self._config.get_csv() -class MetricEndpointBuilder(Generic[T]): - """Builder for metric endpoint queries. +class DateRangeBuilder(RangeBuilder[T]): + """Range builder that returns DateMetricData.""" + def fetch(self) -> DateMetricData[T]: + return self._config.get_date_metric() - Use method chaining to specify the data range, then call fetch() or fetch_csv() to execute. + +class DateSingleItemBuilder(SingleItemBuilder[T]): + """Single item builder that returns DateMetricData.""" + def fetch(self) -> DateMetricData[T]: + return self._config.get_date_metric() + + +class DateSkippedBuilder(SkippedBuilder[T]): + """Skipped builder that returns DateMetricData.""" + 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() + + +class MetricEndpointBuilder(Generic[T]): + """Builder for metric endpoint queries with int-based indexing. Examples: - # Fetch all data data = endpoint.fetch() - - # Single item access data = endpoint[5].fetch() - - # Slice syntax (Python-native) - data = endpoint[:10].fetch() # First 10 - data = endpoint[-5:].fetch() # Last 5 - data = endpoint[100:110].fetch() # Range - - # Convenience methods (pandas-style) - data = endpoint.head().fetch() # First 10 (default) - data = endpoint.head(20).fetch() # First 20 - data = endpoint.tail(5).fetch() # Last 5 - - # Iterator-style chaining + data = endpoint[:10].fetch() + data = endpoint.head(20).fetch() data = endpoint.skip(100).take(10).fetch() """ @@ -419,66 +461,30 @@ class MetricEndpointBuilder(Generic[T]): @overload def __getitem__(self, key: slice) -> RangeBuilder[T]: ... - def __getitem__(self, key: Union[int, slice, date, datetime]) -> Union[SingleItemBuilder[T], RangeBuilder[T]]: - """Access single item or slice. Accepts dates for date-based indexes. - - Examples: - endpoint[5] # Single item at index 5 - endpoint[:10] # First 10 - endpoint[-5:] # Last 5 - endpoint[100:110] # Range 100-109 - endpoint[date(2020, 1, 1):date(2023, 1, 1)] # Date range - endpoint[date(2020, 1, 1):] # Since date - """ - if isinstance(key, (date, datetime)): - idx = _date_to_index(self._config.index, key) - return SingleItemBuilder(_EndpointConfig( - self._config.client, self._config.name, self._config.index, - idx, idx + 1 - )) + def __getitem__(self, key: Union[int, slice]) -> Union[SingleItemBuilder[T], RangeBuilder[T]]: + """Access single item or slice by integer index.""" if isinstance(key, int): - return SingleItemBuilder(_EndpointConfig( - self._config.client, self._config.name, self._config.index, - key, key + 1 - )) - start, stop = key.start, key.stop - if isinstance(start, (date, datetime)): - start = _date_to_index(self._config.index, start) - if isinstance(stop, (date, datetime)): - stop = _date_to_index(self._config.index, stop) - return RangeBuilder(_EndpointConfig( - self._config.client, self._config.name, self._config.index, - start, stop - )) + return SingleItemBuilder(self._config._new(key, key + 1)) + return RangeBuilder(self._config._new(key.start, key.stop)) def head(self, n: int = 10) -> RangeBuilder[T]: - """Get the first n items (pandas-style).""" - return RangeBuilder(_EndpointConfig( - self._config.client, self._config.name, self._config.index, - None, n - )) + """Get the first n items.""" + return RangeBuilder(self._config._new(end=n)) def tail(self, n: int = 10) -> RangeBuilder[T]: - """Get the last n items (pandas-style).""" - start, end = (None, 0) if n == 0 else (-n, None) - return RangeBuilder(_EndpointConfig( - self._config.client, self._config.name, self._config.index, - start, end - )) + """Get the last n items.""" + return RangeBuilder(self._config._new(end=0) if n == 0 else self._config._new(start=-n)) def skip(self, n: int) -> SkippedBuilder[T]: - """Skip the first n items. Chain with take() to get a range.""" - return SkippedBuilder(_EndpointConfig( - self._config.client, self._config.name, self._config.index, - n, None - )) + """Skip the first n items.""" + return SkippedBuilder(self._config._new(start=n)) def fetch(self) -> MetricData[T]: - """Fetch all data as parsed JSON.""" + """Fetch all data.""" return self._config.get_metric() def fetch_csv(self) -> str: - """Fetch all data as CSV string.""" + """Fetch all data as CSV.""" return self._config.get_csv() def path(self) -> str: @@ -486,8 +492,72 @@ class MetricEndpointBuilder(Generic[T]): return self._config.path() -# Type alias for non-generic usage +class DateMetricEndpointBuilder(Generic[T]): + """Builder for metric endpoint queries with date-based indexing. + + Accepts dates in __getitem__ and returns DateMetricData from fetch(). + + Examples: + data = endpoint.fetch() + data = endpoint[date(2020, 1, 1)].fetch() + data = endpoint[date(2020, 1, 1):date(2023, 1, 1)].fetch() + data = endpoint[:10].fetch() + """ + + def __init__(self, client: BrkClientBase, name: str, index: Index): + self._config = _EndpointConfig(client, name, index) + + @overload + def __getitem__(self, key: int) -> DateSingleItemBuilder[T]: ... + @overload + def __getitem__(self, key: datetime) -> DateSingleItemBuilder[T]: ... + @overload + def __getitem__(self, key: date) -> DateSingleItemBuilder[T]: ... + @overload + def __getitem__(self, key: slice) -> DateRangeBuilder[T]: ... + + def __getitem__(self, key: Union[int, slice, date, datetime]) -> Union[DateSingleItemBuilder[T], DateRangeBuilder[T]]: + """Access single item or slice. Accepts int, date, or datetime.""" + if isinstance(key, (date, datetime)): + idx = _date_to_index(self._config.index, key) + return DateSingleItemBuilder(self._config._new(idx, idx + 1)) + if isinstance(key, int): + return DateSingleItemBuilder(self._config._new(key, key + 1)) + start, stop = key.start, key.stop + if isinstance(start, (date, datetime)): + start = _date_to_index(self._config.index, start) + if isinstance(stop, (date, datetime)): + stop = _date_to_index(self._config.index, stop) + return DateRangeBuilder(self._config._new(start, stop)) + + def head(self, n: int = 10) -> DateRangeBuilder[T]: + """Get the first n items.""" + return DateRangeBuilder(self._config._new(end=n)) + + def tail(self, n: int = 10) -> DateRangeBuilder[T]: + """Get the last n items.""" + return DateRangeBuilder(self._config._new(end=0) if n == 0 else self._config._new(start=-n)) + + def skip(self, n: int) -> DateSkippedBuilder[T]: + """Skip the first n items.""" + return DateSkippedBuilder(self._config._new(start=n)) + + def fetch(self) -> DateMetricData[T]: + """Fetch all data.""" + return self._config.get_date_metric() + + def fetch_csv(self) -> str: + """Fetch all data as CSV.""" + return self._config.get_csv() + + def path(self) -> str: + """Get the base endpoint path.""" + return self._config.path() + + +# Type aliases for non-generic usage AnyMetricEndpointBuilder = MetricEndpointBuilder[Any] +AnyDateMetricEndpointBuilder = DateMetricEndpointBuilder[Any] class MetricPattern(Protocol[T]): @@ -535,11 +605,14 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern } writeln!(output).unwrap(); - // Generate helper function + // Generate helper functions writeln!( output, r#"def _ep(c: BrkClientBase, n: str, i: Index) -> MetricEndpointBuilder[Any]: return MetricEndpointBuilder(c, n, i) + +def _dep(c: BrkClientBase, n: str, i: Index) -> DateMetricEndpointBuilder[Any]: + return DateMetricEndpointBuilder(c, n, i) "# ) .unwrap(); @@ -560,10 +633,15 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern for index in &pattern.indexes { let method_name = index_to_field_name(index); let index_name = index.name(); + let (builder_type, helper) = if index.is_date_based() { + ("DateMetricEndpointBuilder", "_dep") + } else { + ("MetricEndpointBuilder", "_ep") + }; writeln!( output, - " def {}(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, '{}')", - method_name, index_name + " def {}(self) -> {}[T]: return {}(self._c, self._n, '{}')", + method_name, builder_type, helper, index_name ) .unwrap(); } diff --git a/crates/brk_bindgen/src/generators/python/mod.rs b/crates/brk_bindgen/src/generators/python/mod.rs index ed37e27db..fa1c684f1 100644 --- a/crates/brk_bindgen/src/generators/python/mod.rs +++ b/crates/brk_bindgen/src/generators/python/mod.rs @@ -29,7 +29,7 @@ pub fn generate_python_client( writeln!(output, "from dataclasses import dataclass").unwrap(); writeln!( output, - "from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict, Union, Protocol, overload, Iterator, Tuple, TYPE_CHECKING" + "from typing import TypeVar, Generic, Any, Dict, Optional, List, Iterator, Literal, TypedDict, Union, Protocol, overload, Tuple, TYPE_CHECKING" ) .unwrap(); writeln!( diff --git a/crates/brk_bindgen/src/generators/rust/api.rs b/crates/brk_bindgen/src/generators/rust/api.rs index e8f7e896c..ac5faac60 100644 --- a/crates/brk_bindgen/src/generators/rust/api.rs +++ b/crates/brk_bindgen/src/generators/rust/api.rs @@ -57,6 +57,20 @@ impl BrkClient {{ index, ) }} + + /// Create a dynamic date-based metric endpoint builder. + /// + /// Returns `Err` if the index is not date-based. + pub fn date_metric(&self, metric: impl Into, index: Index) -> Result> {{ + if !index.is_date_based() {{ + return Err(BrkError {{ message: format!("{{}} is not a date-based index", index.name()) }}); + }} + Ok(DateMetricEndpointBuilder::new( + self.base.clone(), + Arc::from(metric.into().as_str()), + index, + )) + }} "#, VERSION = VERSION ) diff --git a/crates/brk_bindgen/src/generators/rust/client.rs b/crates/brk_bindgen/src/generators/rust/client.rs index 21310dcff..8cb722942 100644 --- a/crates/brk_bindgen/src/generators/rust/client.rs +++ b/crates/brk_bindgen/src/generators/rust/client.rs @@ -200,45 +200,39 @@ impl EndpointConfig {{ }} }} -/// Initial builder for metric endpoint queries. +/// Builder for metric endpoint queries. /// -/// Use method chaining to specify the data range, then call `fetch()` or `fetch_csv()` to execute. +/// Parameterized by element type `T` and response type `D` (defaults to `MetricData`). +/// For date-based indexes, use `DateMetricEndpointBuilder` which sets `D = DateMetricData`. /// /// # Examples /// ```ignore -/// // Fetch all data -/// let data = endpoint.fetch()?; -/// -/// // Get single item at index 5 -/// let data = endpoint.get(5).fetch()?; -/// -/// // Get first 10 using range -/// let data = endpoint.range(..10).fetch()?; -/// -/// // Get range [100, 200) -/// let data = endpoint.range(100..200).fetch()?; -/// -/// // Get first 10 (convenience) -/// let data = endpoint.take(10).fetch()?; -/// -/// // Get last 10 -/// let data = endpoint.last(10).fetch()?; -/// -/// // Iterator-style chaining -/// let data = endpoint.skip(100).take(10).fetch()?; +/// let data = endpoint.fetch()?; // all data +/// let data = endpoint.get(5).fetch()?; // single item +/// let data = endpoint.range(..10).fetch()?; // first 10 +/// let data = endpoint.range(100..200).fetch()?; // range [100, 200) +/// let data = endpoint.take(10).fetch()?; // first 10 (convenience) +/// let data = endpoint.last(10).fetch()?; // last 10 +/// let data = endpoint.skip(100).take(10).fetch()?; // iterator-style /// ``` -pub struct MetricEndpointBuilder {{ +pub struct MetricEndpointBuilder> {{ config: EndpointConfig, - _marker: std::marker::PhantomData, + _marker: std::marker::PhantomData (T, D)>, }} -impl MetricEndpointBuilder {{ +/// Builder for date-based metric endpoint queries. +/// +/// Like `MetricEndpointBuilder` but returns `DateMetricData` and provides +/// date-based access methods (`get_date`, `date_range`). +pub type DateMetricEndpointBuilder = MetricEndpointBuilder>; + +impl MetricEndpointBuilder {{ pub fn new(client: Arc, name: Arc, index: Index) -> Self {{ Self {{ config: EndpointConfig::new(client, name, index), _marker: std::marker::PhantomData }} }} /// Select a specific index position. - pub fn get(mut self, index: usize) -> SingleItemBuilder {{ + pub fn get(mut self, index: usize) -> SingleItemBuilder {{ self.config.start = Some(index as i64); self.config.end = Some(index as i64 + 1); SingleItemBuilder {{ config: self.config, _marker: std::marker::PhantomData }} @@ -252,7 +246,7 @@ impl MetricEndpointBuilder {{ /// endpoint.range(100..110) // indices 100-109 /// endpoint.range(100..) // from 100 to end /// ``` - pub fn range>(mut self, range: R) -> RangeBuilder {{ + pub fn range>(mut self, range: R) -> RangeBuilder {{ self.config.start = match range.start_bound() {{ Bound::Included(&n) => Some(n as i64), Bound::Excluded(&n) => Some(n as i64 + 1), @@ -267,12 +261,12 @@ impl MetricEndpointBuilder {{ }} /// Take the first n items. - pub fn take(self, n: usize) -> RangeBuilder {{ + pub fn take(self, n: usize) -> RangeBuilder {{ self.range(..n) }} /// Take the last n items. - pub fn last(mut self, n: usize) -> RangeBuilder {{ + pub fn last(mut self, n: usize) -> RangeBuilder {{ if n == 0 {{ self.config.end = Some(0); }} else {{ @@ -282,13 +276,13 @@ impl MetricEndpointBuilder {{ }} /// Skip the first n items. Chain with `take(n)` to get a range. - pub fn skip(mut self, n: usize) -> SkippedBuilder {{ + pub fn skip(mut self, n: usize) -> SkippedBuilder {{ self.config.start = Some(n as i64); SkippedBuilder {{ config: self.config, _marker: std::marker::PhantomData }} }} /// Fetch all data as parsed JSON. - pub fn fetch(self) -> Result> {{ + pub fn fetch(self) -> Result {{ self.config.get_json(None) }} @@ -303,15 +297,47 @@ impl MetricEndpointBuilder {{ }} }} -/// Builder for single item access. -pub struct SingleItemBuilder {{ - config: EndpointConfig, - _marker: std::marker::PhantomData, +/// Date-specific methods available only on `DateMetricEndpointBuilder`. +impl MetricEndpointBuilder> {{ + /// Select a specific date position (for day-precision or coarser indexes). + pub fn get_date(self, date: Date) -> SingleItemBuilder> {{ + 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> {{ + 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> {{ + 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> {{ + 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) + }} }} -impl SingleItemBuilder {{ +/// Builder for single item access. +pub struct SingleItemBuilder> {{ + config: EndpointConfig, + _marker: std::marker::PhantomData (T, D)>, +}} + +/// Date-aware single item builder. +pub type DateSingleItemBuilder = SingleItemBuilder>; + +impl SingleItemBuilder {{ /// Fetch the single item. - pub fn fetch(self) -> Result> {{ + pub fn fetch(self) -> Result {{ self.config.get_json(None) }} @@ -322,21 +348,24 @@ impl SingleItemBuilder {{ }} /// Builder after calling `skip(n)`. Chain with `take(n)` to specify count. -pub struct SkippedBuilder {{ +pub struct SkippedBuilder> {{ config: EndpointConfig, - _marker: std::marker::PhantomData, + _marker: std::marker::PhantomData (T, D)>, }} -impl SkippedBuilder {{ +/// Date-aware skipped builder. +pub type DateSkippedBuilder = SkippedBuilder>; + +impl SkippedBuilder {{ /// Take n items after the skipped position. - pub fn take(mut self, n: usize) -> RangeBuilder {{ + pub fn take(mut self, n: usize) -> RangeBuilder {{ let start = self.config.start.unwrap_or(0); self.config.end = Some(start + n as i64); RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }} }} /// Fetch from the skipped position to the end. - pub fn fetch(self) -> Result> {{ + pub fn fetch(self) -> Result {{ self.config.get_json(None) }} @@ -347,14 +376,17 @@ impl SkippedBuilder {{ }} /// Builder with range fully specified. -pub struct RangeBuilder {{ +pub struct RangeBuilder> {{ config: EndpointConfig, - _marker: std::marker::PhantomData, + _marker: std::marker::PhantomData (T, D)>, }} -impl RangeBuilder {{ +/// Date-aware range builder. +pub type DateRangeBuilder = RangeBuilder>; + +impl RangeBuilder {{ /// Fetch the range as parsed JSON. - pub fn fetch(self) -> Result> {{ + pub fn fetch(self) -> Result {{ self.config.get_json(None) }} @@ -389,13 +421,18 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern } writeln!(output).unwrap(); - // Generate helper function + // Generate helper functions writeln!( output, r#"#[inline] fn _ep(c: &Arc, n: &Arc, i: Index) -> MetricEndpointBuilder {{ MetricEndpointBuilder::new(c.clone(), n.clone(), i) }} + +#[inline] +fn _dep(c: &Arc, n: &Arc, i: Index) -> DateMetricEndpointBuilder {{ + DateMetricEndpointBuilder::new(c.clone(), n.clone(), i) +}} "# ) .unwrap(); @@ -412,12 +449,21 @@ fn _ep(c: &Arc, n: &Arc, i: Index) -> M writeln!(output, "impl {} {{", by_name).unwrap(); for index in &pattern.indexes { let method_name = index_to_field_name(index); - writeln!( - output, - " pub fn {}(&self) -> MetricEndpointBuilder {{ _ep(&self.client, &self.name, Index::{}) }}", - method_name, index - ) - .unwrap(); + if index.is_date_based() { + writeln!( + output, + " pub fn {}(&self) -> DateMetricEndpointBuilder {{ _dep(&self.client, &self.name, Index::{}) }}", + method_name, index + ) + .unwrap(); + } else { + writeln!( + output, + " pub fn {}(&self) -> MetricEndpointBuilder {{ _ep(&self.client, &self.name, Index::{}) }}", + method_name, index + ) + .unwrap(); + } } writeln!(output, "}}\n").unwrap(); diff --git a/crates/brk_client/examples/basic.rs b/crates/brk_client/examples/basic.rs index 9afa86a95..3cd1c6383 100644 --- a/crates/brk_client/examples/basic.rs +++ b/crates/brk_client/examples/basic.rs @@ -14,7 +14,7 @@ fn main() -> brk_client::Result<()> { }); // Fetch price data using the typed metrics API - // Using new idiomatic API: last(3).fetch() + // day1() returns DateMetricEndpointBuilder, so fetch() returns DateMetricData let price_close = client .metrics() .prices @@ -25,9 +25,17 @@ fn main() -> brk_client::Result<()> { .day1() .last(3) .fetch()?; - println!("Last 3 price close values: {:?}", price_close); + println!("Last 3 price close values:"); + // iter_dates() returns Option (None for sub-daily indexes) + for (date, value) in price_close.iter_dates().unwrap() { + println!(" {}: {}", date, value); + } + // iter_timestamps() works for all date-based indexes including sub-daily + for (ts, value) in price_close.iter_timestamps() { + println!(" {}: {}", ts, value); + } - // Fetch block data + // Fetch block data with height index (non-date, returns MetricData) let block_count = client .metrics() .blocks @@ -38,9 +46,12 @@ fn main() -> brk_client::Result<()> { .day1() .last(3) .fetch()?; - println!("Last 3 block count values: {:?}", block_count); + println!("Last 3 block count values:"); + for (date, value) in block_count.iter_dates().unwrap() { + println!(" {}: {}", date, value); + } - // Fetch supply data + // Fetch supply data as CSV dbg!(client.metrics().supply.circulating.btc.by.day1().path()); let circulating = client .metrics() @@ -51,9 +62,19 @@ fn main() -> brk_client::Result<()> { .day1() .last(3) .fetch_csv()?; - println!("Last 3 circulating supply values: {:?}", circulating); + println!("Last 3 circulating supply (CSV): {:?}", circulating); - // Using generic metric fetching + // Using dynamic metric fetching with date_metric() for date-based indexes + let date_metric = client + .date_metric(Metric::from("price_close"), Index::Day1)? + .last(3) + .fetch()?; + println!("Dynamic date metric fetch:"); + for (date, value) in date_metric.iter_dates().unwrap() { + println!(" {}: {}", date, value); + } + + // Using generic metric fetching (returns FormatResponse) let metricdata = client.get_metric( Metric::from("price_close"), Index::Day1, diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index f417fd143..029451ba9 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -172,45 +172,39 @@ impl EndpointConfig { } } -/// Initial builder for metric endpoint queries. +/// Builder for metric endpoint queries. /// -/// Use method chaining to specify the data range, then call `fetch()` or `fetch_csv()` to execute. +/// Parameterized by element type `T` and response type `D` (defaults to `MetricData`). +/// For date-based indexes, use `DateMetricEndpointBuilder` which sets `D = DateMetricData`. /// /// # Examples /// ```ignore -/// // Fetch all data -/// let data = endpoint.fetch()?; -/// -/// // Get single item at index 5 -/// let data = endpoint.get(5).fetch()?; -/// -/// // Get first 10 using range -/// let data = endpoint.range(..10).fetch()?; -/// -/// // Get range [100, 200) -/// let data = endpoint.range(100..200).fetch()?; -/// -/// // Get first 10 (convenience) -/// let data = endpoint.take(10).fetch()?; -/// -/// // Get last 10 -/// let data = endpoint.last(10).fetch()?; -/// -/// // Iterator-style chaining -/// let data = endpoint.skip(100).take(10).fetch()?; +/// let data = endpoint.fetch()?; // all data +/// let data = endpoint.get(5).fetch()?; // single item +/// let data = endpoint.range(..10).fetch()?; // first 10 +/// let data = endpoint.range(100..200).fetch()?; // range [100, 200) +/// let data = endpoint.take(10).fetch()?; // first 10 (convenience) +/// let data = endpoint.last(10).fetch()?; // last 10 +/// let data = endpoint.skip(100).take(10).fetch()?; // iterator-style /// ``` -pub struct MetricEndpointBuilder { +pub struct MetricEndpointBuilder> { config: EndpointConfig, - _marker: std::marker::PhantomData, + _marker: std::marker::PhantomData (T, D)>, } -impl MetricEndpointBuilder { +/// Builder for date-based metric endpoint queries. +/// +/// Like `MetricEndpointBuilder` but returns `DateMetricData` and provides +/// date-based access methods (`get_date`, `date_range`). +pub type DateMetricEndpointBuilder = MetricEndpointBuilder>; + +impl MetricEndpointBuilder { pub fn new(client: Arc, name: Arc, index: Index) -> Self { Self { config: EndpointConfig::new(client, name, index), _marker: std::marker::PhantomData } } /// Select a specific index position. - pub fn get(mut self, index: usize) -> SingleItemBuilder { + pub fn get(mut self, index: usize) -> SingleItemBuilder { self.config.start = Some(index as i64); self.config.end = Some(index as i64 + 1); SingleItemBuilder { config: self.config, _marker: std::marker::PhantomData } @@ -224,7 +218,7 @@ impl MetricEndpointBuilder { /// endpoint.range(100..110) // indices 100-109 /// endpoint.range(100..) // from 100 to end /// ``` - pub fn range>(mut self, range: R) -> RangeBuilder { + pub fn range>(mut self, range: R) -> RangeBuilder { self.config.start = match range.start_bound() { Bound::Included(&n) => Some(n as i64), Bound::Excluded(&n) => Some(n as i64 + 1), @@ -239,12 +233,12 @@ impl MetricEndpointBuilder { } /// Take the first n items. - pub fn take(self, n: usize) -> RangeBuilder { + pub fn take(self, n: usize) -> RangeBuilder { self.range(..n) } /// Take the last n items. - pub fn last(mut self, n: usize) -> RangeBuilder { + pub fn last(mut self, n: usize) -> RangeBuilder { if n == 0 { self.config.end = Some(0); } else { @@ -254,13 +248,13 @@ impl MetricEndpointBuilder { } /// Skip the first n items. Chain with `take(n)` to get a range. - pub fn skip(mut self, n: usize) -> SkippedBuilder { + pub fn skip(mut self, n: usize) -> SkippedBuilder { self.config.start = Some(n as i64); SkippedBuilder { config: self.config, _marker: std::marker::PhantomData } } /// Fetch all data as parsed JSON. - pub fn fetch(self) -> Result> { + pub fn fetch(self) -> Result { self.config.get_json(None) } @@ -275,15 +269,47 @@ impl MetricEndpointBuilder { } } -/// Builder for single item access. -pub struct SingleItemBuilder { - config: EndpointConfig, - _marker: std::marker::PhantomData, +/// Date-specific methods available only on `DateMetricEndpointBuilder`. +impl MetricEndpointBuilder> { + /// Select a specific date position (for day-precision or coarser indexes). + pub fn get_date(self, date: Date) -> SingleItemBuilder> { + 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> { + 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> { + 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> { + 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) + } } -impl SingleItemBuilder { +/// Builder for single item access. +pub struct SingleItemBuilder> { + config: EndpointConfig, + _marker: std::marker::PhantomData (T, D)>, +} + +/// Date-aware single item builder. +pub type DateSingleItemBuilder = SingleItemBuilder>; + +impl SingleItemBuilder { /// Fetch the single item. - pub fn fetch(self) -> Result> { + pub fn fetch(self) -> Result { self.config.get_json(None) } @@ -294,21 +320,24 @@ impl SingleItemBuilder { } /// Builder after calling `skip(n)`. Chain with `take(n)` to specify count. -pub struct SkippedBuilder { +pub struct SkippedBuilder> { config: EndpointConfig, - _marker: std::marker::PhantomData, + _marker: std::marker::PhantomData (T, D)>, } -impl SkippedBuilder { +/// Date-aware skipped builder. +pub type DateSkippedBuilder = SkippedBuilder>; + +impl SkippedBuilder { /// Take n items after the skipped position. - pub fn take(mut self, n: usize) -> RangeBuilder { + pub fn take(mut self, n: usize) -> RangeBuilder { let start = self.config.start.unwrap_or(0); self.config.end = Some(start + n as i64); RangeBuilder { config: self.config, _marker: std::marker::PhantomData } } /// Fetch from the skipped position to the end. - pub fn fetch(self) -> Result> { + pub fn fetch(self) -> Result { self.config.get_json(None) } @@ -319,14 +348,17 @@ impl SkippedBuilder { } /// Builder with range fully specified. -pub struct RangeBuilder { +pub struct RangeBuilder> { config: EndpointConfig, - _marker: std::marker::PhantomData, + _marker: std::marker::PhantomData (T, D)>, } -impl RangeBuilder { +/// Date-aware range builder. +pub type DateRangeBuilder = RangeBuilder>; + +impl RangeBuilder { /// Fetch the range as parsed JSON. - pub fn fetch(self) -> Result> { + pub fn fetch(self) -> Result { self.config.get_json(None) } @@ -381,25 +413,30 @@ fn _ep(c: &Arc, n: &Arc, i: Index) -> M MetricEndpointBuilder::new(c.clone(), n.clone(), i) } +#[inline] +fn _dep(c: &Arc, n: &Arc, i: Index) -> DateMetricEndpointBuilder { + DateMetricEndpointBuilder::new(c.clone(), n.clone(), i) +} + // Index accessor structs pub struct MetricPattern1By { client: Arc, name: Arc, _marker: std::marker::PhantomData } impl MetricPattern1By { - pub fn minute1(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Minute1) } - pub fn minute5(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Minute5) } - pub fn minute10(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Minute10) } - pub fn minute30(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Minute30) } - pub fn hour1(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Hour1) } - pub fn hour4(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Hour4) } - pub fn hour12(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Hour12) } - pub fn day1(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Day1) } - pub fn day3(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Day3) } - pub fn week1(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Week1) } - pub fn month1(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Month1) } - pub fn month3(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Month3) } - pub fn month6(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Month6) } - pub fn year1(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Year1) } - pub fn year10(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Year10) } + pub fn minute1(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Minute1) } + pub fn minute5(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Minute5) } + pub fn minute10(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Minute10) } + pub fn minute30(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Minute30) } + pub fn hour1(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Hour1) } + pub fn hour4(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Hour4) } + pub fn hour12(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Hour12) } + pub fn day1(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Day1) } + pub fn day3(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Day3) } + pub fn week1(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Week1) } + pub fn month1(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Month1) } + pub fn month3(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Month3) } + pub fn month6(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Month6) } + pub fn year1(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Year1) } + pub fn year10(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Year10) } pub fn halvingepoch(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::HalvingEpoch) } pub fn difficultyepoch(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::DifficultyEpoch) } pub fn height(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Height) } @@ -416,21 +453,21 @@ impl MetricPattern for MetricPattern1 { fn get(&self, pub struct MetricPattern2By { client: Arc, name: Arc, _marker: std::marker::PhantomData } impl MetricPattern2By { - pub fn minute1(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Minute1) } - pub fn minute5(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Minute5) } - pub fn minute10(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Minute10) } - pub fn minute30(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Minute30) } - pub fn hour1(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Hour1) } - pub fn hour4(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Hour4) } - pub fn hour12(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Hour12) } - pub fn day1(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Day1) } - pub fn day3(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Day3) } - pub fn week1(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Week1) } - pub fn month1(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Month1) } - pub fn month3(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Month3) } - pub fn month6(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Month6) } - pub fn year1(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Year1) } - pub fn year10(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Year10) } + pub fn minute1(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Minute1) } + pub fn minute5(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Minute5) } + pub fn minute10(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Minute10) } + pub fn minute30(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Minute30) } + pub fn hour1(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Hour1) } + pub fn hour4(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Hour4) } + pub fn hour12(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Hour12) } + pub fn day1(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Day1) } + pub fn day3(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Day3) } + pub fn week1(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Week1) } + pub fn month1(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Month1) } + pub fn month3(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Month3) } + pub fn month6(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Month6) } + pub fn year1(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Year1) } + pub fn year10(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Year10) } pub fn halvingepoch(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::HalvingEpoch) } pub fn difficultyepoch(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::DifficultyEpoch) } } @@ -446,7 +483,7 @@ impl MetricPattern for MetricPattern2 { fn get(&self, pub struct MetricPattern3By { client: Arc, name: Arc, _marker: std::marker::PhantomData } impl MetricPattern3By { - pub fn minute1(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Minute1) } + pub fn minute1(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Minute1) } } pub struct MetricPattern3 { name: Arc, pub by: MetricPattern3By } @@ -460,7 +497,7 @@ impl MetricPattern for MetricPattern3 { fn get(&self, pub struct MetricPattern4By { client: Arc, name: Arc, _marker: std::marker::PhantomData } impl MetricPattern4By { - pub fn minute5(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Minute5) } + pub fn minute5(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Minute5) } } pub struct MetricPattern4 { name: Arc, pub by: MetricPattern4By } @@ -474,7 +511,7 @@ impl MetricPattern for MetricPattern4 { fn get(&self, pub struct MetricPattern5By { client: Arc, name: Arc, _marker: std::marker::PhantomData } impl MetricPattern5By { - pub fn minute10(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Minute10) } + pub fn minute10(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Minute10) } } pub struct MetricPattern5 { name: Arc, pub by: MetricPattern5By } @@ -488,7 +525,7 @@ impl MetricPattern for MetricPattern5 { fn get(&self, pub struct MetricPattern6By { client: Arc, name: Arc, _marker: std::marker::PhantomData } impl MetricPattern6By { - pub fn minute30(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Minute30) } + pub fn minute30(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Minute30) } } pub struct MetricPattern6 { name: Arc, pub by: MetricPattern6By } @@ -502,7 +539,7 @@ impl MetricPattern for MetricPattern6 { fn get(&self, pub struct MetricPattern7By { client: Arc, name: Arc, _marker: std::marker::PhantomData } impl MetricPattern7By { - pub fn hour1(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Hour1) } + pub fn hour1(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Hour1) } } pub struct MetricPattern7 { name: Arc, pub by: MetricPattern7By } @@ -516,7 +553,7 @@ impl MetricPattern for MetricPattern7 { fn get(&self, pub struct MetricPattern8By { client: Arc, name: Arc, _marker: std::marker::PhantomData } impl MetricPattern8By { - pub fn hour4(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Hour4) } + pub fn hour4(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Hour4) } } pub struct MetricPattern8 { name: Arc, pub by: MetricPattern8By } @@ -530,7 +567,7 @@ impl MetricPattern for MetricPattern8 { fn get(&self, pub struct MetricPattern9By { client: Arc, name: Arc, _marker: std::marker::PhantomData } impl MetricPattern9By { - pub fn hour12(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Hour12) } + pub fn hour12(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Hour12) } } pub struct MetricPattern9 { name: Arc, pub by: MetricPattern9By } @@ -544,7 +581,7 @@ impl MetricPattern for MetricPattern9 { fn get(&self, pub struct MetricPattern10By { client: Arc, name: Arc, _marker: std::marker::PhantomData } impl MetricPattern10By { - pub fn day1(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Day1) } + pub fn day1(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Day1) } } pub struct MetricPattern10 { name: Arc, pub by: MetricPattern10By } @@ -558,7 +595,7 @@ impl MetricPattern for MetricPattern10 { fn get(&self pub struct MetricPattern11By { client: Arc, name: Arc, _marker: std::marker::PhantomData } impl MetricPattern11By { - pub fn day3(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Day3) } + pub fn day3(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Day3) } } pub struct MetricPattern11 { name: Arc, pub by: MetricPattern11By } @@ -572,7 +609,7 @@ impl MetricPattern for MetricPattern11 { fn get(&self pub struct MetricPattern12By { client: Arc, name: Arc, _marker: std::marker::PhantomData } impl MetricPattern12By { - pub fn week1(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Week1) } + pub fn week1(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Week1) } } pub struct MetricPattern12 { name: Arc, pub by: MetricPattern12By } @@ -586,7 +623,7 @@ impl MetricPattern for MetricPattern12 { fn get(&self pub struct MetricPattern13By { client: Arc, name: Arc, _marker: std::marker::PhantomData } impl MetricPattern13By { - pub fn month1(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Month1) } + pub fn month1(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Month1) } } pub struct MetricPattern13 { name: Arc, pub by: MetricPattern13By } @@ -600,7 +637,7 @@ impl MetricPattern for MetricPattern13 { fn get(&self pub struct MetricPattern14By { client: Arc, name: Arc, _marker: std::marker::PhantomData } impl MetricPattern14By { - pub fn month3(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Month3) } + pub fn month3(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Month3) } } pub struct MetricPattern14 { name: Arc, pub by: MetricPattern14By } @@ -614,7 +651,7 @@ impl MetricPattern for MetricPattern14 { fn get(&self pub struct MetricPattern15By { client: Arc, name: Arc, _marker: std::marker::PhantomData } impl MetricPattern15By { - pub fn month6(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Month6) } + pub fn month6(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Month6) } } pub struct MetricPattern15 { name: Arc, pub by: MetricPattern15By } @@ -628,7 +665,7 @@ impl MetricPattern for MetricPattern15 { fn get(&self pub struct MetricPattern16By { client: Arc, name: Arc, _marker: std::marker::PhantomData } impl MetricPattern16By { - pub fn year1(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Year1) } + pub fn year1(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Year1) } } pub struct MetricPattern16 { name: Arc, pub by: MetricPattern16By } @@ -642,7 +679,7 @@ impl MetricPattern for MetricPattern16 { fn get(&self pub struct MetricPattern17By { client: Arc, name: Arc, _marker: std::marker::PhantomData } impl MetricPattern17By { - pub fn year10(&self) -> MetricEndpointBuilder { _ep(&self.client, &self.name, Index::Year10) } + pub fn year10(&self) -> DateMetricEndpointBuilder { _dep(&self.client, &self.name, Index::Year10) } } pub struct MetricPattern17 { name: Arc, pub by: MetricPattern17By } @@ -6857,6 +6894,20 @@ impl BrkClient { ) } + /// Create a dynamic date-based metric endpoint builder. + /// + /// Returns `Err` if the index is not date-based. + pub fn date_metric(&self, metric: impl Into, index: Index) -> Result> { + if !index.is_date_based() { + return Err(BrkError { message: format!("{} is not a date-based index", index.name()) }); + } + Ok(DateMetricEndpointBuilder::new( + self.base.clone(), + Arc::from(metric.into().as_str()), + index, + )) + } + /// Compact OpenAPI specification /// /// Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information. Full spec available at `/openapi.json`. diff --git a/crates/brk_types/src/index.rs b/crates/brk_types/src/index.rs index 7248eb5d7..642e6df97 100644 --- a/crates/brk_types/src/index.rs +++ b/crates/brk_types/src/index.rs @@ -244,8 +244,47 @@ impl Index { Some(Timestamp::new(INDEX_EPOCH + i as u32 * interval)) } - /// Convert an index value to a date for date-based indexes. - /// Returns None for non-date-based or sub-daily indexes. + /// Convert a date to an index value for day-precision or coarser indexes. + /// Returns None for sub-daily indexes (use `timestamp_to_index` instead), + /// non-date-based indexes, or dates before genesis. + pub fn date_to_index(&self, date: Date) -> Option { + if date < Date::INDEX_ZERO { + return None; + } + match self { + Self::Day1 => Day1::try_from(date).ok().map(usize::from), + Self::Day3 => Some(usize::from(Day3::from_timestamp(Timestamp::from(date)))), + Self::Week1 => Some(usize::from(Week1::from(date))), + Self::Month1 => Some(usize::from(Month1::from(date))), + Self::Month3 => Some(usize::from(Month3::from(Month1::from(date)))), + Self::Month6 => Some(usize::from(Month6::from(Month1::from(date)))), + Self::Year1 => Some(usize::from(Year1::from(date))), + Self::Year10 => Some(usize::from(Year10::from(date))), + _ => None, + } + } + + /// Convert a timestamp to an index value for any date-based index. + /// Works for both sub-daily (minute, hour) and daily+ indexes. + /// Returns None for non-date-based indexes. + pub fn timestamp_to_index(&self, ts: Timestamp) -> Option { + let interval = match self { + Self::Minute1 => MINUTE1_INTERVAL, + Self::Minute5 => MINUTE5_INTERVAL, + Self::Minute10 => MINUTE10_INTERVAL, + Self::Minute30 => MINUTE30_INTERVAL, + Self::Hour1 => HOUR1_INTERVAL, + Self::Hour4 => HOUR4_INTERVAL, + Self::Hour12 => HOUR12_INTERVAL, + Self::Day3 => DAY3_INTERVAL, + _ => return self.date_to_index(Date::from(ts)), + }; + Some(((*ts - INDEX_EPOCH) / interval) as usize) + } + + /// Convert an index value to a date for day-precision or coarser indexes. + /// Returns None for sub-daily indexes (use `index_to_timestamp` instead) + /// and non-date-based indexes. pub fn index_to_date(&self, i: usize) -> Option { match self { Self::Day1 => Some(Date::from(Day1::from(i))), @@ -401,4 +440,69 @@ mod tests { fn test_index_to_date_txindex_returns_none() { assert!(Index::TxIndex.index_to_date(100).is_none()); } + + #[test] + fn test_date_to_index_day1_genesis() { + assert_eq!(Index::Day1.date_to_index(Date::INDEX_ZERO), Some(0)); + } + + #[test] + fn test_date_to_index_day1_one() { + assert_eq!(Index::Day1.date_to_index(Date::INDEX_ONE), Some(1)); + } + + #[test] + fn test_date_to_index_roundtrip_day1() { + let date = Index::Day1.index_to_date(100).unwrap(); + assert_eq!(Index::Day1.date_to_index(date), Some(100)); + } + + #[test] + fn test_date_to_index_roundtrip_week1() { + let date = Index::Week1.index_to_date(50).unwrap(); + assert_eq!(Index::Week1.date_to_index(date), Some(50)); + } + + #[test] + fn test_date_to_index_roundtrip_month1() { + let date = Index::Month1.index_to_date(24).unwrap(); + assert_eq!(Index::Month1.date_to_index(date), Some(24)); + } + + #[test] + fn test_date_to_index_roundtrip_year1() { + let date = Index::Year1.index_to_date(5).unwrap(); + assert_eq!(Index::Year1.date_to_index(date), Some(5)); + } + + #[test] + fn test_date_to_index_roundtrip_month3() { + let date = Index::Month3.index_to_date(4).unwrap(); + assert_eq!(Index::Month3.date_to_index(date), Some(4)); + } + + #[test] + fn test_date_to_index_roundtrip_month6() { + let date = Index::Month6.index_to_date(2).unwrap(); + assert_eq!(Index::Month6.date_to_index(date), Some(2)); + } + + #[test] + fn test_date_to_index_roundtrip_year10() { + let date = Index::Year10.index_to_date(1).unwrap(); + assert_eq!(Index::Year10.date_to_index(date), Some(1)); + } + + #[test] + fn test_date_to_index_pre_genesis_returns_none() { + let pre_genesis = Date::new(2009, 1, 2); + assert!(Index::Day1.date_to_index(pre_genesis).is_none()); + assert!(Index::Week1.date_to_index(pre_genesis).is_none()); + assert!(Index::Month1.date_to_index(pre_genesis).is_none()); + } + + #[test] + fn test_date_to_index_height_returns_none() { + assert!(Index::Height.date_to_index(Date::INDEX_ZERO).is_none()); + } } diff --git a/crates/brk_types/src/metricdata.rs b/crates/brk_types/src/metricdata.rs index 1fee780d2..6b3057cda 100644 --- a/crates/brk_types/src/metricdata.rs +++ b/crates/brk_types/src/metricdata.rs @@ -1,11 +1,11 @@ -use std::io::Write; +use std::{io::Write, ops::Deref}; use schemars::JsonSchema; -use serde::Deserialize; +use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; use vecdb::AnySerializableVec; -use super::{Index, Timestamp, Version}; +use super::{Date, Index, Timestamp, Version}; /// Metric data with range information. /// @@ -67,14 +67,27 @@ impl MetricData { } /// Returns an iterator over dates for the index range. - /// Panics if the index is not date-based. - pub fn dates(&self) -> impl Iterator + '_ { + /// Returns `None` for non-date-based and sub-daily indexes (use `timestamps()` instead). + pub fn dates(&self) -> Option + '_> { + // Check first index to verify date conversion works (sub-daily returns None) + self.index.index_to_date(self.start)?; let index = self.index; - self.indexes().map(move |i| { - index - .index_to_date(i) - .expect("dates() called on non-date-based index") - }) + Some(self.indexes().map(move |i| { + index.index_to_date(i).unwrap() + })) + } + + /// Returns an iterator over timestamps for the index range. + /// Works for all date-based indexes including sub-daily. + /// Returns `None` for non-date-based indexes. + pub fn timestamps(&self) -> Option + '_> { + if !self.is_date_based() { + return None; + } + let index = self.index; + Some(self.indexes().map(move |i| { + index.index_to_timestamp(i).unwrap() + })) } /// Iterate over (index, &value) pairs. @@ -83,9 +96,85 @@ impl MetricData { } /// Iterate over (date, &value) pairs. - /// Panics if the index is not date-based. - pub fn iter_dates(&self) -> impl Iterator + '_ { - self.dates().zip(self.data.iter()) + /// Returns `None` for non-date-based and sub-daily indexes (use `iter_timestamps()` instead). + pub fn iter_dates(&self) -> Option + '_> { + Some(self.dates()?.zip(self.data.iter())) + } + + /// Iterate over (timestamp, &value) pairs. + /// Works for all date-based indexes including sub-daily. + /// Returns `None` for non-date-based indexes. + pub fn iter_timestamps(&self) -> Option + '_> { + Some(self.timestamps()?.zip(self.data.iter())) + } +} + +/// Metric data that is guaranteed to use a date-based index. +/// +/// This is a newtype around `MetricData` that guarantees `is_date_based()` is true, +/// making date methods infallible. +#[derive(Debug)] +pub struct DateMetricData(MetricData); + +impl DateMetricData { + /// Create a `DateMetricData` from a `MetricData`, returning `Err` if the index is not date-based. + pub fn try_new(inner: MetricData) -> Result> { + if inner.is_date_based() { + Ok(Self(inner)) + } else { + Err(inner) + } + } + + /// Consume and return the inner `MetricData`. + pub fn into_inner(self) -> MetricData { + self.0 + } + + /// Returns an iterator over dates for the index range. + /// Returns `None` for sub-daily indexes (use `timestamps()` instead). + pub fn dates(&self) -> Option + '_> { + self.0.dates() + } + + /// Iterate over (date, &value) pairs. + /// Returns `None` for sub-daily indexes (use `iter_timestamps()` instead). + pub fn iter_dates(&self) -> Option + '_> { + self.0.iter_dates() + } + + /// Returns an iterator over timestamps for the index range (infallible). + /// Works for all date-based indexes including sub-daily. + pub fn timestamps(&self) -> impl Iterator + '_ { + self.0.timestamps().expect("DateMetricData is always date-based") + } + + /// Iterate over (timestamp, &value) pairs (infallible). + /// Works for all date-based indexes including sub-daily. + pub fn iter_timestamps(&self) -> impl Iterator + '_ { + self.0.iter_timestamps().expect("DateMetricData is always date-based") + } +} + +impl Deref for DateMetricData { + type Target = MetricData; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'de, T: DeserializeOwned> Deserialize<'de> for DateMetricData { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let inner = MetricData::::deserialize(deserializer)?; + Self::try_new(inner).map_err(|m| { + serde::de::Error::custom(format!( + "expected date-based index, got {:?}", + m.index + )) + }) } } @@ -146,7 +235,7 @@ mod tests { #[test] fn test_dates_for_day1() { let metric = date_based_metric(); - let dates: Vec<_> = metric.dates().collect(); + let dates: Vec<_> = metric.dates().unwrap().collect(); assert_eq!(dates.len(), 5); // Day1 0 = Jan 3, 2009 (genesis) assert_eq!(dates[0].year(), 2009); @@ -180,7 +269,7 @@ mod tests { #[test] fn test_iter_dates() { let metric = date_based_metric(); - let pairs: Vec<_> = metric.iter_dates().collect(); + let pairs: Vec<_> = metric.iter_dates().unwrap().collect(); assert_eq!(pairs.len(), 5); // First pair: (Jan 3 2009, 100) assert_eq!(pairs[0].0.year(), 2009); @@ -193,16 +282,271 @@ mod tests { } #[test] - #[should_panic(expected = "dates() called on non-date-based index")] - fn test_dates_panics_for_non_date_index() { + fn test_dates_returns_none_for_non_date_index() { let metric = height_based_metric(); - let _: Vec<_> = metric.dates().collect(); + assert!(metric.dates().is_none()); } #[test] - #[should_panic(expected = "dates() called on non-date-based index")] - fn test_iter_dates_panics_for_non_date_index() { + fn test_iter_dates_returns_none_for_non_date_index() { let metric = height_based_metric(); - let _: Vec<_> = metric.iter_dates().collect(); + assert!(metric.iter_dates().is_none()); + } + + #[test] + fn test_date_metric_data_try_new_ok() { + let metric = date_based_metric(); + let date_metric = DateMetricData::try_new(metric).unwrap(); + assert_eq!(date_metric.data.len(), 5); + let dates: Vec<_> = date_metric.dates().unwrap().collect(); + assert_eq!(dates.len(), 5); + assert_eq!(dates[0].year(), 2009); + } + + #[test] + fn test_date_metric_data_try_new_err() { + let metric = height_based_metric(); + assert!(DateMetricData::try_new(metric).is_err()); + } + + #[test] + fn test_date_metric_data_iter_dates() { + let metric = date_based_metric(); + let date_metric = DateMetricData::try_new(metric).unwrap(); + let pairs: Vec<_> = date_metric.iter_dates().unwrap().collect(); + assert_eq!(pairs.len(), 5); + assert_eq!(pairs[0].0.day(), 3); + assert_eq!(pairs[0].1, &100); + } + + #[test] + fn test_date_metric_data_deref() { + let metric = date_based_metric(); + let date_metric = DateMetricData::try_new(metric).unwrap(); + // Access MetricData methods via Deref + assert!(date_metric.is_date_based()); + assert_eq!(date_metric.indexes().count(), 5); + } + + // Sub-daily tests + + fn sub_daily_metric() -> MetricData { + MetricData { + version: Version::ONE, + index: Index::Hour1, + total: 200000, + start: 0, + end: 3, + stamp: "2024-01-01T00:00:00Z".to_string(), + data: vec![10.0, 20.0, 30.0], + } + } + + #[test] + fn test_sub_daily_is_date_based() { + let metric = sub_daily_metric(); + assert!(metric.is_date_based()); + } + + #[test] + fn test_sub_daily_dates_returns_none() { + let metric = sub_daily_metric(); + assert!(metric.dates().is_none()); + } + + #[test] + fn test_sub_daily_timestamps_returns_some() { + let metric = sub_daily_metric(); + let ts: Vec<_> = metric.timestamps().unwrap().collect(); + assert_eq!(ts.len(), 3); + // Hour1 index 0 = INDEX_EPOCH (2009-01-01 00:00:00 UTC) + assert_eq!(*ts[0], 1230768000); + // Hour1 index 1 = INDEX_EPOCH + 3600 + assert_eq!(*ts[1], 1230768000 + 3600); + } + + #[test] + fn test_sub_daily_iter_timestamps() { + let metric = sub_daily_metric(); + let pairs: Vec<_> = metric.iter_timestamps().unwrap().collect(); + assert_eq!(pairs.len(), 3); + assert_eq!(*pairs[0].0, 1230768000); + assert_eq!(pairs[0].1, &10.0); + } + + #[test] + fn test_date_metric_data_sub_daily_timestamps() { + let metric = sub_daily_metric(); + let date_metric = DateMetricData::try_new(metric).unwrap(); + // dates() returns None for sub-daily + assert!(date_metric.dates().is_none()); + // timestamps() works for all date-based + let ts: Vec<_> = date_metric.timestamps().collect(); + assert_eq!(ts.len(), 3); + } + + #[test] + fn test_date_metric_data_iter_timestamps() { + let metric = sub_daily_metric(); + let date_metric = DateMetricData::try_new(metric).unwrap(); + let pairs: Vec<_> = date_metric.iter_timestamps().collect(); + assert_eq!(pairs.len(), 3); + assert_eq!(pairs[2].1, &30.0); + } + + #[test] + fn test_day1_timestamps_also_works() { + // timestamps() works for daily indexes too + let metric = date_based_metric(); + let ts: Vec<_> = metric.timestamps().unwrap().collect(); + assert_eq!(ts.len(), 5); + } + + // Empty data + + fn empty_metric() -> MetricData { + MetricData { + version: Version::ONE, + index: Index::Day1, + total: 100, + start: 5, + end: 5, + stamp: "2024-01-01T00:00:00Z".to_string(), + data: vec![], + } + } + + #[test] + fn test_empty_indexes() { + let metric = empty_metric(); + assert_eq!(metric.indexes().count(), 0); + } + + #[test] + fn test_empty_iter() { + let metric = empty_metric(); + assert_eq!(metric.iter().count(), 0); + } + + #[test] + fn test_empty_dates() { + let metric = empty_metric(); + assert_eq!(metric.dates().unwrap().count(), 0); + } + + #[test] + fn test_empty_timestamps() { + let metric = empty_metric(); + assert_eq!(metric.timestamps().unwrap().count(), 0); + } + + // Non-date timestamps/iter_timestamps + + #[test] + fn test_timestamps_returns_none_for_non_date() { + let metric = height_based_metric(); + assert!(metric.timestamps().is_none()); + } + + #[test] + fn test_iter_timestamps_returns_none_for_non_date() { + let metric = height_based_metric(); + assert!(metric.iter_timestamps().is_none()); + } + + // DateMetricData sub-daily iter_dates returns None + + #[test] + fn test_date_metric_data_sub_daily_iter_dates_returns_none() { + let metric = sub_daily_metric(); + let date_metric = DateMetricData::try_new(metric).unwrap(); + assert!(date_metric.iter_dates().is_none()); + } + + // Month1 dates + + fn month1_metric() -> MetricData { + MetricData { + version: Version::ONE, + index: Index::Month1, + total: 200, + start: 0, + end: 3, + stamp: "2024-01-01T00:00:00Z".to_string(), + data: vec![1000, 2000, 3000], + } + } + + #[test] + fn test_dates_for_month1() { + let metric = month1_metric(); + let dates: Vec<_> = metric.dates().unwrap().collect(); + assert_eq!(dates.len(), 3); + assert_eq!(dates[0].year(), 2009); + assert_eq!(dates[0].month(), 1); + assert_eq!(dates[0].day(), 1); + assert_eq!(dates[1].month(), 2); + assert_eq!(dates[2].month(), 3); + } + + #[test] + fn test_timestamps_for_month1() { + let metric = month1_metric(); + let ts: Vec<_> = metric.timestamps().unwrap().collect(); + assert_eq!(ts.len(), 3); + // Each should be a valid timestamp + assert!(*ts[0] > 0); + assert!(*ts[1] > *ts[0]); + assert!(*ts[2] > *ts[1]); + } + + // Deserialize roundtrip + + #[test] + fn test_date_metric_data_deserialize_valid() { + let json = r#"{"version":1,"index":"day1","total":100,"start":0,"end":2,"stamp":"2024-01-01T00:00:00Z","data":[1,2]}"#; + let result: Result, _> = serde_json::from_str(json); + assert!(result.is_ok()); + let dm = result.unwrap(); + assert_eq!(dm.data.len(), 2); + } + + #[test] + fn test_date_metric_data_deserialize_rejects_non_date() { + let json = r#"{"version":1,"index":"height","total":100,"start":0,"end":2,"stamp":"2024-01-01T00:00:00Z","data":[1,2]}"#; + let result: Result, _> = serde_json::from_str(json); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("date-based"), "error should mention date-based: {}", err); + } + + // timestamp_to_index tests + + #[test] + fn test_timestamp_to_index_hour1() { + // INDEX_EPOCH + 2 hours + let ts = Timestamp::new(1230768000 + 7200); + assert_eq!(Index::Hour1.timestamp_to_index(ts), Some(2)); + } + + #[test] + fn test_timestamp_to_index_minute5() { + // INDEX_EPOCH + 15 minutes (= 3 * 5min intervals) + let ts = Timestamp::new(1230768000 + 900); + assert_eq!(Index::Minute5.timestamp_to_index(ts), Some(3)); + } + + #[test] + fn test_timestamp_to_index_non_date_returns_none() { + let ts = Timestamp::new(1230768000); + assert!(Index::Height.timestamp_to_index(ts).is_none()); + } + + #[test] + fn test_timestamp_to_index_day1_via_date_fallback() { + // Day1 goes through date_to_index fallback + // 2009-01-09 = Day1 index 1 + let ts = Timestamp::from(Date::new(2009, 1, 9)); + assert_eq!(Index::Day1.timestamp_to_index(ts), Some(1)); } } diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index 7ae02799b..d2d3f5bf1 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -998,20 +998,14 @@ function dateToIndex(index, d) { * Wrap raw metric data with helper methods. * @template T * @param {MetricData} raw - Raw JSON response - * @returns {MetricData} + * @returns {DateMetricData} */ function _wrapMetricData(raw) { const { index, start, end, data } = raw; const _dateBased = _DATE_INDEXES.has(index); - return /** @type {MetricData} */ ({ + return /** @type {DateMetricData} */ ({ ...raw, isDateBased: _dateBased, - dates() { - /** @type {globalThis.Date[]} */ - const result = []; - for (let i = start; i < end; i++) result.push(indexToDate(index, i)); - return result; - }, indexes() { /** @type {number[]} */ const result = []; @@ -1019,41 +1013,48 @@ function _wrapMetricData(raw) { return result; }, keys() { - return _dateBased ? this.dates() : this.indexes(); + return this.indexes(); }, entries() { - /** @type {Array<[globalThis.Date | number, T]>} */ + /** @type {Array<[number, T]>} */ const result = []; - if (_dateBased) { - for (let i = 0; i < data.length; i++) result.push([indexToDate(index, start + i), data[i]]); - } else { - for (let i = 0; i < data.length; i++) result.push([start + i, data[i]]); - } + for (let i = 0; i < data.length; i++) result.push([start + i, data[i]]); return result; }, toMap() { - /** @type {Map} */ + /** @type {Map} */ const map = new Map(); - if (_dateBased) { - for (let i = 0; i < data.length; i++) map.set(indexToDate(index, start + i), data[i]); - } else { - for (let i = 0; i < data.length; i++) map.set(start + i, data[i]); - } + for (let i = 0; i < data.length; i++) map.set(start + i, data[i]); return map; }, *[Symbol.iterator]() { - if (_dateBased) { - for (let i = 0; i < data.length; i++) yield [indexToDate(index, start + i), data[i]]; - } else { - for (let i = 0; i < data.length; i++) yield [start + i, data[i]]; - } + for (let i = 0; i < data.length; i++) yield /** @type {[number, T]} */ ([start + i, data[i]]); + }, + // DateMetricData methods (only meaningful for date-based indexes) + dates() { + /** @type {globalThis.Date[]} */ + const result = []; + for (let i = start; i < end; i++) result.push(indexToDate(index, i)); + return result; + }, + dateEntries() { + /** @type {Array<[globalThis.Date, T]>} */ + const result = []; + for (let i = 0; i < data.length; i++) result.push([indexToDate(index, start + i), data[i]]); + return result; + }, + toDateMap() { + /** @type {Map} */ + const map = new Map(); + for (let i = 0; i < data.length; i++) map.set(indexToDate(index, start + i), data[i]); + return map; }, }); } /** * @template T - * @typedef {Object} MetricData + * @typedef {Object} MetricDataBase * @property {number} version - Version of the metric data * @property {Index} index - The index type used for this query * @property {number} total - Total number of data points @@ -1062,26 +1063,33 @@ function _wrapMetricData(raw) { * @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 {() => (globalThis.Date[] | number[])} keys - Get keys (dates for date-based, index numbers otherwise) - * @property {() => Array<[globalThis.Date | number, T]>} entries - Get [key, value] pairs (dates for date-based, index numbers otherwise) - * @property {() => Map} toMap - Return data as Map (dates for date-based, index numbers otherwise) - * @property {() => globalThis.Date[]} dates - Get dates (date-based indexes only, throws otherwise) * @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} toMap - Convert to Map */ + +/** @template T @typedef {MetricDataBase & Iterable<[number, T]>} MetricData */ + +/** + * @template T + * @typedef {Object} DateMetricDataExtras + * @property {() => globalThis.Date[]} dates - Get dates for each data point + * @property {() => Array<[globalThis.Date, T]>} dateEntries - Get [date, value] pairs + * @property {() => Map} toDateMap - Convert to Map + */ + +/** @template T @typedef {MetricData & DateMetricDataExtras} DateMetricData */ /** @typedef {MetricData} AnyMetricData */ -/** - * Thenable interface for await support. - * @template T - * @typedef {(onfulfilled?: (value: MetricData) => MetricData, onrejected?: (reason: Error) => never) => Promise>} Thenable - */ +/** @template T @typedef {(onfulfilled?: (value: MetricData) => any, onrejected?: (reason: Error) => never) => Promise>} Thenable */ +/** @template T @typedef {(onfulfilled?: (value: DateMetricData) => any, onrejected?: (reason: Error) => never) => Promise>} DateThenable */ /** - * Metric endpoint builder. Callable (returns itself) so both .by.day1 and .by.day1() work. * @template T * @typedef {Object} MetricEndpointBuilder * @property {(index: number) => SingleItemBuilder} get - Get single item at index - * @property {(start?: number | globalThis.Date, end?: number | globalThis.Date) => RangeBuilder} slice - Slice by index or Date + * @property {(start?: number, end?: number) => RangeBuilder} slice - Slice by index * @property {(n: number) => RangeBuilder} first - Get first n items * @property {(n: number) => RangeBuilder} last - Get last n items * @property {(n: number) => SkippedBuilder} skip - Skip first n items, chain with take() @@ -1090,38 +1098,66 @@ function _wrapMetricData(raw) { * @property {Thenable} then - Thenable (await endpoint) * @property {string} path - The endpoint path */ -/** @typedef {MetricEndpointBuilder} AnyMetricEndpointBuilder */ /** * @template T - * @typedef {Object} SingleItemBuilder + * @typedef {Object} DateMetricEndpointBuilder + * @property {(index: number | globalThis.Date) => DateSingleItemBuilder} get - Get single item at index or Date + * @property {(start?: number | globalThis.Date, end?: number | globalThis.Date) => DateRangeBuilder} slice - Slice by index or Date + * @property {(n: number) => DateRangeBuilder} first - Get first n items + * @property {(n: number) => DateRangeBuilder} last - Get last n items + * @property {(n: number) => DateSkippedBuilder} skip - Skip first n items, chain with take() + * @property {(onUpdate?: (value: DateMetricData) => void) => Promise>} fetch - Fetch all data + * @property {() => Promise} fetchCsv - Fetch all data as CSV + * @property {DateThenable} then - Thenable (await endpoint) + * @property {string} path - The endpoint path + */ + +/** @typedef {MetricEndpointBuilder} AnyMetricEndpointBuilder */ + +/** @template T @typedef {Object} SingleItemBuilder * @property {(onUpdate?: (value: MetricData) => void) => Promise>} fetch - Fetch the item * @property {() => Promise} fetchCsv - Fetch as CSV * @property {Thenable} then - Thenable */ -/** - * @template T - * @typedef {Object} SkippedBuilder +/** @template T @typedef {Object} DateSingleItemBuilder + * @property {(onUpdate?: (value: DateMetricData) => void) => Promise>} fetch - Fetch the item + * @property {() => Promise} fetchCsv - Fetch as CSV + * @property {DateThenable} then - Thenable + */ + +/** @template T @typedef {Object} SkippedBuilder * @property {(n: number) => RangeBuilder} take - Take n items after skipped position * @property {(onUpdate?: (value: MetricData) => void) => Promise>} fetch - Fetch from skipped position to end * @property {() => Promise} fetchCsv - Fetch as CSV * @property {Thenable} then - Thenable */ -/** - * @template T - * @typedef {Object} RangeBuilder +/** @template T @typedef {Object} DateSkippedBuilder + * @property {(n: number) => DateRangeBuilder} take - Take n items after skipped position + * @property {(onUpdate?: (value: DateMetricData) => void) => Promise>} fetch - Fetch from skipped position to end + * @property {() => Promise} fetchCsv - Fetch as CSV + * @property {DateThenable} then - Thenable + */ + +/** @template T @typedef {Object} RangeBuilder * @property {(onUpdate?: (value: MetricData) => void) => Promise>} fetch - Fetch the range * @property {() => Promise} fetchCsv - Fetch as CSV * @property {Thenable} then - Thenable */ +/** @template T @typedef {Object} DateRangeBuilder + * @property {(onUpdate?: (value: DateMetricData) => void) => Promise>} fetch - Fetch the range + * @property {() => Promise} fetchCsv - Fetch as CSV + * @property {DateThenable} then - Thenable + */ + /** * @template T * @typedef {Object} MetricPattern * @property {string} name - The metric name - * @property {Readonly>>>} by - Index endpoints as lazy getters. Access via .by.day1 or .by['day1'] + * @property {Readonly>>>} by - Index endpoints as lazy getters * @property {() => readonly Index[]} indexes - Get the list of available indexes * @property {(index: Index) => MetricEndpointBuilder|undefined} get - Get an endpoint for a specific index */ @@ -1134,7 +1170,7 @@ function _wrapMetricData(raw) { * @param {BrkClientBase} client * @param {string} name - The metric vec name * @param {Index} index - The index name - * @returns {MetricEndpointBuilder} + * @returns {DateMetricEndpointBuilder} */ function _endpoint(client, name, index) { const p = `/api/metric/${name}/${index}`; @@ -1157,7 +1193,7 @@ function _endpoint(client, name, index) { /** * @param {number} [start] * @param {number} [end] - * @returns {RangeBuilder} + * @returns {DateRangeBuilder} */ const rangeBuilder = (start, end) => ({ fetch(onUpdate) { return client._fetchMetricData(buildPath(start, end), onUpdate); }, @@ -1167,7 +1203,7 @@ function _endpoint(client, name, index) { /** * @param {number} idx - * @returns {SingleItemBuilder} + * @returns {DateSingleItemBuilder} */ const singleItemBuilder = (idx) => ({ fetch(onUpdate) { return client._fetchMetricData(buildPath(idx, idx + 1), onUpdate); }, @@ -1177,7 +1213,7 @@ function _endpoint(client, name, index) { /** * @param {number} start - * @returns {SkippedBuilder} + * @returns {DateSkippedBuilder} */ const skippedBuilder = (start) => ({ take(n) { return rangeBuilder(start, start + n); }, @@ -1186,9 +1222,9 @@ function _endpoint(client, name, index) { then(resolve, reject) { return this.fetch().then(resolve, reject); }, }); - /** @type {MetricEndpointBuilder} */ + /** @type {DateMetricEndpointBuilder} */ const endpoint = { - get(idx) { return singleItemBuilder(idx); }, + get(idx) { if (idx instanceof Date) idx = dateToIndex(index, idx); return singleItemBuilder(idx); }, slice(start, end) { if (start instanceof Date) start = dateToIndex(index, start); if (end instanceof Date) end = dateToIndex(index, end); @@ -1215,7 +1251,8 @@ class BrkClientBase { */ constructor(options) { const isString = typeof options === 'string'; - this.baseUrl = isString ? options : options.baseUrl; + const rawUrl = isString ? options : options.baseUrl; + this.baseUrl = rawUrl.endsWith('/') ? rawUrl.slice(0, -1) : rawUrl; this.timeout = isString ? 5000 : (options.timeout ?? 5000); /** @type {Promise} */ this._cachePromise = _openCache(isString ? undefined : options.cache); @@ -1229,8 +1266,7 @@ class BrkClientBase { * @returns {Promise} */ async get(path) { - const base = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl; - const url = `${base}${path}`; + const url = `${this.baseUrl}${path}`; const res = await fetch(url, { signal: AbortSignal.timeout(this.timeout) }); if (!res.ok) throw new BrkError(`HTTP ${res.status}: ${url}`, res.status); return res; @@ -1244,8 +1280,7 @@ class BrkClientBase { * @returns {Promise} */ async getJson(path, onUpdate) { - const base = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl; - const url = `${base}${path}`; + const url = `${this.baseUrl}${path}`; const cache = this._cache ?? await this._cachePromise; let resolved = false; @@ -1307,8 +1342,8 @@ class BrkClientBase { * Fetch metric data and wrap with helper methods (internal) * @template T * @param {string} path - * @param {(value: MetricData) => void} [onUpdate] - * @returns {Promise>} + * @param {(value: DateMetricData) => void} [onUpdate] + * @returns {Promise>} */ async _fetchMetricData(path, onUpdate) { const wrappedOnUpdate = onUpdate ? (/** @type {MetricData} */ raw) => onUpdate(_wrapMetricData(raw)) : undefined; @@ -1382,7 +1417,7 @@ const _i37 = /** @type {const} */ (["emptyaddressindex"]); * @param {readonly Index[]} indexes - The supported indexes */ function _mp(client, name, indexes) { - const by = /** @type {any} */ ({}); + const by = {}; for (const idx of indexes) { Object.defineProperty(by, idx, { get() { return _endpoint(client, name, idx); }, @@ -1393,123 +1428,124 @@ function _mp(client, name, indexes) { return { name, by, + /** @returns {readonly Index[]} */ indexes() { return indexes; }, - /** @param {Index} index */ + /** @param {Index} index @returns {MetricEndpointBuilder|undefined} */ get(index) { return indexes.includes(index) ? _endpoint(client, name, index) : undefined; } }; } -/** @template T @typedef {{ name: string, by: { readonly minute1: MetricEndpointBuilder, readonly minute5: MetricEndpointBuilder, readonly minute10: MetricEndpointBuilder, readonly minute30: MetricEndpointBuilder, readonly hour1: MetricEndpointBuilder, readonly hour4: MetricEndpointBuilder, readonly hour12: MetricEndpointBuilder, readonly day1: MetricEndpointBuilder, readonly day3: MetricEndpointBuilder, readonly week1: MetricEndpointBuilder, readonly month1: MetricEndpointBuilder, readonly month3: MetricEndpointBuilder, readonly month6: MetricEndpointBuilder, readonly year1: MetricEndpointBuilder, readonly year10: MetricEndpointBuilder, readonly halvingepoch: MetricEndpointBuilder, readonly difficultyepoch: MetricEndpointBuilder, readonly height: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern1 */ +/** @template T @typedef {{ name: string, by: { readonly minute1: DateMetricEndpointBuilder, readonly minute5: DateMetricEndpointBuilder, readonly minute10: DateMetricEndpointBuilder, readonly minute30: DateMetricEndpointBuilder, readonly hour1: DateMetricEndpointBuilder, readonly hour4: DateMetricEndpointBuilder, readonly hour12: DateMetricEndpointBuilder, readonly day1: DateMetricEndpointBuilder, readonly day3: DateMetricEndpointBuilder, readonly week1: DateMetricEndpointBuilder, readonly month1: DateMetricEndpointBuilder, readonly month3: DateMetricEndpointBuilder, readonly month6: DateMetricEndpointBuilder, readonly year1: DateMetricEndpointBuilder, readonly year10: DateMetricEndpointBuilder, readonly halvingepoch: MetricEndpointBuilder, readonly difficultyepoch: MetricEndpointBuilder, readonly height: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern1 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern1} */ -function createMetricPattern1(client, name) { return _mp(client, name, _i1); } -/** @template T @typedef {{ name: string, by: { readonly minute1: MetricEndpointBuilder, readonly minute5: MetricEndpointBuilder, readonly minute10: MetricEndpointBuilder, readonly minute30: MetricEndpointBuilder, readonly hour1: MetricEndpointBuilder, readonly hour4: MetricEndpointBuilder, readonly hour12: MetricEndpointBuilder, readonly day1: MetricEndpointBuilder, readonly day3: MetricEndpointBuilder, readonly week1: MetricEndpointBuilder, readonly month1: MetricEndpointBuilder, readonly month3: MetricEndpointBuilder, readonly month6: MetricEndpointBuilder, readonly year1: MetricEndpointBuilder, readonly year10: MetricEndpointBuilder, readonly halvingepoch: MetricEndpointBuilder, readonly difficultyepoch: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern2 */ +function createMetricPattern1(client, name) { return /** @type {MetricPattern1} */ (_mp(client, name, _i1)); } +/** @template T @typedef {{ name: string, by: { readonly minute1: DateMetricEndpointBuilder, readonly minute5: DateMetricEndpointBuilder, readonly minute10: DateMetricEndpointBuilder, readonly minute30: DateMetricEndpointBuilder, readonly hour1: DateMetricEndpointBuilder, readonly hour4: DateMetricEndpointBuilder, readonly hour12: DateMetricEndpointBuilder, readonly day1: DateMetricEndpointBuilder, readonly day3: DateMetricEndpointBuilder, readonly week1: DateMetricEndpointBuilder, readonly month1: DateMetricEndpointBuilder, readonly month3: DateMetricEndpointBuilder, readonly month6: DateMetricEndpointBuilder, readonly year1: DateMetricEndpointBuilder, readonly year10: DateMetricEndpointBuilder, readonly halvingepoch: MetricEndpointBuilder, readonly difficultyepoch: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern2 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern2} */ -function createMetricPattern2(client, name) { return _mp(client, name, _i2); } -/** @template T @typedef {{ name: string, by: { readonly minute1: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern3 */ +function createMetricPattern2(client, name) { return /** @type {MetricPattern2} */ (_mp(client, name, _i2)); } +/** @template T @typedef {{ name: string, by: { readonly minute1: DateMetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern3 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern3} */ -function createMetricPattern3(client, name) { return _mp(client, name, _i3); } -/** @template T @typedef {{ name: string, by: { readonly minute5: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern4 */ +function createMetricPattern3(client, name) { return /** @type {MetricPattern3} */ (_mp(client, name, _i3)); } +/** @template T @typedef {{ name: string, by: { readonly minute5: DateMetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern4 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern4} */ -function createMetricPattern4(client, name) { return _mp(client, name, _i4); } -/** @template T @typedef {{ name: string, by: { readonly minute10: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern5 */ +function createMetricPattern4(client, name) { return /** @type {MetricPattern4} */ (_mp(client, name, _i4)); } +/** @template T @typedef {{ name: string, by: { readonly minute10: DateMetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern5 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern5} */ -function createMetricPattern5(client, name) { return _mp(client, name, _i5); } -/** @template T @typedef {{ name: string, by: { readonly minute30: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern6 */ +function createMetricPattern5(client, name) { return /** @type {MetricPattern5} */ (_mp(client, name, _i5)); } +/** @template T @typedef {{ name: string, by: { readonly minute30: DateMetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern6 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern6} */ -function createMetricPattern6(client, name) { return _mp(client, name, _i6); } -/** @template T @typedef {{ name: string, by: { readonly hour1: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern7 */ +function createMetricPattern6(client, name) { return /** @type {MetricPattern6} */ (_mp(client, name, _i6)); } +/** @template T @typedef {{ name: string, by: { readonly hour1: DateMetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern7 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern7} */ -function createMetricPattern7(client, name) { return _mp(client, name, _i7); } -/** @template T @typedef {{ name: string, by: { readonly hour4: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern8 */ +function createMetricPattern7(client, name) { return /** @type {MetricPattern7} */ (_mp(client, name, _i7)); } +/** @template T @typedef {{ name: string, by: { readonly hour4: DateMetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern8 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern8} */ -function createMetricPattern8(client, name) { return _mp(client, name, _i8); } -/** @template T @typedef {{ name: string, by: { readonly hour12: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern9 */ +function createMetricPattern8(client, name) { return /** @type {MetricPattern8} */ (_mp(client, name, _i8)); } +/** @template T @typedef {{ name: string, by: { readonly hour12: DateMetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern9 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern9} */ -function createMetricPattern9(client, name) { return _mp(client, name, _i9); } -/** @template T @typedef {{ name: string, by: { readonly day1: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern10 */ +function createMetricPattern9(client, name) { return /** @type {MetricPattern9} */ (_mp(client, name, _i9)); } +/** @template T @typedef {{ name: string, by: { readonly day1: DateMetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern10 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern10} */ -function createMetricPattern10(client, name) { return _mp(client, name, _i10); } -/** @template T @typedef {{ name: string, by: { readonly day3: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern11 */ +function createMetricPattern10(client, name) { return /** @type {MetricPattern10} */ (_mp(client, name, _i10)); } +/** @template T @typedef {{ name: string, by: { readonly day3: DateMetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern11 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern11} */ -function createMetricPattern11(client, name) { return _mp(client, name, _i11); } -/** @template T @typedef {{ name: string, by: { readonly week1: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern12 */ +function createMetricPattern11(client, name) { return /** @type {MetricPattern11} */ (_mp(client, name, _i11)); } +/** @template T @typedef {{ name: string, by: { readonly week1: DateMetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern12 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern12} */ -function createMetricPattern12(client, name) { return _mp(client, name, _i12); } -/** @template T @typedef {{ name: string, by: { readonly month1: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern13 */ +function createMetricPattern12(client, name) { return /** @type {MetricPattern12} */ (_mp(client, name, _i12)); } +/** @template T @typedef {{ name: string, by: { readonly month1: DateMetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern13 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern13} */ -function createMetricPattern13(client, name) { return _mp(client, name, _i13); } -/** @template T @typedef {{ name: string, by: { readonly month3: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern14 */ +function createMetricPattern13(client, name) { return /** @type {MetricPattern13} */ (_mp(client, name, _i13)); } +/** @template T @typedef {{ name: string, by: { readonly month3: DateMetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern14 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern14} */ -function createMetricPattern14(client, name) { return _mp(client, name, _i14); } -/** @template T @typedef {{ name: string, by: { readonly month6: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern15 */ +function createMetricPattern14(client, name) { return /** @type {MetricPattern14} */ (_mp(client, name, _i14)); } +/** @template T @typedef {{ name: string, by: { readonly month6: DateMetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern15 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern15} */ -function createMetricPattern15(client, name) { return _mp(client, name, _i15); } -/** @template T @typedef {{ name: string, by: { readonly year1: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern16 */ +function createMetricPattern15(client, name) { return /** @type {MetricPattern15} */ (_mp(client, name, _i15)); } +/** @template T @typedef {{ name: string, by: { readonly year1: DateMetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern16 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern16} */ -function createMetricPattern16(client, name) { return _mp(client, name, _i16); } -/** @template T @typedef {{ name: string, by: { readonly year10: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern17 */ +function createMetricPattern16(client, name) { return /** @type {MetricPattern16} */ (_mp(client, name, _i16)); } +/** @template T @typedef {{ name: string, by: { readonly year10: DateMetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern17 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern17} */ -function createMetricPattern17(client, name) { return _mp(client, name, _i17); } +function createMetricPattern17(client, name) { return /** @type {MetricPattern17} */ (_mp(client, name, _i17)); } /** @template T @typedef {{ name: string, by: { readonly halvingepoch: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern18 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern18} */ -function createMetricPattern18(client, name) { return _mp(client, name, _i18); } +function createMetricPattern18(client, name) { return /** @type {MetricPattern18} */ (_mp(client, name, _i18)); } /** @template T @typedef {{ name: string, by: { readonly difficultyepoch: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern19 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern19} */ -function createMetricPattern19(client, name) { return _mp(client, name, _i19); } +function createMetricPattern19(client, name) { return /** @type {MetricPattern19} */ (_mp(client, name, _i19)); } /** @template T @typedef {{ name: string, by: { readonly height: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern20 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern20} */ -function createMetricPattern20(client, name) { return _mp(client, name, _i20); } +function createMetricPattern20(client, name) { return /** @type {MetricPattern20} */ (_mp(client, name, _i20)); } /** @template T @typedef {{ name: string, by: { readonly txindex: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern21 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern21} */ -function createMetricPattern21(client, name) { return _mp(client, name, _i21); } +function createMetricPattern21(client, name) { return /** @type {MetricPattern21} */ (_mp(client, name, _i21)); } /** @template T @typedef {{ name: string, by: { readonly txinindex: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern22 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern22} */ -function createMetricPattern22(client, name) { return _mp(client, name, _i22); } +function createMetricPattern22(client, name) { return /** @type {MetricPattern22} */ (_mp(client, name, _i22)); } /** @template T @typedef {{ name: string, by: { readonly txoutindex: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern23 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern23} */ -function createMetricPattern23(client, name) { return _mp(client, name, _i23); } +function createMetricPattern23(client, name) { return /** @type {MetricPattern23} */ (_mp(client, name, _i23)); } /** @template T @typedef {{ name: string, by: { readonly emptyoutputindex: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern24 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern24} */ -function createMetricPattern24(client, name) { return _mp(client, name, _i24); } +function createMetricPattern24(client, name) { return /** @type {MetricPattern24} */ (_mp(client, name, _i24)); } /** @template T @typedef {{ name: string, by: { readonly opreturnindex: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern25 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern25} */ -function createMetricPattern25(client, name) { return _mp(client, name, _i25); } +function createMetricPattern25(client, name) { return /** @type {MetricPattern25} */ (_mp(client, name, _i25)); } /** @template T @typedef {{ name: string, by: { readonly p2aaddressindex: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern26 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern26} */ -function createMetricPattern26(client, name) { return _mp(client, name, _i26); } +function createMetricPattern26(client, name) { return /** @type {MetricPattern26} */ (_mp(client, name, _i26)); } /** @template T @typedef {{ name: string, by: { readonly p2msoutputindex: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern27 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern27} */ -function createMetricPattern27(client, name) { return _mp(client, name, _i27); } +function createMetricPattern27(client, name) { return /** @type {MetricPattern27} */ (_mp(client, name, _i27)); } /** @template T @typedef {{ name: string, by: { readonly p2pk33addressindex: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern28 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern28} */ -function createMetricPattern28(client, name) { return _mp(client, name, _i28); } +function createMetricPattern28(client, name) { return /** @type {MetricPattern28} */ (_mp(client, name, _i28)); } /** @template T @typedef {{ name: string, by: { readonly p2pk65addressindex: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern29 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern29} */ -function createMetricPattern29(client, name) { return _mp(client, name, _i29); } +function createMetricPattern29(client, name) { return /** @type {MetricPattern29} */ (_mp(client, name, _i29)); } /** @template T @typedef {{ name: string, by: { readonly p2pkhaddressindex: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern30 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern30} */ -function createMetricPattern30(client, name) { return _mp(client, name, _i30); } +function createMetricPattern30(client, name) { return /** @type {MetricPattern30} */ (_mp(client, name, _i30)); } /** @template T @typedef {{ name: string, by: { readonly p2shaddressindex: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern31 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern31} */ -function createMetricPattern31(client, name) { return _mp(client, name, _i31); } +function createMetricPattern31(client, name) { return /** @type {MetricPattern31} */ (_mp(client, name, _i31)); } /** @template T @typedef {{ name: string, by: { readonly p2traddressindex: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern32 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern32} */ -function createMetricPattern32(client, name) { return _mp(client, name, _i32); } +function createMetricPattern32(client, name) { return /** @type {MetricPattern32} */ (_mp(client, name, _i32)); } /** @template T @typedef {{ name: string, by: { readonly p2wpkhaddressindex: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern33 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern33} */ -function createMetricPattern33(client, name) { return _mp(client, name, _i33); } +function createMetricPattern33(client, name) { return /** @type {MetricPattern33} */ (_mp(client, name, _i33)); } /** @template T @typedef {{ name: string, by: { readonly p2wshaddressindex: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern34 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern34} */ -function createMetricPattern34(client, name) { return _mp(client, name, _i34); } +function createMetricPattern34(client, name) { return /** @type {MetricPattern34} */ (_mp(client, name, _i34)); } /** @template T @typedef {{ name: string, by: { readonly unknownoutputindex: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern35 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern35} */ -function createMetricPattern35(client, name) { return _mp(client, name, _i35); } +function createMetricPattern35(client, name) { return /** @type {MetricPattern35} */ (_mp(client, name, _i35)); } /** @template T @typedef {{ name: string, by: { readonly fundedaddressindex: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern36 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern36} */ -function createMetricPattern36(client, name) { return _mp(client, name, _i36); } +function createMetricPattern36(client, name) { return /** @type {MetricPattern36} */ (_mp(client, name, _i36)); } /** @template T @typedef {{ name: string, by: { readonly emptyaddressindex: MetricEndpointBuilder }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder|undefined }} MetricPattern37 */ /** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern37} */ -function createMetricPattern37(client, name) { return _mp(client, name, _i37); } +function createMetricPattern37(client, name) { return /** @type {MetricPattern37} */ (_mp(client, name, _i37)); } // Reusable structural pattern factories diff --git a/modules/brk-client/tests/metric_data.js b/modules/brk-client/tests/metric_data.js index a7a45ecfe..1bed7ec96 100644 --- a/modules/brk-client/tests/metric_data.js +++ b/modules/brk-client/tests/metric_data.js @@ -21,7 +21,7 @@ console.log("\n2. isDateBased:"); if (!price.isDateBased) throw new Error("day1 should be date-based"); console.log(` day1: ${price.isDateBased}`); -// Test indexes() +// Test indexes() - always returns numbers console.log("\n3. indexes():"); const indexes = price.indexes(); console.log(` ${JSON.stringify(indexes)}`); @@ -29,7 +29,7 @@ if (indexes.length !== 5) throw new Error("Expected 5 indexes"); if (indexes[0] !== price.start) throw new Error("First index should equal start"); -// Test dates() +// Test dates() - DateMetricData method console.log("\n4. dates():"); const dates = price.dates(); console.log( @@ -47,69 +47,85 @@ if ( ); } -// Test keys() - date-based returns dates -console.log("\n5. keys() (date-based):"); +// Test keys() - always returns numbers (alias for indexes) +console.log("\n5. keys():"); const keys = price.keys(); -console.log(` Length: ${keys.length}, First: ${keys[0].toISOString()}`); if (keys.length !== 5) throw new Error("Expected 5 keys"); -if (!(keys[0] instanceof Date)) throw new Error("Expected Date keys for day1"); +if (typeof keys[0] !== "number") throw new Error("Expected number keys"); +console.log(` Length: ${keys.length}, First: ${keys[0]}`); -// Test entries() +// Test entries() - returns [number, value] pairs console.log("\n6. entries():"); const entries = price.entries(); -console.log( - ` First: [${entries[0][0].toISOString()}, ${entries[0][1]}]`, -); +if (typeof entries[0][0] !== "number") + throw new Error("Expected number entry key"); +console.log(` First: [${entries[0][0]}, ${entries[0][1]}]`); if (entries[0][1] !== price.data[0]) throw new Error("First entry value mismatch"); -// Test toMap() -console.log("\n7. toMap():"); +// Test dateEntries() - DateMetricData method, returns [Date, value] pairs +console.log("\n7. dateEntries():"); +const dateEntries = price.dateEntries(); +if (!(dateEntries[0][0] instanceof Date)) + throw new Error("Expected Date entry key"); +console.log(` First: [${dateEntries[0][0].toISOString()}, ${dateEntries[0][1]}]`); + +// Test toMap() - returns Map +console.log("\n8. toMap():"); const map = price.toMap(); console.log(` Size: ${map.size}`); if (map.size !== 5) throw new Error("Expected map size 5"); -// Test Symbol.iterator (for...of) - date-based should yield [date, value] -console.log("\n8. for...of iteration (date-based):"); +// Test toDateMap() - DateMetricData method +console.log("\n9. toDateMap():"); +const dateMap = price.toDateMap(); +console.log(` Size: ${dateMap.size}`); +if (dateMap.size !== 5) throw new Error("Expected date map size 5"); + +// Test Symbol.iterator (for...of) - yields [number, value] +console.log("\n10. for...of iteration:"); let count = 0; -for (const [key, val] of price) { - if (count === 0 && !(key instanceof Date)) - throw new Error("Expected Date keys in iteration for date-based"); +for (const [key, _val] of price) { + if (count === 0 && typeof key !== "number") + throw new Error("Expected number keys in iteration"); count++; } console.log(` Iterated ${count} items`); if (count !== 5) throw new Error("Expected 5 iterations"); // Test with non-date-based index (height) -console.log("\n9. Testing height-based metric:"); +console.log("\n11. Testing height-based metric:"); const heightMetric = await client.metrics.prices.usd.price.by.height.last(3); console.log( ` Total: ${heightMetric.total}, Start: ${heightMetric.start}, End: ${heightMetric.end}`, ); -if (heightMetric.isDateBased) throw new Error("height should not be date-based"); +if (heightMetric.isDateBased) + throw new Error("height should not be date-based"); -// Test keys() - non-date returns numbers +// Test keys() - always numbers const heightKeys = heightMetric.keys(); console.log(` keys(): ${JSON.stringify(heightKeys)}`); if (typeof heightKeys[0] !== "number") throw new Error("Expected number keys for height"); -// Test entries() - non-date returns [number, value] +// Test entries() - [number, value] const heightEntries = heightMetric.entries(); -console.log(` entries()[0]: [${heightEntries[0][0]}, ${heightEntries[0][1]}]`); +console.log( + ` entries()[0]: [${heightEntries[0][0]}, ${heightEntries[0][1]}]`, +); if (heightEntries[0][0] !== heightMetric.start) throw new Error("First entry index mismatch"); -// Test toMap() - non-date +// Test toMap() - Map const heightMap = heightMetric.toMap(); if (heightMap.size !== 3) throw new Error("Expected map size 3"); if (heightMap.get(heightMetric.start) !== heightMetric.data[0]) throw new Error("First value mismatch"); // Test for...of on non-date metric -console.log("\n10. for...of iteration (height):"); +console.log("\n12. for...of iteration (height):"); let heightCount = 0; -for (const [key, val] of heightMetric) { +for (const [key, _val] of heightMetric) { if (heightCount === 0 && typeof key !== "number") throw new Error("Expected number keys for height iteration"); heightCount++; @@ -117,7 +133,7 @@ for (const [key, val] of heightMetric) { console.log(` Iterated ${heightCount} items`); // Test different date indexes -console.log("\n11. Testing month1:"); +console.log("\n13. Testing month1:"); const monthMetric = await client.metrics.prices.usd.split.close.by.month1.first(3); const monthDates = monthMetric.dates(); @@ -132,7 +148,7 @@ if ( } // Test indexToDate directly -console.log("\n12. Testing indexToDate():"); +console.log("\n14. Testing indexToDate():"); const genesis = client.indexToDate("day1", 0); if ( genesis.getFullYear() !== 2009 || @@ -199,13 +215,14 @@ console.log(` year10 0: ${d0.toISOString()}`); console.log(` year10 1: ${d1.toISOString()}`); // Test dateToIndex -console.log("\n13. Testing dateToIndex():"); +console.log("\n15. Testing dateToIndex():"); const idx = client.dateToIndex("day1", new Date(Date.UTC(2009, 0, 9))); if (idx !== 1) throw new Error(`Expected day1 index 1, got ${idx}`); console.log(` day1 2009-01-09: ${idx}`); const monthIdx = client.dateToIndex("month1", new Date(Date.UTC(2010, 0, 1))); -if (monthIdx !== 12) throw new Error(`Expected month1 index 12, got ${monthIdx}`); +if (monthIdx !== 12) + throw new Error(`Expected month1 index 12, got ${monthIdx}`); console.log(` month1 2010-01-01: ${monthIdx}`); const yearIdx = client.dateToIndex("year1", new Date(Date.UTC(2019, 0, 1))); @@ -215,15 +232,18 @@ console.log(` year1 2019-01-01: ${yearIdx}`); // Test roundtrip: indexToDate -> dateToIndex const testDate = client.indexToDate("day1", 100); const roundtrip = client.dateToIndex("day1", testDate); -if (roundtrip !== 100) throw new Error(`Roundtrip failed: expected 100, got ${roundtrip}`); +if (roundtrip !== 100) + throw new Error(`Roundtrip failed: expected 100, got ${roundtrip}`); console.log(` Roundtrip day1 100: ${testDate.toISOString()} -> ${roundtrip}`); // Test slice with Date -console.log("\n14. Testing slice with Date:"); +console.log("\n16. Testing slice with Date:"); const dateSlice = await client.metrics.prices.usd.split.close.by.day1 .slice(new Date(Date.UTC(2020, 0, 1)), new Date(Date.UTC(2020, 0, 4))) .fetch(); -console.log(` Slice start: ${dateSlice.start}, end: ${dateSlice.end}, items: ${dateSlice.data.length}`); +console.log( + ` Slice start: ${dateSlice.start}, end: ${dateSlice.end}, items: ${dateSlice.data.length}`, +); if (dateSlice.data.length !== dateSlice.end - dateSlice.start) throw new Error("Slice data length mismatch"); diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index c940b7aee..4ed5e7fa1 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict, Union, Protocol, overload, Iterator, Tuple, TYPE_CHECKING +from typing import TypeVar, Generic, Any, Dict, Optional, List, Iterator, Literal, TypedDict, Union, Protocol, overload, Tuple, TYPE_CHECKING from http.client import HTTPSConnection, HTTPConnection from urllib.parse import urlparse from datetime import date, datetime, timedelta, timezone @@ -1085,16 +1085,16 @@ class BrkClientBase: """Make a GET request and return text.""" return self.get(path).decode() - def close(self): + def close(self) -> None: """Close the HTTP client.""" if self._conn: self._conn.close() self._conn = None - def __enter__(self): + def __enter__(self) -> BrkClientBase: return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, exc_type: Optional[type], exc_val: Optional[BaseException], exc_tb: Optional[Any]) -> None: self.close() @@ -1199,7 +1199,7 @@ def _date_to_index(index: str, d: Union[date, datetime]) -> int: @dataclass class MetricData(Generic[T]): - """Metric data with range information.""" + """Metric data with range information. Always int-indexed.""" version: int index: Index total: int @@ -1213,63 +1213,96 @@ class MetricData(Generic[T]): """Whether this metric uses a date-based index.""" return self.index in _DATE_INDEXES - def dates(self) -> list: - """Get dates for the index range. Date-based indexes only, throws otherwise.""" - return [_index_to_date(self.index, i) for i in range(self.start, self.end)] - def indexes(self) -> List[int]: """Get raw index numbers.""" return list(range(self.start, self.end)) - def keys(self) -> list: - """Get keys: dates for date-based indexes, index numbers otherwise.""" - return self.dates() if self.is_date_based else self.indexes() + def keys(self) -> List[int]: + """Get keys as index numbers.""" + return self.indexes() - def items(self) -> list: - """Get (key, value) pairs: keys are dates for date-based, numbers otherwise.""" - return list(zip(self.keys(), self.data)) + def items(self) -> List[Tuple[int, T]]: + """Get (index, value) pairs.""" + return list(zip(self.indexes(), self.data)) - def to_dict(self) -> dict: - """Return {key: value} dict: keys are dates for date-based, numbers otherwise.""" - return dict(zip(self.keys(), self.data)) + def to_dict(self) -> Dict[int, T]: + """Return {index: value} dict.""" + return dict(zip(self.indexes(), self.data)) - def __iter__(self): - """Iterate over (key, value) pairs. Keys are dates for date-based, numbers otherwise.""" - return iter(zip(self.keys(), self.data)) + def __iter__(self) -> Iterator[Tuple[int, T]]: + """Iterate over (index, value) pairs.""" + return iter(zip(self.indexes(), self.data)) + + def __len__(self) -> int: + return len(self.data) + + def to_polars(self) -> pl.DataFrame: + """Convert to Polars DataFrame with 'index' and 'value' columns.""" + try: + import polars as pl # type: ignore[import-not-found] + except ImportError: + raise ImportError("polars is required: pip install polars") + return pl.DataFrame({"index": self.indexes(), "value": self.data}) + + def to_pandas(self) -> pd.DataFrame: + """Convert to Pandas DataFrame with 'index' and 'value' columns.""" + try: + import pandas as pd # type: ignore[import-not-found] + except ImportError: + raise ImportError("pandas is required: pip install pandas") + return pd.DataFrame({"index": self.indexes(), "value": self.data}) + + +@dataclass +class DateMetricData(MetricData[T]): + """Metric data with date-based index. Extends MetricData 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+.""" + return [_index_to_date(self.index, i) for i in range(self.start, self.end)] + + def date_items(self) -> List[Tuple[Union[date, datetime], T]]: + """Get (date, value) pairs.""" + return list(zip(self.dates(), self.data)) + + def to_date_dict(self) -> Dict[Union[date, datetime], T]: + """Return {date: value} dict.""" + return dict(zip(self.dates(), self.data)) def to_polars(self, with_dates: bool = True) -> pl.DataFrame: - """Convert to Polars DataFrame. Requires polars to be installed. + """Convert to Polars DataFrame. Returns a DataFrame with columns: - - 'date' and 'value' if with_dates=True and index is date-based + - 'date' and 'value' if with_dates=True (default) - 'index' and 'value' otherwise """ try: import polars as pl # type: ignore[import-not-found] except ImportError: raise ImportError("polars is required: pip install polars") - if with_dates and self.is_date_based: + if with_dates: return pl.DataFrame({"date": self.dates(), "value": self.data}) return pl.DataFrame({"index": self.indexes(), "value": self.data}) def to_pandas(self, with_dates: bool = True) -> pd.DataFrame: - """Convert to Pandas DataFrame. Requires pandas to be installed. + """Convert to Pandas DataFrame. Returns a DataFrame with columns: - - 'date' and 'value' if with_dates=True and index is date-based + - 'date' and 'value' if with_dates=True (default) - 'index' and 'value' otherwise """ try: import pandas as pd # type: ignore[import-not-found] except ImportError: raise ImportError("pandas is required: pip install pandas") - if with_dates and self.is_date_based: + if with_dates: return pd.DataFrame({"date": self.dates(), "value": self.data}) return pd.DataFrame({"index": self.indexes(), "value": self.data}) -# Type alias for non-generic usage +# Type aliases for non-generic usage AnyMetricData = MetricData[Any] +AnyDateMetricData = DateMetricData[Any] class _EndpointConfig: @@ -1303,9 +1336,15 @@ class _EndpointConfig: p = self.path() return f"{p}?{query}" if query else p - def get_metric(self) -> MetricData: + 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_date_metric(self) -> DateMetricData[Any]: + return DateMetricData(**self.client.get_json(self._build_path())) + def get_csv(self) -> str: return self.client.get_text(self._build_path(format='csv')) @@ -1349,10 +1388,7 @@ class SkippedBuilder(Generic[T]): def take(self, n: int) -> RangeBuilder[T]: """Take n items after the skipped position.""" start = self._config.start or 0 - return RangeBuilder(_EndpointConfig( - self._config.client, self._config.name, self._config.index, - start, start + n - )) + return RangeBuilder(self._config._new(start, start + n)) def fetch(self) -> MetricData[T]: """Fetch from skipped position to end.""" @@ -1363,29 +1399,35 @@ class SkippedBuilder(Generic[T]): return self._config.get_csv() -class MetricEndpointBuilder(Generic[T]): - """Builder for metric endpoint queries. +class DateRangeBuilder(RangeBuilder[T]): + """Range builder that returns DateMetricData.""" + def fetch(self) -> DateMetricData[T]: + return self._config.get_date_metric() - Use method chaining to specify the data range, then call fetch() or fetch_csv() to execute. + +class DateSingleItemBuilder(SingleItemBuilder[T]): + """Single item builder that returns DateMetricData.""" + def fetch(self) -> DateMetricData[T]: + return self._config.get_date_metric() + + +class DateSkippedBuilder(SkippedBuilder[T]): + """Skipped builder that returns DateMetricData.""" + 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() + + +class MetricEndpointBuilder(Generic[T]): + """Builder for metric endpoint queries with int-based indexing. Examples: - # Fetch all data data = endpoint.fetch() - - # Single item access data = endpoint[5].fetch() - - # Slice syntax (Python-native) - data = endpoint[:10].fetch() # First 10 - data = endpoint[-5:].fetch() # Last 5 - data = endpoint[100:110].fetch() # Range - - # Convenience methods (pandas-style) - data = endpoint.head().fetch() # First 10 (default) - data = endpoint.head(20).fetch() # First 20 - data = endpoint.tail(5).fetch() # Last 5 - - # Iterator-style chaining + data = endpoint[:10].fetch() + data = endpoint.head(20).fetch() data = endpoint.skip(100).take(10).fetch() """ @@ -1397,66 +1439,30 @@ class MetricEndpointBuilder(Generic[T]): @overload def __getitem__(self, key: slice) -> RangeBuilder[T]: ... - def __getitem__(self, key: Union[int, slice, date, datetime]) -> Union[SingleItemBuilder[T], RangeBuilder[T]]: - """Access single item or slice. Accepts dates for date-based indexes. - - Examples: - endpoint[5] # Single item at index 5 - endpoint[:10] # First 10 - endpoint[-5:] # Last 5 - endpoint[100:110] # Range 100-109 - endpoint[date(2020, 1, 1):date(2023, 1, 1)] # Date range - endpoint[date(2020, 1, 1):] # Since date - """ - if isinstance(key, (date, datetime)): - idx = _date_to_index(self._config.index, key) - return SingleItemBuilder(_EndpointConfig( - self._config.client, self._config.name, self._config.index, - idx, idx + 1 - )) + def __getitem__(self, key: Union[int, slice]) -> Union[SingleItemBuilder[T], RangeBuilder[T]]: + """Access single item or slice by integer index.""" if isinstance(key, int): - return SingleItemBuilder(_EndpointConfig( - self._config.client, self._config.name, self._config.index, - key, key + 1 - )) - start, stop = key.start, key.stop - if isinstance(start, (date, datetime)): - start = _date_to_index(self._config.index, start) - if isinstance(stop, (date, datetime)): - stop = _date_to_index(self._config.index, stop) - return RangeBuilder(_EndpointConfig( - self._config.client, self._config.name, self._config.index, - start, stop - )) + return SingleItemBuilder(self._config._new(key, key + 1)) + return RangeBuilder(self._config._new(key.start, key.stop)) def head(self, n: int = 10) -> RangeBuilder[T]: - """Get the first n items (pandas-style).""" - return RangeBuilder(_EndpointConfig( - self._config.client, self._config.name, self._config.index, - None, n - )) + """Get the first n items.""" + return RangeBuilder(self._config._new(end=n)) def tail(self, n: int = 10) -> RangeBuilder[T]: - """Get the last n items (pandas-style).""" - start, end = (None, 0) if n == 0 else (-n, None) - return RangeBuilder(_EndpointConfig( - self._config.client, self._config.name, self._config.index, - start, end - )) + """Get the last n items.""" + return RangeBuilder(self._config._new(end=0) if n == 0 else self._config._new(start=-n)) def skip(self, n: int) -> SkippedBuilder[T]: - """Skip the first n items. Chain with take() to get a range.""" - return SkippedBuilder(_EndpointConfig( - self._config.client, self._config.name, self._config.index, - n, None - )) + """Skip the first n items.""" + return SkippedBuilder(self._config._new(start=n)) def fetch(self) -> MetricData[T]: - """Fetch all data as parsed JSON.""" + """Fetch all data.""" return self._config.get_metric() def fetch_csv(self) -> str: - """Fetch all data as CSV string.""" + """Fetch all data as CSV.""" return self._config.get_csv() def path(self) -> str: @@ -1464,8 +1470,72 @@ class MetricEndpointBuilder(Generic[T]): return self._config.path() -# Type alias for non-generic usage +class DateMetricEndpointBuilder(Generic[T]): + """Builder for metric endpoint queries with date-based indexing. + + Accepts dates in __getitem__ and returns DateMetricData from fetch(). + + Examples: + data = endpoint.fetch() + data = endpoint[date(2020, 1, 1)].fetch() + data = endpoint[date(2020, 1, 1):date(2023, 1, 1)].fetch() + data = endpoint[:10].fetch() + """ + + def __init__(self, client: BrkClientBase, name: str, index: Index): + self._config = _EndpointConfig(client, name, index) + + @overload + def __getitem__(self, key: int) -> DateSingleItemBuilder[T]: ... + @overload + def __getitem__(self, key: datetime) -> DateSingleItemBuilder[T]: ... + @overload + def __getitem__(self, key: date) -> DateSingleItemBuilder[T]: ... + @overload + def __getitem__(self, key: slice) -> DateRangeBuilder[T]: ... + + def __getitem__(self, key: Union[int, slice, date, datetime]) -> Union[DateSingleItemBuilder[T], DateRangeBuilder[T]]: + """Access single item or slice. Accepts int, date, or datetime.""" + if isinstance(key, (date, datetime)): + idx = _date_to_index(self._config.index, key) + return DateSingleItemBuilder(self._config._new(idx, idx + 1)) + if isinstance(key, int): + return DateSingleItemBuilder(self._config._new(key, key + 1)) + start, stop = key.start, key.stop + if isinstance(start, (date, datetime)): + start = _date_to_index(self._config.index, start) + if isinstance(stop, (date, datetime)): + stop = _date_to_index(self._config.index, stop) + return DateRangeBuilder(self._config._new(start, stop)) + + def head(self, n: int = 10) -> DateRangeBuilder[T]: + """Get the first n items.""" + return DateRangeBuilder(self._config._new(end=n)) + + def tail(self, n: int = 10) -> DateRangeBuilder[T]: + """Get the last n items.""" + return DateRangeBuilder(self._config._new(end=0) if n == 0 else self._config._new(start=-n)) + + def skip(self, n: int) -> DateSkippedBuilder[T]: + """Skip the first n items.""" + return DateSkippedBuilder(self._config._new(start=n)) + + def fetch(self) -> DateMetricData[T]: + """Fetch all data.""" + return self._config.get_date_metric() + + def fetch_csv(self) -> str: + """Fetch all data as CSV.""" + return self._config.get_csv() + + def path(self) -> str: + """Get the base endpoint path.""" + return self._config.path() + + +# Type aliases for non-generic usage AnyMetricEndpointBuilder = MetricEndpointBuilder[Any] +AnyDateMetricEndpointBuilder = DateMetricEndpointBuilder[Any] class MetricPattern(Protocol[T]): @@ -1527,25 +1597,28 @@ _i37 = ('emptyaddressindex',) def _ep(c: BrkClientBase, n: str, i: Index) -> MetricEndpointBuilder[Any]: return MetricEndpointBuilder(c, n, i) +def _dep(c: BrkClientBase, n: str, i: Index) -> DateMetricEndpointBuilder[Any]: + return DateMetricEndpointBuilder(c, n, i) + # Index accessor classes class _MetricPattern1By(Generic[T]): def __init__(self, c: BrkClientBase, n: str): self._c, self._n = c, n - def minute1(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'minute1') - def minute5(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'minute5') - def minute10(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'minute10') - def minute30(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'minute30') - def hour1(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'hour1') - def hour4(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'hour4') - def hour12(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'hour12') - def day1(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'day1') - def day3(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'day3') - def week1(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'week1') - def month1(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'month1') - def month3(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'month3') - def month6(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'month6') - def year1(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'year1') - def year10(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'year10') + def minute1(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'minute1') + def minute5(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'minute5') + def minute10(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'minute10') + def minute30(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'minute30') + def hour1(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'hour1') + def hour4(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'hour4') + def hour12(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'hour12') + def day1(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'day1') + def day3(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'day3') + def week1(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'week1') + def month1(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'month1') + def month3(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'month3') + def month6(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'month6') + def year1(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'year1') + def year10(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'year10') def halvingepoch(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'halvingepoch') def difficultyepoch(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'difficultyepoch') def height(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'height') @@ -1560,21 +1633,21 @@ class MetricPattern1(Generic[T]): class _MetricPattern2By(Generic[T]): def __init__(self, c: BrkClientBase, n: str): self._c, self._n = c, n - def minute1(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'minute1') - def minute5(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'minute5') - def minute10(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'minute10') - def minute30(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'minute30') - def hour1(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'hour1') - def hour4(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'hour4') - def hour12(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'hour12') - def day1(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'day1') - def day3(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'day3') - def week1(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'week1') - def month1(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'month1') - def month3(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'month3') - def month6(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'month6') - def year1(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'year1') - def year10(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'year10') + def minute1(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'minute1') + def minute5(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'minute5') + def minute10(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'minute10') + def minute30(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'minute30') + def hour1(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'hour1') + def hour4(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'hour4') + def hour12(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'hour12') + def day1(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'day1') + def day3(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'day3') + def week1(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'week1') + def month1(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'month1') + def month3(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'month3') + def month6(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'month6') + def year1(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'year1') + def year10(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'year10') def halvingepoch(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'halvingepoch') def difficultyepoch(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'difficultyepoch') @@ -1588,7 +1661,7 @@ class MetricPattern2(Generic[T]): class _MetricPattern3By(Generic[T]): def __init__(self, c: BrkClientBase, n: str): self._c, self._n = c, n - def minute1(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'minute1') + def minute1(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'minute1') class MetricPattern3(Generic[T]): by: _MetricPattern3By[T] @@ -1600,7 +1673,7 @@ class MetricPattern3(Generic[T]): class _MetricPattern4By(Generic[T]): def __init__(self, c: BrkClientBase, n: str): self._c, self._n = c, n - def minute5(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'minute5') + def minute5(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'minute5') class MetricPattern4(Generic[T]): by: _MetricPattern4By[T] @@ -1612,7 +1685,7 @@ class MetricPattern4(Generic[T]): class _MetricPattern5By(Generic[T]): def __init__(self, c: BrkClientBase, n: str): self._c, self._n = c, n - def minute10(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'minute10') + def minute10(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'minute10') class MetricPattern5(Generic[T]): by: _MetricPattern5By[T] @@ -1624,7 +1697,7 @@ class MetricPattern5(Generic[T]): class _MetricPattern6By(Generic[T]): def __init__(self, c: BrkClientBase, n: str): self._c, self._n = c, n - def minute30(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'minute30') + def minute30(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'minute30') class MetricPattern6(Generic[T]): by: _MetricPattern6By[T] @@ -1636,7 +1709,7 @@ class MetricPattern6(Generic[T]): class _MetricPattern7By(Generic[T]): def __init__(self, c: BrkClientBase, n: str): self._c, self._n = c, n - def hour1(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'hour1') + def hour1(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'hour1') class MetricPattern7(Generic[T]): by: _MetricPattern7By[T] @@ -1648,7 +1721,7 @@ class MetricPattern7(Generic[T]): class _MetricPattern8By(Generic[T]): def __init__(self, c: BrkClientBase, n: str): self._c, self._n = c, n - def hour4(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'hour4') + def hour4(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'hour4') class MetricPattern8(Generic[T]): by: _MetricPattern8By[T] @@ -1660,7 +1733,7 @@ class MetricPattern8(Generic[T]): class _MetricPattern9By(Generic[T]): def __init__(self, c: BrkClientBase, n: str): self._c, self._n = c, n - def hour12(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'hour12') + def hour12(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'hour12') class MetricPattern9(Generic[T]): by: _MetricPattern9By[T] @@ -1672,7 +1745,7 @@ class MetricPattern9(Generic[T]): class _MetricPattern10By(Generic[T]): def __init__(self, c: BrkClientBase, n: str): self._c, self._n = c, n - def day1(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'day1') + def day1(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'day1') class MetricPattern10(Generic[T]): by: _MetricPattern10By[T] @@ -1684,7 +1757,7 @@ class MetricPattern10(Generic[T]): class _MetricPattern11By(Generic[T]): def __init__(self, c: BrkClientBase, n: str): self._c, self._n = c, n - def day3(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'day3') + def day3(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'day3') class MetricPattern11(Generic[T]): by: _MetricPattern11By[T] @@ -1696,7 +1769,7 @@ class MetricPattern11(Generic[T]): class _MetricPattern12By(Generic[T]): def __init__(self, c: BrkClientBase, n: str): self._c, self._n = c, n - def week1(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'week1') + def week1(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'week1') class MetricPattern12(Generic[T]): by: _MetricPattern12By[T] @@ -1708,7 +1781,7 @@ class MetricPattern12(Generic[T]): class _MetricPattern13By(Generic[T]): def __init__(self, c: BrkClientBase, n: str): self._c, self._n = c, n - def month1(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'month1') + def month1(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'month1') class MetricPattern13(Generic[T]): by: _MetricPattern13By[T] @@ -1720,7 +1793,7 @@ class MetricPattern13(Generic[T]): class _MetricPattern14By(Generic[T]): def __init__(self, c: BrkClientBase, n: str): self._c, self._n = c, n - def month3(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'month3') + def month3(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'month3') class MetricPattern14(Generic[T]): by: _MetricPattern14By[T] @@ -1732,7 +1805,7 @@ class MetricPattern14(Generic[T]): class _MetricPattern15By(Generic[T]): def __init__(self, c: BrkClientBase, n: str): self._c, self._n = c, n - def month6(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'month6') + def month6(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'month6') class MetricPattern15(Generic[T]): by: _MetricPattern15By[T] @@ -1744,7 +1817,7 @@ class MetricPattern15(Generic[T]): class _MetricPattern16By(Generic[T]): def __init__(self, c: BrkClientBase, n: str): self._c, self._n = c, n - def year1(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'year1') + def year1(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'year1') class MetricPattern16(Generic[T]): by: _MetricPattern16By[T] @@ -1756,7 +1829,7 @@ class MetricPattern16(Generic[T]): class _MetricPattern17By(Generic[T]): def __init__(self, c: BrkClientBase, n: str): self._c, self._n = c, n - def year10(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, 'year10') + def year10(self) -> DateMetricEndpointBuilder[T]: return _dep(self._c, self._n, 'year10') class MetricPattern17(Generic[T]): by: _MetricPattern17By[T] diff --git a/packages/brk_client/tests/test_metric_data.py b/packages/brk_client/tests/test_metric_data.py index a84f6049c..c16485e81 100644 --- a/packages/brk_client/tests/test_metric_data.py +++ b/packages/brk_client/tests/test_metric_data.py @@ -1,18 +1,20 @@ -# Tests for MetricData helper methods including polars/pandas conversion +# Tests for MetricData and DateMetricData helper methods including polars/pandas conversion # Run: uv run pytest tests/test_metric_data.py -v -from datetime import date, datetime, timezone +from datetime import date, datetime, timezone, timedelta import pytest -from brk_client import MetricData +from brk_client import MetricData, DateMetricData + + +# ============ Fixtures ============ -# Test data fixtures @pytest.fixture -def date_based_metric(): - """MetricData with day1 (date-based).""" - return MetricData( +def day1_metric(): + """DateMetricData with day1 (date-based, daily).""" + return DateMetricData( version=1, index="day1", total=100, @@ -24,7 +26,7 @@ def date_based_metric(): @pytest.fixture -def height_based_metric(): +def height_metric(): """MetricData with height (non-date-based).""" return MetricData( version=1, @@ -38,9 +40,9 @@ def height_based_metric(): @pytest.fixture -def month_based_metric(): - """MetricData with month1.""" - return MetricData( +def month1_metric(): + """DateMetricData with month1.""" + return DateMetricData( version=1, index="month1", total=200, @@ -51,103 +53,139 @@ def month_based_metric(): ) -# ============ is_date_based tests ============ +@pytest.fixture +def hour1_metric(): + """DateMetricData with hour1 (sub-daily).""" + return DateMetricData( + version=1, + index="hour1", + total=200000, + start=0, + end=3, + stamp="2024-01-01T00:00:00Z", + data=[10.0, 20.0, 30.0], + ) + + +@pytest.fixture +def minute5_metric(): + """DateMetricData with minute5 (sub-daily).""" + return DateMetricData( + version=1, + index="minute5", + total=500000, + start=0, + end=3, + stamp="2024-01-01T00:00:00Z", + data=[1, 2, 3], + ) + + +@pytest.fixture +def week1_metric(): + """DateMetricData with week1.""" + return DateMetricData( + version=1, + index="week1", + total=800, + start=0, + end=3, + stamp="2024-01-01T00:00:00Z", + data=[5, 10, 15], + ) + + +@pytest.fixture +def year1_metric(): + """DateMetricData with year1.""" + return DateMetricData( + version=1, + index="year1", + total=20, + start=0, + end=3, + stamp="2024-01-01T00:00:00Z", + data=[100, 200, 300], + ) + + +@pytest.fixture +def day3_metric(): + """DateMetricData with day3.""" + return DateMetricData( + version=1, + index="day3", + total=2000, + start=0, + end=3, + stamp="2024-01-01T00:00:00Z", + data=[7, 8, 9], + ) + + +@pytest.fixture +def empty_metric(): + """MetricData with empty data.""" + return MetricData( + version=1, + index="day1", + total=100, + start=5, + end=5, + stamp="2024-01-01T00:00:00Z", + data=[], + ) + + +# ============ is_date_based ============ class TestIsDateBased: - """Test the is_date_based property.""" + def test_day1(self, day1_metric): + assert day1_metric.is_date_based is True - def test_day1_is_date_based(self, date_based_metric): - assert date_based_metric.is_date_based is True + def test_month1(self, month1_metric): + assert month1_metric.is_date_based is True - def test_height_is_not_date_based(self, height_based_metric): - assert height_based_metric.is_date_based is False + def test_hour1(self, hour1_metric): + assert hour1_metric.is_date_based is True - def test_month1_is_date_based(self, month_based_metric): - assert month_based_metric.is_date_based is True + def test_minute5(self, minute5_metric): + assert minute5_metric.is_date_based is True + + def test_week1(self, week1_metric): + assert week1_metric.is_date_based is True + + def test_year1(self, year1_metric): + assert year1_metric.is_date_based is True + + def test_day3(self, day3_metric): + assert day3_metric.is_date_based is True + + def test_height(self, height_metric): + assert height_metric.is_date_based is False -# ============ Date conversion tests ============ +# ============ MetricData (int-indexed) ============ -class TestIndexToDate: - """Test the _index_to_date function via MetricData.dates().""" +class TestMetricData: + def test_keys(self, height_metric): + assert height_metric.keys() == [800000, 800001, 800002, 800003, 800004] - def test_day1_zero(self, date_based_metric): - """day1 0 is genesis: Jan 3, 2009.""" - dates = date_based_metric.dates() - assert dates[0] == date(2009, 1, 3) - - def test_day1_one(self, date_based_metric): - """day1 1 is Jan 9, 2009 (6 day gap after genesis).""" - dates = date_based_metric.dates() - assert dates[1] == date(2009, 1, 9) - - def test_day1_two(self, date_based_metric): - """day1 2 is Jan 10, 2009.""" - dates = date_based_metric.dates() - assert dates[2] == date(2009, 1, 10) - - def test_month1_dates(self, month_based_metric): - """month1 returns correct dates.""" - dates = month_based_metric.dates() - assert dates[0] == date(2009, 1, 1) - assert dates[1] == date(2009, 2, 1) - assert dates[2] == date(2009, 3, 1) - - -# ============ Smart keys/items/to_dict/iter tests ============ - - -class TestSmartHelpers: - """Test smart MetricData helpers that auto-detect date vs numeric keys.""" - - def test_keys_date_based(self, date_based_metric): - """keys() returns dates for date-based metric.""" - keys = date_based_metric.keys() - assert len(keys) == 5 - assert keys[0] == date(2009, 1, 3) # genesis - assert keys[1] == date(2009, 1, 9) # day1 1 - - def test_keys_height_based(self, height_based_metric): - """keys() returns index numbers for non-date-based metric.""" - keys = height_based_metric.keys() - assert keys == [800000, 800001, 800002, 800003, 800004] - - def test_items_date_based(self, date_based_metric): - """items() returns (date, value) pairs for date-based metric.""" - items = date_based_metric.items() - assert items[0] == (date(2009, 1, 3), 100) - assert items[1] == (date(2009, 1, 9), 200) - - def test_items_height_based(self, height_based_metric): - """items() returns (index, value) pairs for height-based metric.""" - items = height_based_metric.items() + def test_items(self, height_metric): + items = height_metric.items() assert items[0] == (800000, 1.5) - assert items[4] == (800004, 5.5) + assert items[-1] == (800004, 5.5) - def test_to_dict_date_based(self, date_based_metric): - """to_dict() returns {date: value} for date-based metric.""" - d = date_based_metric.to_dict() - assert d[date(2009, 1, 3)] == 100 - assert d[date(2009, 1, 9)] == 200 - - def test_to_dict_height_based(self, height_based_metric): - """to_dict() returns {index: value} for height-based metric.""" - d = height_based_metric.to_dict() + def test_to_dict(self, height_metric): + d = height_metric.to_dict() assert d[800000] == 1.5 assert d[800004] == 5.5 + assert len(d) == 5 - def test_iter_date_based(self, date_based_metric): - """Default iteration yields (date, value) for date-based metric.""" - result = list(date_based_metric) - assert result[0] == (date(2009, 1, 3), 100) - assert result[1] == (date(2009, 1, 9), 200) - assert len(result) == 5 - - def test_iter_height_based(self, height_based_metric): - """Default iteration yields (index, value) for height-based metric.""" - result = list(height_based_metric) + def test_iter(self, height_metric): + result = list(height_metric) assert result == [ (800000, 1.5), (800001, 2.5), @@ -156,130 +194,334 @@ class TestSmartHelpers: (800004, 5.5), ] + def test_len(self, height_metric): + assert len(height_metric) == 5 -# ============ Explicit indexes/dates tests ============ + def test_indexes(self, height_metric): + assert height_metric.indexes() == [800000, 800001, 800002, 800003, 800004] + + def test_empty_data(self, empty_metric): + assert len(empty_metric) == 0 + assert empty_metric.keys() == [] + assert empty_metric.items() == [] + assert empty_metric.to_dict() == {} + assert list(empty_metric) == [] + assert empty_metric.indexes() == [] -class TestExplicitAccessors: - """Test explicit indexes() and dates() methods.""" - - def test_indexes_returns_range(self, date_based_metric): - """indexes() returns list of index values.""" - assert date_based_metric.indexes() == [0, 1, 2, 3, 4] - - def test_indexes_with_offset(self, height_based_metric): - """indexes() respects start/end offsets.""" - assert height_based_metric.indexes() == [800000, 800001, 800002, 800003, 800004] - - def test_dates_raises_for_non_date(self, height_based_metric): - """dates() raises for non-date-based index.""" - with pytest.raises(ValueError): - height_based_metric.dates() +# ============ DateMetricData inheritance ============ -# ============ Polars tests ============ +class TestDateMetricDataInheritance: + def test_isinstance(self, day1_metric): + assert isinstance(day1_metric, DateMetricData) + assert isinstance(day1_metric, MetricData) + + def test_int_keys_inherited(self, day1_metric): + assert day1_metric.keys() == [0, 1, 2, 3, 4] + + def test_int_items_inherited(self, day1_metric): + items = day1_metric.items() + assert items[0] == (0, 100) + assert items[4] == (4, 500) + + def test_int_iter_inherited(self, day1_metric): + result = list(day1_metric) + assert result[0] == (0, 100) + + def test_len_inherited(self, day1_metric): + assert len(day1_metric) == 5 + + def test_indexes_inherited(self, day1_metric): + assert day1_metric.indexes() == [0, 1, 2, 3, 4] + + def test_to_dict_inherited(self, day1_metric): + d = day1_metric.to_dict() + assert d[0] == 100 + assert isinstance(list(d.keys())[0], int) + + +# ============ _index_to_date conversions ============ + + +class TestIndexToDate: + """Test date conversion for all index types.""" + + def test_day1_genesis(self, day1_metric): + """Day1 index 0 = 2009-01-03 (genesis).""" + dates = day1_metric.dates() + assert dates[0] == date(2009, 1, 3) + + def test_day1_index_one(self, day1_metric): + """Day1 index 1 = 2009-01-09 (6-day gap after genesis).""" + dates = day1_metric.dates() + assert dates[1] == date(2009, 1, 9) + + def test_day1_consecutive(self, day1_metric): + """Day1 indexes 2+ are consecutive days after index 1.""" + dates = day1_metric.dates() + assert dates[2] == date(2009, 1, 10) + assert dates[3] == date(2009, 1, 11) + assert dates[4] == date(2009, 1, 12) + + def test_day1_returns_date_type(self, day1_metric): + dates = day1_metric.dates() + assert type(dates[0]) is date + + def test_month1(self, month1_metric): + dates = month1_metric.dates() + assert dates[0] == date(2009, 1, 1) + assert dates[1] == date(2009, 2, 1) + assert dates[2] == date(2009, 3, 1) + assert type(dates[0]) is date + + def test_week1(self, week1_metric): + dates = week1_metric.dates() + assert dates[0] == date(2009, 1, 3) # genesis + assert dates[1] == date(2009, 1, 10) # +7 days + assert dates[2] == date(2009, 1, 17) # +14 days + assert type(dates[0]) is date + + def test_year1(self, year1_metric): + dates = year1_metric.dates() + assert dates[0] == date(2009, 1, 1) + assert dates[1] == date(2010, 1, 1) + assert dates[2] == date(2011, 1, 1) + assert type(dates[0]) is date + + def test_day3(self, day3_metric): + dates = day3_metric.dates() + assert dates[0] == date(2009, 1, 1) # epoch + assert dates[1] == date(2009, 1, 4) # +3 days + assert dates[2] == date(2009, 1, 7) # +6 days + + def test_hour1_returns_datetime(self, hour1_metric): + """Sub-daily indexes return datetime, not date.""" + dates = hour1_metric.dates() + assert isinstance(dates[0], datetime) + # hour1 index 0 = epoch (2009-01-01 00:00:00 UTC) + assert dates[0] == datetime(2009, 1, 1, 0, 0, 0, tzinfo=timezone.utc) + assert dates[1] == datetime(2009, 1, 1, 1, 0, 0, tzinfo=timezone.utc) + assert dates[2] == datetime(2009, 1, 1, 2, 0, 0, tzinfo=timezone.utc) + + def test_minute5_returns_datetime(self, minute5_metric): + dates = minute5_metric.dates() + assert isinstance(dates[0], datetime) + assert dates[0] == datetime(2009, 1, 1, 0, 0, 0, tzinfo=timezone.utc) + assert dates[1] == datetime(2009, 1, 1, 0, 5, 0, tzinfo=timezone.utc) + assert dates[2] == datetime(2009, 1, 1, 0, 10, 0, tzinfo=timezone.utc) + + +# ============ _date_to_index conversions ============ + + +class TestDateToIndex: + """Test reverse date-to-index conversion.""" + + def test_day1_genesis(self): + from brk_client import _date_to_index + assert _date_to_index("day1", date(2009, 1, 3)) == 0 + + def test_day1_before_day_one(self): + from brk_client import _date_to_index + # Dates before day1 1 map to 0 + assert _date_to_index("day1", date(2009, 1, 5)) == 0 + + def test_day1_index_one(self): + from brk_client import _date_to_index + assert _date_to_index("day1", date(2009, 1, 9)) == 1 + + def test_day1_later(self): + from brk_client import _date_to_index + assert _date_to_index("day1", date(2009, 1, 10)) == 2 + + def test_month1(self): + from brk_client import _date_to_index + assert _date_to_index("month1", date(2009, 1, 1)) == 0 + assert _date_to_index("month1", date(2009, 2, 1)) == 1 + assert _date_to_index("month1", date(2010, 1, 1)) == 12 + + def test_year1(self): + from brk_client import _date_to_index + assert _date_to_index("year1", date(2009, 1, 1)) == 0 + assert _date_to_index("year1", date(2010, 6, 15)) == 1 + assert _date_to_index("year1", date(2020, 1, 1)) == 11 + + def test_week1(self): + from brk_client import _date_to_index + assert _date_to_index("week1", date(2009, 1, 3)) == 0 + assert _date_to_index("week1", date(2009, 1, 10)) == 1 + + def test_hour1_with_datetime(self): + from brk_client import _date_to_index + epoch = datetime(2009, 1, 1, tzinfo=timezone.utc) + assert _date_to_index("hour1", epoch) == 0 + assert _date_to_index("hour1", epoch + timedelta(hours=1)) == 1 + assert _date_to_index("hour1", epoch + timedelta(hours=24)) == 24 + + def test_minute5_with_datetime(self): + from brk_client import _date_to_index + epoch = datetime(2009, 1, 1, tzinfo=timezone.utc) + assert _date_to_index("minute5", epoch) == 0 + assert _date_to_index("minute5", epoch + timedelta(minutes=5)) == 1 + assert _date_to_index("minute5", epoch + timedelta(minutes=12)) == 2 # floor + + def test_hour1_with_plain_date(self): + """Plain date is treated as midnight UTC for sub-daily.""" + from brk_client import _date_to_index + # 2009-01-01 as date → midnight UTC → index 0 + assert _date_to_index("hour1", date(2009, 1, 1)) == 0 + # 2009-01-02 as date → midnight UTC → 24 hours later + assert _date_to_index("hour1", date(2009, 1, 2)) == 24 + + def test_roundtrip_day1(self): + """date → index → date roundtrip for day1.""" + from brk_client import _date_to_index, _index_to_date + for i in range(10): + d = _index_to_date("day1", i) + assert _date_to_index("day1", d) == i + + def test_roundtrip_month1(self): + from brk_client import _date_to_index, _index_to_date + for i in range(24): + d = _index_to_date("month1", i) + assert _date_to_index("month1", d) == i + + def test_roundtrip_hour1(self): + from brk_client import _date_to_index, _index_to_date + for i in range(48): + d = _index_to_date("hour1", i) + assert _date_to_index("hour1", d) == i + + +# ============ DateMetricData date methods ============ + + +class TestDateMetricDataMethods: + def test_date_items(self, day1_metric): + items = day1_metric.date_items() + assert items[0] == (date(2009, 1, 3), 100) + assert items[1] == (date(2009, 1, 9), 200) + assert len(items) == 5 + + def test_to_date_dict(self, day1_metric): + d = day1_metric.to_date_dict() + assert d[date(2009, 1, 3)] == 100 + assert d[date(2009, 1, 9)] == 200 + assert len(d) == 5 + # Keys should be date objects + assert type(list(d.keys())[0]) is date + + def test_date_items_sub_daily(self, hour1_metric): + items = hour1_metric.date_items() + assert isinstance(items[0][0], datetime) + assert items[0] == (datetime(2009, 1, 1, 0, 0, 0, tzinfo=timezone.utc), 10.0) + + def test_to_date_dict_sub_daily(self, hour1_metric): + d = hour1_metric.to_date_dict() + key = datetime(2009, 1, 1, 0, 0, 0, tzinfo=timezone.utc) + assert d[key] == 10.0 + assert isinstance(list(d.keys())[0], datetime) + + +# ============ Polars ============ class TestPolarsConversion: - """Test MetricData.to_polars() conversion.""" - @pytest.fixture(autouse=True) def check_polars(self): - """Skip if polars not installed.""" pytest.importorskip("polars") - def test_to_polars_with_dates(self, date_based_metric): - """to_polars() includes date column for date-based index.""" + def test_metric_data_to_polars(self, height_metric): import polars as pl + df = height_metric.to_polars() + assert isinstance(df, pl.DataFrame) + assert list(df.columns) == ["index", "value"] + assert df["index"].to_list() == [800000, 800001, 800002, 800003, 800004] + assert df["value"].to_list() == [1.5, 2.5, 3.5, 4.5, 5.5] - df = date_based_metric.to_polars() + def test_date_metric_to_polars_with_dates(self, day1_metric): + import polars as pl + df = day1_metric.to_polars() assert isinstance(df, pl.DataFrame) assert "date" in df.columns assert "value" in df.columns + assert "index" not in df.columns assert len(df) == 5 assert df["value"].to_list() == [100, 200, 300, 400, 500] - def test_to_polars_without_dates(self, date_based_metric): - """to_polars(with_dates=False) uses index column.""" + def test_date_metric_to_polars_without_dates(self, day1_metric): import polars as pl - - df = date_based_metric.to_polars(with_dates=False) + df = day1_metric.to_polars(with_dates=False) assert "index" in df.columns assert "date" not in df.columns assert df["index"].to_list() == [0, 1, 2, 3, 4] - def test_to_polars_non_date_index(self, height_based_metric): - """to_polars() uses index column for non-date-based index.""" - import polars as pl - - df = height_based_metric.to_polars() - assert "index" in df.columns - assert "date" not in df.columns - assert df["index"].to_list() == [800000, 800001, 800002, 800003, 800004] - assert df["value"].to_list() == [1.5, 2.5, 3.5, 4.5, 5.5] - - def test_to_polars_month1(self, month_based_metric): - """to_polars() works with month1.""" - import polars as pl - - df = month_based_metric.to_polars() + def test_month1_to_polars(self, month1_metric): + df = month1_metric.to_polars() assert "date" in df.columns assert len(df) == 3 dates = df["date"].to_list() assert dates[0] == date(2009, 1, 1) - assert dates[1] == date(2009, 2, 1) assert dates[2] == date(2009, 3, 1) + def test_sub_daily_to_polars(self, hour1_metric): + df = hour1_metric.to_polars() + assert "date" in df.columns + assert len(df) == 3 -# ============ Pandas tests ============ + def test_empty_to_polars(self, empty_metric): + df = empty_metric.to_polars() + assert len(df) == 0 + assert list(df.columns) == ["index", "value"] + + +# ============ Pandas ============ class TestPandasConversion: - """Test MetricData.to_pandas() conversion.""" - @pytest.fixture(autouse=True) def check_pandas(self): - """Skip if pandas not installed.""" pytest.importorskip("pandas") - def test_to_pandas_with_dates(self, date_based_metric): - """to_pandas() includes date column for date-based index.""" + def test_metric_data_to_pandas(self, height_metric): import pandas as pd + df = height_metric.to_pandas() + assert isinstance(df, pd.DataFrame) + assert list(df.columns) == ["index", "value"] + assert df["index"].tolist() == [800000, 800001, 800002, 800003, 800004] + assert df["value"].tolist() == [1.5, 2.5, 3.5, 4.5, 5.5] - df = date_based_metric.to_pandas() + def test_date_metric_to_pandas_with_dates(self, day1_metric): + import pandas as pd + df = day1_metric.to_pandas() assert isinstance(df, pd.DataFrame) assert "date" in df.columns assert "value" in df.columns assert len(df) == 5 assert df["value"].tolist() == [100, 200, 300, 400, 500] - def test_to_pandas_without_dates(self, date_based_metric): - """to_pandas(with_dates=False) uses index column.""" + def test_date_metric_to_pandas_without_dates(self, day1_metric): import pandas as pd - - df = date_based_metric.to_pandas(with_dates=False) + df = day1_metric.to_pandas(with_dates=False) assert "index" in df.columns assert "date" not in df.columns assert df["index"].tolist() == [0, 1, 2, 3, 4] - def test_to_pandas_non_date_index(self, height_based_metric): - """to_pandas() uses index column for non-date-based index.""" - import pandas as pd - - df = height_based_metric.to_pandas() - assert "index" in df.columns - assert "date" not in df.columns - assert df["index"].tolist() == [800000, 800001, 800002, 800003, 800004] - assert df["value"].tolist() == [1.5, 2.5, 3.5, 4.5, 5.5] - - def test_to_pandas_month1(self, month_based_metric): - """to_pandas() works with month1.""" - import pandas as pd - - df = month_based_metric.to_pandas() + def test_month1_to_pandas(self, month1_metric): + df = month1_metric.to_pandas() assert "date" in df.columns assert len(df) == 3 dates = df["date"].tolist() assert dates[0] == date(2009, 1, 1) - assert dates[1] == date(2009, 2, 1) assert dates[2] == date(2009, 3, 1) + + def test_sub_daily_to_pandas(self, hour1_metric): + df = hour1_metric.to_pandas() + assert "date" in df.columns + assert len(df) == 3 + + def test_empty_to_pandas(self, empty_metric): + df = empty_metric.to_pandas() + assert len(df) == 0 + assert list(df.columns) == ["index", "value"] diff --git a/website/scripts/options/distribution/activity.js b/website/scripts/options/distribution/activity.js index f930c3e0e..1a4af3f99 100644 --- a/website/scripts/options/distribution/activity.js +++ b/website/scripts/options/distribution/activity.js @@ -13,45 +13,17 @@ import { Unit } from "../../utils/units.js"; import { line, baseline, dotsBaseline, dots } from "../series.js"; -import { satsBtcUsd, mapCohortsWithAll, flatMapCohortsWithAll } from "../shared.js"; +import { + satsBtcUsd, + mapCohortsWithAll, + flatMapCohortsWithAll, +} from "../shared.js"; import { colors } from "../../utils/colors.js"; // ============================================================================ // Shared Helpers // ============================================================================ -/** - * Create SOPR series from realized pattern (30d > 7d > raw order) - * @param {{ sopr: AnyMetricPattern, sopr7dEma: AnyMetricPattern, sopr30dEma: AnyMetricPattern }} realized - * @param {string} rawName - Name for the raw SOPR series - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function soprSeries(realized, rawName = "SOPR") { - return [ - baseline({ - metric: realized.sopr30dEma, - name: "30d EMA", - color: colors.bi.p3, - unit: Unit.ratio, - base: 1, - }), - baseline({ - metric: realized.sopr7dEma, - name: "7d EMA", - color: colors.bi.p2, - unit: Unit.ratio, - base: 1, - }), - dotsBaseline({ - metric: realized.sopr, - name: rawName, - color: colors.bi.p1, - unit: Unit.ratio, - base: 1, - }), - ]; -} - /** * Create grouped SOPR chart entries (Raw, 7d EMA, 30d EMA) * @template {{ color: Color, name: string }} T @@ -237,18 +209,308 @@ function coinsDestroyedTree(list, all, title) { } // ============================================================================ -// SOPR Helpers +// Rolling Helpers // ============================================================================ /** - * Create SOPR series for single cohort (30d > 7d > raw order) - * @param {UtxoCohortObject | CohortWithoutRelative} cohort - * @returns {AnyFetchedSeriesBlueprint[]} + * Rolling SOPR tree for single cohort + * @param {Object} m + * @param {AnyMetricPattern} m.s24h + * @param {AnyMetricPattern} m.s7d + * @param {AnyMetricPattern} m.s30d + * @param {AnyMetricPattern} m.s1y + * @param {AnyMetricPattern} m.ema24h7d + * @param {AnyMetricPattern} m.ema24h30d + * @param {(metric: string) => string} title + * @param {string} prefix + * @returns {PartialOptionsTree} */ -function createSingleSoprSeries(cohort) { - return soprSeries(cohort.tree.realized); +function singleRollingSoprTree(m, title, prefix = "") { + return [ + { + name: "Compare", + title: title(`Rolling ${prefix}SOPR`), + bottom: [ + baseline({ metric: m.s24h, name: "24h", color: colors.time._24h, unit: Unit.ratio, base: 1 }), + baseline({ metric: m.s7d, name: "7d", color: colors.time._1w, unit: Unit.ratio, base: 1 }), + baseline({ metric: m.s30d, name: "30d", color: colors.time._1m, unit: Unit.ratio, base: 1 }), + baseline({ metric: m.s1y, name: "1y", color: colors.time._1y, unit: Unit.ratio, base: 1 }), + ], + }, + { + name: "24h", + title: title(`${prefix}SOPR (24h)`), + bottom: [ + baseline({ metric: m.ema24h30d, name: "30d EMA", color: colors.bi.p3, unit: Unit.ratio, base: 1 }), + baseline({ metric: m.ema24h7d, name: "7d EMA", color: colors.bi.p2, unit: Unit.ratio, base: 1 }), + dotsBaseline({ metric: m.s24h, name: "24h", color: colors.bi.p1, unit: Unit.ratio, base: 1 }), + ], + }, + { + name: "7d", + title: title(`${prefix}SOPR (7d)`), + bottom: [baseline({ metric: m.s7d, name: "SOPR", unit: Unit.ratio, base: 1 })], + }, + { + name: "30d", + title: title(`${prefix}SOPR (30d)`), + bottom: [baseline({ metric: m.s30d, name: "SOPR", unit: Unit.ratio, base: 1 })], + }, + { + name: "1y", + title: title(`${prefix}SOPR (1y)`), + bottom: [baseline({ metric: m.s1y, name: "SOPR", unit: Unit.ratio, base: 1 })], + }, + ]; } +/** + * Rolling sell side risk tree for single cohort + * @param {AnyRealizedPattern} r + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function singleRollingSellSideRiskTree(r, title) { + return [ + { + name: "Compare", + title: title("Rolling Sell Side Risk"), + bottom: [ + line({ metric: r.sellSideRiskRatio24h, name: "24h", color: colors.time._24h, unit: Unit.ratio }), + line({ metric: r.sellSideRiskRatio7d, name: "7d", color: colors.time._1w, unit: Unit.ratio }), + line({ metric: r.sellSideRiskRatio30d, name: "30d", color: colors.time._1m, unit: Unit.ratio }), + line({ metric: r.sellSideRiskRatio1y, name: "1y", color: colors.time._1y, unit: Unit.ratio }), + ], + }, + { + name: "24h", + title: title("Sell Side Risk (24h)"), + bottom: [ + line({ metric: r.sellSideRiskRatio24h30dEma, name: "30d EMA", color: colors.time._1m, unit: Unit.ratio }), + line({ metric: r.sellSideRiskRatio24h7dEma, name: "7d EMA", color: colors.time._1w, unit: Unit.ratio }), + dots({ metric: r.sellSideRiskRatio24h, name: "Raw", color: colors.bitcoin, unit: Unit.ratio }), + ], + }, + { + name: "7d", + title: title("Sell Side Risk (7d)"), + bottom: [line({ metric: r.sellSideRiskRatio7d, name: "Risk", unit: Unit.ratio })], + }, + { + name: "30d", + title: title("Sell Side Risk (30d)"), + bottom: [line({ metric: r.sellSideRiskRatio30d, name: "Risk", unit: Unit.ratio })], + }, + { + name: "1y", + title: title("Sell Side Risk (1y)"), + bottom: [line({ metric: r.sellSideRiskRatio1y, name: "Risk", unit: Unit.ratio })], + }, + ]; +} + +/** + * Rolling value created/destroyed tree for single cohort + * @param {Object} m + * @param {AnyMetricPattern} m.created24h + * @param {AnyMetricPattern} m.created7d + * @param {AnyMetricPattern} m.created30d + * @param {AnyMetricPattern} m.created1y + * @param {AnyMetricPattern} m.destroyed24h + * @param {AnyMetricPattern} m.destroyed7d + * @param {AnyMetricPattern} m.destroyed30d + * @param {AnyMetricPattern} m.destroyed1y + * @param {(metric: string) => string} title + * @param {string} prefix + * @returns {PartialOptionsTree} + */ +function singleRollingValueTree(m, title, prefix = "") { + return [ + { + name: "Compare", + tree: [ + { + name: "Created", + title: title(`Rolling ${prefix}Value Created`), + bottom: [ + line({ metric: m.created24h, name: "24h", color: colors.time._24h, unit: Unit.usd }), + line({ metric: m.created7d, name: "7d", color: colors.time._1w, unit: Unit.usd }), + line({ metric: m.created30d, name: "30d", color: colors.time._1m, unit: Unit.usd }), + line({ metric: m.created1y, name: "1y", color: colors.time._1y, unit: Unit.usd }), + ], + }, + { + name: "Destroyed", + title: title(`Rolling ${prefix}Value Destroyed`), + bottom: [ + line({ metric: m.destroyed24h, name: "24h", color: colors.time._24h, unit: Unit.usd }), + line({ metric: m.destroyed7d, name: "7d", color: colors.time._1w, unit: Unit.usd }), + line({ metric: m.destroyed30d, name: "30d", color: colors.time._1m, unit: Unit.usd }), + line({ metric: m.destroyed1y, name: "1y", color: colors.time._1y, unit: Unit.usd }), + ], + }, + ], + }, + { + name: "24h", + title: title(`${prefix}Value Created & Destroyed (24h)`), + bottom: [ + line({ metric: m.created24h, name: "Created", color: colors.usd, unit: Unit.usd }), + line({ metric: m.destroyed24h, name: "Destroyed", color: colors.loss, unit: Unit.usd }), + ], + }, + { + name: "7d", + title: title(`${prefix}Value Created & Destroyed (7d)`), + bottom: [ + line({ metric: m.created7d, name: "Created", color: colors.usd, unit: Unit.usd }), + line({ metric: m.destroyed7d, name: "Destroyed", color: colors.loss, unit: Unit.usd }), + ], + }, + { + name: "30d", + title: title(`${prefix}Value Created & Destroyed (30d)`), + bottom: [ + line({ metric: m.created30d, name: "Created", color: colors.usd, unit: Unit.usd }), + line({ metric: m.destroyed30d, name: "Destroyed", color: colors.loss, unit: Unit.usd }), + ], + }, + { + name: "1y", + title: title(`${prefix}Value Created & Destroyed (1y)`), + bottom: [ + line({ metric: m.created1y, name: "Created", color: colors.usd, unit: Unit.usd }), + line({ metric: m.destroyed1y, name: "Destroyed", color: colors.loss, unit: Unit.usd }), + ], + }, + ]; +} + +/** + * Rolling SOPR charts for grouped cohorts + * @template {{ color: Color, name: string }} T + * @param {readonly T[]} list + * @param {T} all + * @param {(item: T) => AnyMetricPattern} get24h + * @param {(item: T) => AnyMetricPattern} get7d + * @param {(item: T) => AnyMetricPattern} get30d + * @param {(item: T) => AnyMetricPattern} get1y + * @param {(metric: string) => string} title + * @param {string} prefix + * @returns {PartialOptionsTree} + */ +function groupedRollingSoprCharts(list, all, get24h, get7d, get30d, get1y, title, prefix = "") { + return [ + { + 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 }), + ), + }, + { + 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 }), + ), + }, + { + 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 }), + ), + }, + { + 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 }), + ), + }, + ]; +} + +/** + * Rolling sell side risk charts for grouped cohorts + * @param {readonly CohortObject[]} list + * @param {CohortObject} all + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function groupedRollingSellSideRiskCharts(list, all, title) { + return [ + { + name: "24h", + title: title("Sell Side Risk (24h)"), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + line({ metric: tree.realized.sellSideRiskRatio24h, name, color, unit: Unit.ratio }), + ), + }, + { + name: "7d", + title: title("Sell Side Risk (7d)"), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + line({ metric: tree.realized.sellSideRiskRatio7d, name, color, unit: Unit.ratio }), + ), + }, + { + name: "30d", + title: title("Sell Side Risk (30d)"), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + line({ metric: tree.realized.sellSideRiskRatio30d, name, color, unit: Unit.ratio }), + ), + }, + { + name: "1y", + title: title("Sell Side Risk (1y)"), + bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => + line({ metric: tree.realized.sellSideRiskRatio1y, name, color, unit: Unit.ratio }), + ), + }, + ]; +} + +/** + * Rolling value created/destroyed charts for grouped cohorts + * @template {{ color: Color, name: string }} T + * @param {readonly T[]} list + * @param {T} all + * @param {readonly { name: string, getCreated: (item: T) => AnyMetricPattern, getDestroyed: (item: T) => AnyMetricPattern }[]} windows + * @param {(metric: string) => string} title + * @param {string} prefix + * @returns {PartialOptionsTree} + */ +function groupedRollingValueCharts(list, all, windows, title, prefix = "") { + return [ + { + name: "Created", + tree: windows.map((w) => ({ + 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 }), + ), + })), + }, + { + name: "Destroyed", + tree: windows.map((w) => ({ + 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 }), + ), + })), + }, + ]; +} + +// ============================================================================ +// SOPR Helpers +// ============================================================================ + /** * Create SOPR tree with normal and adjusted sub-sections * @param {CohortAll | CohortFull | CohortWithAdjusted} cohort @@ -256,23 +518,21 @@ function createSingleSoprSeries(cohort) { * @returns {PartialOptionsTree} */ function createSingleSoprTreeWithAdjusted(cohort, title) { - const { realized } = cohort.tree; + const r = cohort.tree.realized; return [ { name: "Normal", - title: title("SOPR"), - bottom: soprSeries(realized), + tree: singleRollingSoprTree( + { s24h: r.sopr24h, s7d: r.sopr7d, s30d: r.sopr30d, s1y: r.sopr1y, ema24h7d: r.sopr24h7dEma, ema24h30d: r.sopr24h30dEma }, + title, + ), }, { name: "Adjusted", - title: title("Adjusted SOPR"), - bottom: soprSeries( - { - sopr: realized.adjustedSopr, - sopr7dEma: realized.adjustedSopr7dEma, - sopr30dEma: realized.adjustedSopr30dEma, - }, - "Adjusted SOPR", + tree: singleRollingSoprTree( + { s24h: r.adjustedSopr24h, s7d: r.adjustedSopr7d, s30d: r.adjustedSopr30d, s1y: r.adjustedSopr1y, ema24h7d: r.adjustedSopr24h7dEma, ema24h30d: r.adjustedSopr24h30dEma }, + title, + "Adjusted ", ), }, ]; @@ -308,27 +568,56 @@ function createGroupedSoprTreeWithAdjusted(list, all, title) { return [ { name: "Normal", - tree: groupedSoprCharts( - list, - all, - (c) => c.tree.realized.sopr, - (c) => c.tree.realized.sopr7dEma, - (c) => c.tree.realized.sopr30dEma, - title, - "", - ), + tree: [ + ...groupedSoprCharts( + list, + all, + (c) => c.tree.realized.sopr, + (c) => c.tree.realized.sopr7dEma, + (c) => c.tree.realized.sopr30dEma, + title, + "", + ), + { + name: "Rolling", + tree: groupedRollingSoprCharts( + list, + all, + (c) => c.tree.realized.sopr24h, + (c) => c.tree.realized.sopr7d, + (c) => c.tree.realized.sopr30d, + (c) => c.tree.realized.sopr1y, + title, + ), + }, + ], }, { name: "Adjusted", - tree: groupedSoprCharts( - list, - all, - (c) => c.tree.realized.adjustedSopr, - (c) => c.tree.realized.adjustedSopr7dEma, - (c) => c.tree.realized.adjustedSopr30dEma, - title, - "Adjusted ", - ), + tree: [ + ...groupedSoprCharts( + list, + all, + (c) => c.tree.realized.adjustedSopr, + (c) => c.tree.realized.adjustedSopr7dEma, + (c) => c.tree.realized.adjustedSopr30dEma, + title, + "Adjusted ", + ), + { + name: "Rolling", + tree: groupedRollingSoprCharts( + list, + all, + (c) => c.tree.realized.adjustedSopr24h, + (c) => c.tree.realized.adjustedSopr7d, + (c) => c.tree.realized.adjustedSopr30d, + (c) => c.tree.realized.adjustedSopr1y, + title, + "Adjusted ", + ), + }, + ], }, ]; } @@ -344,6 +633,7 @@ function createGroupedSoprTreeWithAdjusted(list, all, title) { * @param {(metric: string) => string} args.title * @param {AnyFetchedSeriesBlueprint[]} [args.valueMetrics] - Optional additional value metrics * @param {PartialOptionsTree} [args.soprTree] - Optional SOPR tree override + * @param {PartialOptionsTree} [args.valueRollingTree] - Optional value rolling tree override * @returns {PartialOptionsGroup} */ export function createActivitySection({ @@ -351,6 +641,7 @@ export function createActivitySection({ title, valueMetrics = [], soprTree, + valueRollingTree, }) { const { tree, color } = cohort; @@ -431,17 +722,18 @@ export function createActivitySection({ }, ], }, - soprTree - ? { name: "SOPR", tree: soprTree } - : { - name: "SOPR", - title: title("SOPR"), - bottom: createSingleSoprSeries(cohort), - }, + { + name: "SOPR", + tree: + soprTree ?? + singleRollingSoprTree( + { s24h: tree.realized.sopr24h, s7d: tree.realized.sopr7d, s30d: tree.realized.sopr30d, s1y: tree.realized.sopr1y, ema24h7d: tree.realized.sopr24h7dEma, ema24h30d: tree.realized.sopr24h30dEma }, + title, + ), + }, { name: "Sell Side Risk", - title: title("Sell Side Risk Ratio"), - bottom: createSingleSellSideRiskSeries(tree), + tree: singleRollingSellSideRiskTree(tree.realized, title), }, { name: "Value", @@ -500,6 +792,20 @@ export function createActivitySection({ }, ], }, + { + name: "Rolling", + tree: + valueRollingTree ?? + singleRollingValueTree( + { + created24h: tree.realized.valueCreated24h, created7d: tree.realized.valueCreated7d, + created30d: tree.realized.valueCreated30d, created1y: tree.realized.valueCreated1y, + destroyed24h: tree.realized.valueDestroyed24h, destroyed7d: tree.realized.valueDestroyed7d, + destroyed30d: tree.realized.valueDestroyed30d, destroyed1y: tree.realized.valueDestroyed1y, + }, + title, + ), + }, ], }, { @@ -574,6 +880,33 @@ export function createActivitySectionWithAdjusted({ cohort, title }) { defaultActive: false, }), ], + valueRollingTree: [ + { + name: "Normal", + tree: singleRollingValueTree( + { + created24h: tree.realized.valueCreated24h, created7d: tree.realized.valueCreated7d, + created30d: tree.realized.valueCreated30d, created1y: tree.realized.valueCreated1y, + destroyed24h: tree.realized.valueDestroyed24h, destroyed7d: tree.realized.valueDestroyed7d, + destroyed30d: tree.realized.valueDestroyed30d, destroyed1y: tree.realized.valueDestroyed1y, + }, + title, + ), + }, + { + name: "Adjusted", + tree: singleRollingValueTree( + { + created24h: tree.realized.adjustedValueCreated24h, created7d: tree.realized.adjustedValueCreated7d, + created30d: tree.realized.adjustedValueCreated30d, created1y: tree.realized.adjustedValueCreated1y, + destroyed24h: tree.realized.adjustedValueDestroyed24h, destroyed7d: tree.realized.adjustedValueDestroyed7d, + destroyed30d: tree.realized.adjustedValueDestroyed30d, destroyed1y: tree.realized.adjustedValueDestroyed1y, + }, + title, + "Adjusted ", + ), + }, + ], }); } @@ -701,16 +1034,45 @@ export function createGroupedActivitySection({ }, { name: "SOPR", - tree: soprTree ?? createGroupedSoprTree(list, all, title), + tree: soprTree ?? [ + ...createGroupedSoprTree(list, all, title), + { + name: "Rolling", + tree: groupedRollingSoprCharts( + list, + all, + (c) => c.tree.realized.sopr24h, + (c) => c.tree.realized.sopr7d, + (c) => c.tree.realized.sopr30d, + (c) => c.tree.realized.sopr1y, + title, + ), + }, + ], }, { name: "Sell Side Risk", - title: title("Sell Side Risk Ratio"), - bottom: createGroupedSellSideRiskSeries(list, all), + tree: groupedRollingSellSideRiskCharts(list, all, title), }, { name: "Value", - tree: valueTree ?? createGroupedValueTree(list, all, title), + tree: valueTree ?? [ + ...createGroupedValueTree(list, all, title), + { + name: "Rolling", + tree: groupedRollingValueCharts( + list, + all, + [ + { name: "24h", getCreated: (c) => c.tree.realized.valueCreated24h, getDestroyed: (c) => c.tree.realized.valueDestroyed24h }, + { name: "7d", getCreated: (c) => c.tree.realized.valueCreated7d, getDestroyed: (c) => c.tree.realized.valueDestroyed7d }, + { name: "30d", getCreated: (c) => c.tree.realized.valueCreated30d, getDestroyed: (c) => c.tree.realized.valueDestroyed30d }, + { name: "1y", getCreated: (c) => c.tree.realized.valueCreated1y, getDestroyed: (c) => c.tree.realized.valueDestroyed1y }, + ], + title, + ), + }, + ], }, { name: "Coins Destroyed", tree: coinsDestroyedTree(list, all, title) }, ], @@ -786,6 +1148,40 @@ function createGroupedValueTreeWithAdjusted(list, all, title) { ], }, { name: "Breakdown", tree: valueBreakdownTree(list, all, title) }, + { + name: "Rolling", + tree: [ + { + name: "Normal", + tree: groupedRollingValueCharts( + list, + all, + [ + { name: "24h", getCreated: (c) => c.tree.realized.valueCreated24h, getDestroyed: (c) => c.tree.realized.valueDestroyed24h }, + { name: "7d", getCreated: (c) => c.tree.realized.valueCreated7d, getDestroyed: (c) => c.tree.realized.valueDestroyed7d }, + { name: "30d", getCreated: (c) => c.tree.realized.valueCreated30d, getDestroyed: (c) => c.tree.realized.valueDestroyed30d }, + { name: "1y", getCreated: (c) => c.tree.realized.valueCreated1y, getDestroyed: (c) => c.tree.realized.valueDestroyed1y }, + ], + title, + ), + }, + { + name: "Adjusted", + tree: groupedRollingValueCharts( + list, + all, + [ + { name: "24h", getCreated: (c) => c.tree.realized.adjustedValueCreated24h, getDestroyed: (c) => c.tree.realized.adjustedValueDestroyed24h }, + { name: "7d", getCreated: (c) => c.tree.realized.adjustedValueCreated7d, getDestroyed: (c) => c.tree.realized.adjustedValueDestroyed7d }, + { name: "30d", getCreated: (c) => c.tree.realized.adjustedValueCreated30d, getDestroyed: (c) => c.tree.realized.adjustedValueDestroyed30d }, + { name: "1y", getCreated: (c) => c.tree.realized.adjustedValueCreated1y, getDestroyed: (c) => c.tree.realized.adjustedValueDestroyed1y }, + ], + title, + "Adjusted ", + ), + }, + ], + }, ]; } @@ -804,50 +1200,6 @@ export function createGroupedActivitySectionWithAdjusted({ list, all, title }) { }); } -/** - * Create sell side risk ratio series for single cohort - * @param {{ realized: AnyRealizedPattern }} tree - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function createSingleSellSideRiskSeries(tree) { - return [ - line({ - metric: tree.realized.sellSideRiskRatio30dEma, - name: "30d EMA", - color: colors.time._1m, - unit: Unit.ratio, - }), - line({ - metric: tree.realized.sellSideRiskRatio7dEma, - name: "7d EMA", - color: colors.time._1w, - unit: Unit.ratio, - }), - dots({ - metric: tree.realized.sellSideRiskRatio, - name: "Raw", - color: colors.bitcoin, - unit: Unit.ratio, - }), - ]; -} - -/** - * Create sell side risk ratio series for grouped cohorts - * @param {readonly CohortObject[]} list - * @param {CohortObject} all - * @returns {AnyFetchedSeriesBlueprint[]} - */ -function createGroupedSellSideRiskSeries(list, all) { - return flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [ - line({ - metric: tree.realized.sellSideRiskRatio, - name, - color, - unit: Unit.ratio, - }), - ]); -} /** * Create value created & destroyed series for single cohort diff --git a/website/scripts/options/distribution/cost-basis.js b/website/scripts/options/distribution/cost-basis.js index 163782f42..0409d9037 100644 --- a/website/scripts/options/distribution/cost-basis.js +++ b/website/scripts/options/distribution/cost-basis.js @@ -238,7 +238,11 @@ export function createGroupedCostBasisSection({ list, all, title }) { * @param {{ list: readonly (CohortAll | CohortFull | CohortWithPercentiles)[], all: CohortAll, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ -export function createGroupedCostBasisSectionWithPercentiles({ list, all, title }) { +export function createGroupedCostBasisSectionWithPercentiles({ + list, + all, + title, +}) { return { name: "Cost Basis", tree: [ diff --git a/website/scripts/options/distribution/data.js b/website/scripts/options/distribution/data.js index 236df6053..28cc35637 100644 --- a/website/scripts/options/distribution/data.js +++ b/website/scripts/options/distribution/data.js @@ -89,17 +89,15 @@ export function buildCohortData() { }); // Age range cohorts - const dateRange = entries(utxoCohorts.ageRange).map( - ([key, tree], i, arr) => { - const names = AGE_RANGE_NAMES[key]; - return { - name: names.short, - title: `UTXOs ${names.long}`, - color: colors.at(i, arr.length), - tree, - }; - }, - ); + const dateRange = entries(utxoCohorts.ageRange).map(([key, tree], i, arr) => { + const names = AGE_RANGE_NAMES[key]; + return { + name: names.short, + title: `UTXOs ${names.long}`, + color: colors.at(i, arr.length), + tree, + }; + }); // Epoch cohorts const epoch = entries(utxoCohorts.epoch).map(([key, tree], i, arr) => { diff --git a/website/scripts/options/distribution/holdings.js b/website/scripts/options/distribution/holdings.js index 60ab0ebe7..8949c3631 100644 --- a/website/scripts/options/distribution/holdings.js +++ b/website/scripts/options/distribution/holdings.js @@ -15,7 +15,13 @@ import { Unit } from "../../utils/units.js"; import { line, baseline } from "../series.js"; -import { satsBtcUsd, satsBtcUsdBaseline, mapCohorts, mapCohortsWithAll, flatMapCohortsWithAll } from "../shared.js"; +import { + satsBtcUsd, + satsBtcUsdBaseline, + mapCohorts, + mapCohortsWithAll, + flatMapCohortsWithAll, +} from "../shared.js"; import { colors } from "../../utils/colors.js"; import { priceLines } from "../constants.js"; @@ -26,10 +32,27 @@ import { priceLines } from "../constants.js"; */ function baseSupplySeries(tree) { return [ - ...satsBtcUsd({ pattern: tree.supply.total, name: "Total", color: colors.default }), - ...satsBtcUsd({ pattern: tree.unrealized.supplyInProfit, name: "In Profit", color: colors.profit }), - ...satsBtcUsd({ pattern: tree.unrealized.supplyInLoss, name: "In Loss", color: colors.loss }), - ...satsBtcUsd({ pattern: tree.supply.halved, name: "Halved", color: colors.gray, style: 4 }), + ...satsBtcUsd({ + pattern: tree.supply.total, + name: "Total", + color: colors.default, + }), + ...satsBtcUsd({ + pattern: tree.unrealized.supplyInProfit, + name: "In Profit", + color: colors.profit, + }), + ...satsBtcUsd({ + pattern: tree.unrealized.supplyInLoss, + name: "In Loss", + color: colors.loss, + }), + ...satsBtcUsd({ + pattern: tree.supply.halved, + name: "Halved", + color: colors.gray, + style: 4, + }), ]; } @@ -40,8 +63,18 @@ function baseSupplySeries(tree) { */ function ownSupplyPctSeries(tree) { return [ - line({ metric: tree.relative.supplyInProfitRelToOwnSupply, name: "In Profit", color: colors.profit, unit: Unit.pctOwn }), - line({ metric: tree.relative.supplyInLossRelToOwnSupply, name: "In Loss", color: colors.loss, unit: Unit.pctOwn }), + line({ + metric: tree.relative.supplyInProfitRelToOwnSupply, + name: "In Profit", + color: colors.profit, + unit: Unit.pctOwn, + }), + line({ + metric: tree.relative.supplyInLossRelToOwnSupply, + name: "In Loss", + color: colors.loss, + unit: Unit.pctOwn, + }), ...priceLines({ numbers: [100, 50, 0], unit: Unit.pctOwn }), ]; } @@ -53,9 +86,24 @@ function ownSupplyPctSeries(tree) { */ function circulatingSupplyPctSeries(tree) { return [ - line({ metric: tree.relative.supplyRelToCirculatingSupply, name: "Total", color: colors.default, unit: Unit.pctSupply }), - line({ metric: tree.relative.supplyInProfitRelToCirculatingSupply, name: "In Profit", color: colors.profit, unit: Unit.pctSupply }), - line({ metric: tree.relative.supplyInLossRelToCirculatingSupply, name: "In Loss", color: colors.loss, unit: Unit.pctSupply }), + line({ + metric: tree.relative.supplyRelToCirculatingSupply, + name: "Total", + color: colors.default, + unit: Unit.pctSupply, + }), + line({ + metric: tree.relative.supplyInProfitRelToCirculatingSupply, + name: "In Profit", + color: colors.profit, + unit: Unit.pctSupply, + }), + line({ + metric: tree.relative.supplyInLossRelToCirculatingSupply, + name: "In Loss", + color: colors.loss, + unit: Unit.pctSupply, + }), ]; } @@ -99,7 +147,12 @@ function grouped30dUtxoCountChangeChart(list, all, title) { name: "UTXO Count", title: title("UTXO Count 30d Change"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => - baseline({ metric: tree.outputs.utxoCount30dChange, name, unit: Unit.count, color }), + baseline({ + metric: tree.outputs.utxoCount30dChange, + name, + unit: Unit.count, + color, + }), ), }; } @@ -157,7 +210,12 @@ function singleAddressCountChart(cohort, title) { name: "Address Count", title: title("Address Count"), bottom: [ - line({ metric: cohort.addrCount.count, name: "Address Count", color: cohort.color, unit: Unit.count }), + line({ + metric: cohort.addrCount.count, + name: "Address Count", + color: cohort.color, + unit: Unit.count, + }), ], }; } @@ -271,10 +329,27 @@ function createSingleSupplySeriesWithRelative(cohort) { function createSingleSupplySeriesWithOwnSupply(cohort) { const { tree } = cohort; return [ - ...satsBtcUsd({ pattern: tree.unrealized.supplyInProfit, name: "In Profit", color: colors.profit }), - ...satsBtcUsd({ pattern: tree.unrealized.supplyInLoss, name: "In Loss", color: colors.loss }), - ...satsBtcUsd({ pattern: tree.supply.total, name: "Total", color: colors.default }), - ...satsBtcUsd({ pattern: tree.supply.halved, name: "Halved", color: colors.gray, style: 4 }), + ...satsBtcUsd({ + pattern: tree.unrealized.supplyInProfit, + name: "In Profit", + color: colors.profit, + }), + ...satsBtcUsd({ + pattern: tree.unrealized.supplyInLoss, + name: "In Loss", + color: colors.loss, + }), + ...satsBtcUsd({ + pattern: tree.supply.total, + name: "Total", + color: colors.default, + }), + ...satsBtcUsd({ + pattern: tree.supply.halved, + name: "Halved", + color: colors.gray, + style: 4, + }), ...ownSupplyPctSeries(tree), ]; } @@ -518,7 +593,12 @@ export function createGroupedHoldingsSectionAddress({ list, all, title }) { name: "Address Count", title: title("Address Count 30d Change"), bottom: mapCohortsWithAll(list, all, ({ name, color, addrCount }) => - baseline({ metric: addrCount._30dChange, name, unit: Unit.count, color }), + baseline({ + metric: addrCount._30dChange, + name, + unit: Unit.count, + color, + }), ), }, ], @@ -532,7 +612,11 @@ export function createGroupedHoldingsSectionAddress({ list, all, title }) { * @param {{ list: readonly AddressCohortObject[], all: CohortAll, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ -export function createGroupedHoldingsSectionAddressAmount({ list, all, title }) { +export function createGroupedHoldingsSectionAddressAmount({ + list, + all, + title, +}) { return { name: "Holdings", tree: [ @@ -635,7 +719,12 @@ export function createGroupedHoldingsSectionAddressAmount({ list, all, title }) name: "Address Count", title: title("Address Count 30d Change"), bottom: mapCohortsWithAll(list, all, ({ name, color, addrCount }) => - baseline({ metric: addrCount._30dChange, name, unit: Unit.count, color }), + baseline({ + metric: addrCount._30dChange, + name, + unit: Unit.count, + color, + }), ), }, ], @@ -703,7 +792,11 @@ export function createGroupedHoldingsSection({ list, all, title }) { * @param {{ list: readonly (CohortAgeRange | CohortBasicWithoutMarketCap)[], all: CohortAll, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ -export function createGroupedHoldingsSectionWithOwnSupply({ list, all, title }) { +export function createGroupedHoldingsSectionWithOwnSupply({ + list, + all, + title, +}) { return { name: "Holdings", tree: [ diff --git a/website/scripts/options/distribution/index.js b/website/scripts/options/distribution/index.js index df6cfbecc..b273dbdea 100644 --- a/website/scripts/options/distribution/index.js +++ b/website/scripts/options/distribution/index.js @@ -321,7 +321,12 @@ export function createAddressCohortFolder(cohort) { * @param {CohortGroupFull} args * @returns {PartialOptionsGroup} */ -export function createGroupedCohortFolderFull({ name, title: groupTitle, list, all }) { +export function createGroupedCohortFolderFull({ + name, + title: groupTitle, + list, + all, +}) { const title = formatCohortTitle(groupTitle); return { name: name || "all", @@ -340,7 +345,12 @@ export function createGroupedCohortFolderFull({ name, title: groupTitle, list, a * @param {CohortGroupWithAdjusted} args * @returns {PartialOptionsGroup} */ -export function createGroupedCohortFolderWithAdjusted({ name, title: groupTitle, list, all }) { +export function createGroupedCohortFolderWithAdjusted({ + name, + title: groupTitle, + list, + all, +}) { const title = formatCohortTitle(groupTitle); return { name: name || "all", @@ -359,7 +369,12 @@ export function createGroupedCohortFolderWithAdjusted({ name, title: groupTitle, * @param {CohortGroupWithNuplPercentiles} args * @returns {PartialOptionsGroup} */ -export function createGroupedCohortFolderWithNupl({ name, title: groupTitle, list, all }) { +export function createGroupedCohortFolderWithNupl({ + name, + title: groupTitle, + list, + all, +}) { const title = formatCohortTitle(groupTitle); return { name: name || "all", @@ -378,7 +393,12 @@ export function createGroupedCohortFolderWithNupl({ name, title: groupTitle, lis * @param {CohortGroupLongTerm} args * @returns {PartialOptionsGroup} */ -export function createGroupedCohortFolderLongTerm({ name, title: groupTitle, list, all }) { +export function createGroupedCohortFolderLongTerm({ + name, + title: groupTitle, + list, + all, +}) { const title = formatCohortTitle(groupTitle); return { name: name || "all", @@ -397,7 +417,12 @@ export function createGroupedCohortFolderLongTerm({ name, title: groupTitle, lis * @param {CohortGroupAgeRange} args * @returns {PartialOptionsGroup} */ -export function createGroupedCohortFolderAgeRange({ name, title: groupTitle, list, all }) { +export function createGroupedCohortFolderAgeRange({ + name, + title: groupTitle, + list, + all, +}) { const title = formatCohortTitle(groupTitle); return { name: name || "all", @@ -406,7 +431,11 @@ export function createGroupedCohortFolderAgeRange({ name, title: groupTitle, lis createGroupedValuationSectionWithOwnMarketCap({ list, all, title }), createGroupedPricesSection({ list, all, title }), createGroupedCostBasisSectionWithPercentiles({ list, all, title }), - createGroupedProfitabilitySectionWithInvestedCapitalPct({ list, all, title }), + createGroupedProfitabilitySectionWithInvestedCapitalPct({ + list, + all, + title, + }), createGroupedActivitySection({ list, all, title }), ], }; @@ -416,7 +445,12 @@ export function createGroupedCohortFolderAgeRange({ name, title: groupTitle, lis * @param {CohortGroupMinAge} args * @returns {PartialOptionsGroup} */ -export function createGroupedCohortFolderMinAge({ name, title: groupTitle, list, all }) { +export function createGroupedCohortFolderMinAge({ + name, + title: groupTitle, + list, + all, +}) { const title = formatCohortTitle(groupTitle); return { name: name || "all", @@ -435,7 +469,12 @@ export function createGroupedCohortFolderMinAge({ name, title: groupTitle, list, * @param {CohortGroupBasicWithMarketCap} args * @returns {PartialOptionsGroup} */ -export function createGroupedCohortFolderBasicWithMarketCap({ name, title: groupTitle, list, all }) { +export function createGroupedCohortFolderBasicWithMarketCap({ + name, + title: groupTitle, + list, + all, +}) { const title = formatCohortTitle(groupTitle); return { name: name || "all", @@ -454,7 +493,12 @@ export function createGroupedCohortFolderBasicWithMarketCap({ name, title: group * @param {CohortGroupBasicWithoutMarketCap} args * @returns {PartialOptionsGroup} */ -export function createGroupedCohortFolderBasicWithoutMarketCap({ name, title: groupTitle, list, all }) { +export function createGroupedCohortFolderBasicWithoutMarketCap({ + name, + title: groupTitle, + list, + all, +}) { const title = formatCohortTitle(groupTitle); return { name: name || "all", @@ -463,7 +507,11 @@ export function createGroupedCohortFolderBasicWithoutMarketCap({ name, title: gr createGroupedValuationSection({ list, all, title }), createGroupedPricesSection({ list, all, title }), createGroupedCostBasisSection({ list, all, title }), - createGroupedProfitabilitySectionBasicWithInvestedCapitalPct({ list, all, title }), + createGroupedProfitabilitySectionBasicWithInvestedCapitalPct({ + list, + all, + title, + }), createGroupedActivitySection({ list, all, title }), ], }; @@ -473,7 +521,12 @@ export function createGroupedCohortFolderBasicWithoutMarketCap({ name, title: gr * @param {CohortGroupAddress} args * @returns {PartialOptionsGroup} */ -export function createGroupedCohortFolderAddress({ name, title: groupTitle, list, all }) { +export function createGroupedCohortFolderAddress({ + name, + title: groupTitle, + list, + all, +}) { const title = formatCohortTitle(groupTitle); return { name: name || "all", @@ -482,7 +535,11 @@ export function createGroupedCohortFolderAddress({ name, title: groupTitle, list createGroupedValuationSection({ list, all, title }), createGroupedPricesSection({ list, all, title }), createGroupedCostBasisSection({ list, all, title }), - createGroupedProfitabilitySectionBasicWithInvestedCapitalPct({ list, all, title }), + createGroupedProfitabilitySectionBasicWithInvestedCapitalPct({ + list, + all, + title, + }), createGroupedActivitySection({ list, all, title }), ], }; @@ -492,7 +549,12 @@ export function createGroupedCohortFolderAddress({ name, title: groupTitle, list * @param {CohortGroupWithoutRelative} args * @returns {PartialOptionsGroup} */ -export function createGroupedCohortFolderWithoutRelative({ name, title: groupTitle, list, all }) { +export function createGroupedCohortFolderWithoutRelative({ + name, + title: groupTitle, + list, + all, +}) { const title = formatCohortTitle(groupTitle); return { name: name || "all", @@ -511,7 +573,12 @@ export function createGroupedCohortFolderWithoutRelative({ name, title: groupTit * @param {AddressCohortGroupObject} args * @returns {PartialOptionsGroup} */ -export function createGroupedAddressCohortFolder({ name, title: groupTitle, list, all }) { +export function createGroupedAddressCohortFolder({ + name, + title: groupTitle, + list, + all, +}) { const title = formatCohortTitle(groupTitle); return { name: name || "all", diff --git a/website/scripts/options/distribution/prices.js b/website/scripts/options/distribution/prices.js index 76295fc7b..a2af05ac6 100644 --- a/website/scripts/options/distribution/prices.js +++ b/website/scripts/options/distribution/prices.js @@ -28,10 +28,30 @@ function createCompareChart(tree, title) { name: "Compare", title: title("Prices"), top: [ - price({ metric: tree.realized.realizedPrice, name: "Realized", color: colors.realized }), - price({ metric: tree.realized.investorPrice, name: "Investor", color: colors.investor }), - price({ metric: tree.realized.upperPriceBand, name: "I²/R", color: colors.stat.max, style: 2, defaultActive: false }), - price({ metric: tree.realized.lowerPriceBand, name: "R²/I", color: colors.stat.min, style: 2, defaultActive: false }), + price({ + metric: tree.realized.realizedPrice, + name: "Realized", + color: colors.realized, + }), + price({ + metric: tree.realized.investorPrice, + name: "Investor", + color: colors.investor, + }), + price({ + metric: tree.realized.upperPriceBand, + name: "I²/R", + color: colors.stat.max, + style: 2, + defaultActive: false, + }), + price({ + metric: tree.realized.lowerPriceBand, + name: "R²/I", + color: colors.stat.min, + style: 2, + defaultActive: false, + }), ], }; } diff --git a/website/scripts/options/distribution/profitability.js b/website/scripts/options/distribution/profitability.js index 1b4d1c9f6..c19b7df0f 100644 --- a/website/scripts/options/distribution/profitability.js +++ b/website/scripts/options/distribution/profitability.js @@ -6,7 +6,13 @@ import { Unit } from "../../utils/units.js"; import { line, baseline, dots, dotsBaseline } from "../series.js"; import { colors } from "../../utils/colors.js"; import { priceLine, priceLines } from "../constants.js"; -import { satsBtcUsd, satsBtcUsdFrom, mapCohorts, mapCohortsWithAll, flatMapCohortsWithAll } from "../shared.js"; +import { + satsBtcUsd, + satsBtcUsdFrom, + mapCohorts, + mapCohortsWithAll, + flatMapCohortsWithAll, +} from "../shared.js"; // ============================================================================ // Core Series Builders (Composable Primitives) @@ -28,13 +34,33 @@ import { satsBtcUsd, satsBtcUsdFrom, mapCohorts, mapCohortsWithAll, flatMapCohor */ function pnlLines(metrics, unit) { const series = [ - line({ metric: metrics.profit, name: "Profit", color: colors.profit, unit }), + line({ + metric: metrics.profit, + name: "Profit", + color: colors.profit, + unit, + }), line({ metric: metrics.loss, name: "Loss", color: colors.loss, unit }), ]; if (metrics.total) { - series.push(line({ metric: metrics.total, name: "Total", color: colors.default, unit })); + series.push( + line({ + metric: metrics.total, + name: "Total", + color: colors.default, + unit, + }), + ); } - series.push(line({ metric: metrics.negLoss, name: "Negative Loss", color: colors.loss, unit, defaultActive: false })); + series.push( + line({ + metric: metrics.negLoss, + name: "Negative Loss", + color: colors.loss, + unit, + defaultActive: false, + }), + ); return series; } @@ -83,7 +109,10 @@ function getUnrealizedMetrics(tree) { */ function unrealizedUsd(m) { return [ - ...pnlLines({ profit: m.profit, loss: m.loss, negLoss: m.negLoss, total: m.total }, Unit.usd), + ...pnlLines( + { profit: m.profit, loss: m.loss, negLoss: m.negLoss, total: m.total }, + Unit.usd, + ), priceLine({ unit: Unit.usd, defaultActive: false }), ]; } @@ -708,6 +737,166 @@ function sentInPnlTree(tree, title) { ]; } +// ============================================================================ +// Rolling Realized Helpers +// ============================================================================ + +/** + * Rolling realized value tree for single cohort (available on all realized patterns) + * @param {AnyRealizedPattern} r + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function singleRollingRealizedValueTree(r, title) { + return [ + { + name: "Compare", + title: title("Rolling Realized Value"), + bottom: [ + line({ metric: r.realizedValue24h, name: "24h", color: colors.time._24h, unit: Unit.usd }), + line({ metric: r.realizedValue7d, name: "7d", color: colors.time._1w, unit: Unit.usd }), + line({ metric: r.realizedValue30d, name: "30d", color: colors.time._1m, unit: Unit.usd }), + line({ metric: r.realizedValue1y, name: "1y", color: colors.time._1y, unit: Unit.usd }), + ], + }, + { name: "24h", title: title("Realized Value (24h)"), bottom: [line({ metric: r.realizedValue24h, name: "Value", unit: Unit.usd })] }, + { name: "7d", title: title("Realized Value (7d)"), bottom: [line({ metric: r.realizedValue7d, name: "Value", unit: Unit.usd })] }, + { name: "30d", title: title("Realized Value (30d)"), bottom: [line({ metric: r.realizedValue30d, name: "Value", unit: Unit.usd })] }, + { name: "1y", title: title("Realized Value (1y)"), bottom: [line({ metric: r.realizedValue1y, name: "Value", unit: Unit.usd })] }, + ]; +} + +/** + * Rolling realized tree with P/L for single cohort (for RealizedWithExtras patterns) + * @param {RealizedWithExtras} r + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function singleRollingRealizedTreeWithExtras(r, title) { + return [ + { + name: "Value", + tree: singleRollingRealizedValueTree(r, title), + }, + { + name: "Profit", + tree: [ + { + name: "Compare", + title: title("Rolling Realized Profit"), + bottom: [ + line({ metric: r.realizedProfit24h, name: "24h", color: colors.time._24h, unit: Unit.usd }), + line({ metric: r.realizedProfit7d, name: "7d", color: colors.time._1w, unit: Unit.usd }), + line({ metric: r.realizedProfit30d, name: "30d", color: colors.time._1m, unit: Unit.usd }), + line({ metric: r.realizedProfit1y, name: "1y", color: colors.time._1y, unit: Unit.usd }), + ], + }, + { name: "24h", title: title("Realized Profit (24h)"), bottom: [line({ metric: r.realizedProfit24h, name: "Profit", color: colors.profit, unit: Unit.usd })] }, + { name: "7d", title: title("Realized Profit (7d)"), bottom: [line({ metric: r.realizedProfit7d, name: "Profit", color: colors.profit, unit: Unit.usd })] }, + { name: "30d", title: title("Realized Profit (30d)"), bottom: [line({ metric: r.realizedProfit30d, name: "Profit", color: colors.profit, unit: Unit.usd })] }, + { name: "1y", title: title("Realized Profit (1y)"), bottom: [line({ metric: r.realizedProfit1y, name: "Profit", color: colors.profit, unit: Unit.usd })] }, + ], + }, + { + name: "Loss", + tree: [ + { + name: "Compare", + title: title("Rolling Realized Loss"), + bottom: [ + line({ metric: r.realizedLoss24h, name: "24h", color: colors.time._24h, unit: Unit.usd }), + line({ metric: r.realizedLoss7d, name: "7d", color: colors.time._1w, unit: Unit.usd }), + line({ metric: r.realizedLoss30d, name: "30d", color: colors.time._1m, unit: Unit.usd }), + line({ metric: r.realizedLoss1y, name: "1y", color: colors.time._1y, unit: Unit.usd }), + ], + }, + { name: "24h", title: title("Realized Loss (24h)"), bottom: [line({ metric: r.realizedLoss24h, name: "Loss", color: colors.loss, unit: Unit.usd })] }, + { name: "7d", title: title("Realized Loss (7d)"), bottom: [line({ metric: r.realizedLoss7d, name: "Loss", color: colors.loss, unit: Unit.usd })] }, + { name: "30d", title: title("Realized Loss (30d)"), bottom: [line({ metric: r.realizedLoss30d, name: "Loss", color: colors.loss, unit: Unit.usd })] }, + { name: "1y", title: title("Realized Loss (1y)"), bottom: [line({ metric: r.realizedLoss1y, name: "Loss", color: colors.loss, unit: Unit.usd })] }, + ], + }, + { + name: "P/L Ratio", + tree: [ + { + name: "Compare", + title: title("Rolling Realized P/L Ratio"), + bottom: [ + baseline({ metric: r.realizedProfitToLossRatio24h, name: "24h", color: colors.time._24h, unit: Unit.ratio }), + baseline({ metric: r.realizedProfitToLossRatio7d, name: "7d", color: colors.time._1w, unit: Unit.ratio }), + baseline({ metric: r.realizedProfitToLossRatio30d, name: "30d", color: colors.time._1m, unit: Unit.ratio }), + baseline({ metric: r.realizedProfitToLossRatio1y, name: "1y", color: colors.time._1y, unit: Unit.ratio }), + ], + }, + { name: "24h", title: title("Realized P/L Ratio (24h)"), bottom: [baseline({ metric: r.realizedProfitToLossRatio24h, name: "P/L Ratio", unit: Unit.ratio })] }, + { name: "7d", title: title("Realized P/L Ratio (7d)"), bottom: [baseline({ metric: r.realizedProfitToLossRatio7d, name: "P/L Ratio", unit: Unit.ratio })] }, + { name: "30d", title: title("Realized P/L Ratio (30d)"), bottom: [baseline({ metric: r.realizedProfitToLossRatio30d, name: "P/L Ratio", unit: Unit.ratio })] }, + { name: "1y", title: title("Realized P/L Ratio (1y)"), bottom: [baseline({ metric: r.realizedProfitToLossRatio1y, name: "P/L Ratio", unit: Unit.ratio })] }, + ], + }, + ]; +} + +/** + * Grouped rolling realized value charts (available on all realized patterns) + * @param {readonly CohortObject[]} list + * @param {CohortObject} all + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function groupedRollingRealizedValueCharts(list, all, title) { + return [ + { name: "24h", title: title("Realized Value (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedValue24h, name, color, unit: Unit.usd })) }, + { name: "7d", title: title("Realized Value (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedValue7d, name, color, unit: Unit.usd })) }, + { name: "30d", title: title("Realized Value (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedValue30d, name, color, unit: Unit.usd })) }, + { name: "1y", title: title("Realized Value (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedValue1y, name, color, unit: Unit.usd })) }, + ]; +} + +/** + * Grouped rolling realized charts with P/L (for RealizedWithExtras cohorts) + * @param {readonly (CohortAgeRange | CohortLongTerm | CohortAll | CohortFull)[]} list + * @param {CohortAll} all + * @param {(metric: string) => string} title + * @returns {PartialOptionsTree} + */ +function groupedRollingRealizedChartsWithExtras(list, all, title) { + return [ + { + name: "Value", + tree: groupedRollingRealizedValueCharts(list, all, title), + }, + { + name: "Profit", + tree: [ + { name: "24h", title: title("Realized Profit (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedProfit24h, name, color, unit: Unit.usd })) }, + { name: "7d", title: title("Realized Profit (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedProfit7d, name, color, unit: Unit.usd })) }, + { name: "30d", title: title("Realized Profit (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedProfit30d, name, color, unit: Unit.usd })) }, + { name: "1y", title: title("Realized Profit (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedProfit1y, name, color, unit: Unit.usd })) }, + ], + }, + { + name: "Loss", + tree: [ + { name: "24h", title: title("Realized Loss (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedLoss24h, name, color, unit: Unit.usd })) }, + { name: "7d", title: title("Realized Loss (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedLoss7d, name, color, unit: Unit.usd })) }, + { name: "30d", title: title("Realized Loss (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedLoss30d, name, color, unit: Unit.usd })) }, + { name: "1y", title: title("Realized Loss (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => line({ metric: tree.realized.realizedLoss1y, name, color, unit: Unit.usd })) }, + ], + }, + { + name: "P/L Ratio", + tree: [ + { name: "24h", title: title("Realized P/L Ratio (24h)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ metric: tree.realized.realizedProfitToLossRatio24h, name, color, unit: Unit.ratio })) }, + { name: "7d", title: title("Realized P/L Ratio (7d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ metric: tree.realized.realizedProfitToLossRatio7d, name, color, unit: Unit.ratio })) }, + { name: "30d", title: title("Realized P/L Ratio (30d)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ metric: tree.realized.realizedProfitToLossRatio30d, name, color, unit: Unit.ratio })) }, + { name: "1y", title: title("Realized P/L Ratio (1y)"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ metric: tree.realized.realizedProfitToLossRatio1y, name, color, unit: Unit.ratio })) }, + ], + }, + ]; +} + // ============================================================================ // Realized Subfolder Builders // ============================================================================ @@ -716,9 +905,10 @@ function sentInPnlTree(tree, title) { * Base realized subfolder (no P/L ratio) * @param {{ realized: AnyRealizedPattern }} tree * @param {(metric: string) => string} title + * @param {PartialOptionsTree} [rollingTree] * @returns {PartialOptionsGroup} */ -function realizedSubfolder(tree, title) { +function realizedSubfolder(tree, title, rollingTree) { const r = tree.realized; return { name: "Realized", @@ -761,6 +951,10 @@ function realizedSubfolder(tree, title) { }), ], }, + { + name: "Rolling", + tree: rollingTree ?? singleRollingRealizedValueTree(r, title), + }, { name: "Cumulative", tree: [ @@ -797,21 +991,21 @@ function realizedSubfolder(tree, title) { } /** - * Realized subfolder with P/L ratio + * Realized subfolder with P/L ratio and rolling P/L * @param {{ realized: RealizedWithExtras }} tree * @param {(metric: string) => string} title * @returns {PartialOptionsGroup} */ function realizedSubfolderWithExtras(tree, title) { - const base = realizedSubfolder(tree, title); const r = tree.realized; + const base = realizedSubfolder(tree, title, singleRollingRealizedTreeWithExtras(r, title)); // Insert P/L Ratio after Total (index 3) base.tree.splice(4, 0, { name: "P/L Ratio", title: title("Realized Profit/Loss Ratio"), bottom: [ baseline({ - metric: r.realizedProfitToLossRatio, + metric: r.realizedProfitToLossRatio1y, name: "P/L Ratio", unit: Unit.ratio, }), @@ -1706,7 +1900,7 @@ function groupedRealizedPnlSumWithExtras(list, all, title) { title: title("Realized Profit/Loss Ratio"), bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) => baseline({ - metric: tree.realized.realizedProfitToLossRatio, + metric: tree.realized.realizedProfitToLossRatio1y, name, color, unit: Unit.ratio, @@ -1929,6 +2123,10 @@ function groupedRealizedSubfolder(list, all, title) { }), ), }, + { + name: "Rolling", + tree: groupedRollingRealizedValueCharts(list, all, title), + }, { name: "Cumulative", tree: [ @@ -1987,6 +2185,10 @@ function groupedRealizedSubfolderWithExtras(list, all, title) { }), ), }, + { + name: "Rolling", + tree: groupedRollingRealizedChartsWithExtras(list, all, title), + }, { name: "Cumulative", tree: [ @@ -2050,7 +2252,10 @@ export function createGroupedProfitabilitySectionBasicWithInvestedCapitalPct({ { name: "Unrealized", tree: groupedPnlCharts(list, all, title) }, groupedRealizedSubfolder(list, all, title), { name: "Volume", tree: groupedSentInPnl(list, all, title) }, - { name: "Invested Capital", tree: groupedInvestedCapital(list, all, title) }, + { + name: "Invested Capital", + tree: groupedInvestedCapital(list, all, title), + }, groupedSentiment(list, all, title), ], }; @@ -2089,7 +2294,10 @@ export function createGroupedProfitabilitySectionWithInvestedCapitalPct({ }, groupedRealizedSubfolderWithExtras(list, all, title), { name: "Volume", tree: groupedSentInPnl(list, all, title) }, - { name: "Invested Capital", tree: groupedInvestedCapital(list, all, title) }, + { + name: "Invested Capital", + tree: groupedInvestedCapital(list, all, title), + }, groupedSentiment(list, all, title), ], }; @@ -2100,7 +2308,11 @@ export function createGroupedProfitabilitySectionWithInvestedCapitalPct({ * @param {{ list: readonly (CohortFull | CohortBasicWithMarketCap)[], all: CohortAll, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ -export function createGroupedProfitabilitySectionWithNupl({ list, all, title }) { +export function createGroupedProfitabilitySectionWithNupl({ + list, + all, + title, +}) { return { name: "Profitability", tree: [ @@ -2124,7 +2336,10 @@ export function createGroupedProfitabilitySectionWithNupl({ list, all, title }) }, groupedRealizedSubfolder(list, all, title), { name: "Volume", tree: groupedSentInPnl(list, all, title) }, - { name: "Invested Capital", tree: groupedInvestedCapital(list, all, title) }, + { + name: "Invested Capital", + tree: groupedInvestedCapital(list, all, title), + }, groupedSentiment(list, all, title), ], }; @@ -2135,7 +2350,11 @@ export function createGroupedProfitabilitySectionWithNupl({ list, all, title }) * @param {{ list: readonly CohortLongTerm[], all: CohortAll, title: (metric: string) => string }} args * @returns {PartialOptionsGroup} */ -export function createGroupedProfitabilitySectionLongTerm({ list, all, title }) { +export function createGroupedProfitabilitySectionLongTerm({ + list, + all, + title, +}) { return { name: "Profitability", tree: [ @@ -2181,7 +2400,10 @@ export function createGroupedProfitabilitySectionLongTerm({ list, all, title }) }, groupedRealizedSubfolderWithExtras(list, all, title), { name: "Volume", tree: groupedSentInPnl(list, all, title) }, - { name: "Invested Capital", tree: groupedInvestedCapital(list, all, title) }, + { + name: "Invested Capital", + tree: groupedInvestedCapital(list, all, title), + }, groupedSentiment(list, all, title), ], }; @@ -2242,7 +2464,10 @@ export function createGroupedProfitabilitySectionWithPeakRegret({ }, groupedRealizedSubfolder(list, all, title), { name: "Volume", tree: groupedSentInPnl(list, all, title) }, - { name: "Invested Capital", tree: groupedInvestedCapital(list, all, title) }, + { + name: "Invested Capital", + tree: groupedInvestedCapital(list, all, title), + }, groupedSentiment(list, all, title), ], }; diff --git a/website/scripts/options/market.js b/website/scripts/options/market.js index 60ac9f1a9..7f311806a 100644 --- a/website/scripts/options/market.js +++ b/website/scripts/options/market.js @@ -193,6 +193,7 @@ export function createMarketSection() { range, indicators, lookback, + dca, } = market; const shortPeriodsBase = [ @@ -671,45 +672,297 @@ export function createMarketSection() { tree: [ { name: "RSI", - title: "RSI (14d)", - bottom: [ - line({ - metric: indicators.rsi._1d.rsi, - name: "RSI", - color: colors.indicator.main, - unit: Unit.index, - }), - line({ - metric: indicators.rsi._1d.rsiMax, - name: "Max", - color: colors.stat.max, - defaultActive: false, - unit: Unit.index, - }), - line({ - metric: indicators.rsi._1d.rsiMin, - name: "Min", - color: colors.stat.min, - defaultActive: false, - unit: Unit.index, - }), - priceLine({ unit: Unit.index, number: 70 }), - priceLine({ unit: Unit.index, number: 50, defaultActive: false }), - priceLine({ unit: Unit.index, number: 30 }), + tree: [ + { + name: "Compare", + title: "RSI Comparison", + bottom: [ + line({ + metric: indicators.rsi._1d.rsi, + name: "1d", + color: colors.time._24h, + unit: Unit.index, + }), + line({ + metric: indicators.rsi._1w.rsi, + name: "1w", + color: colors.time._1w, + unit: Unit.index, + }), + line({ + metric: indicators.rsi._1m.rsi, + name: "1m", + color: colors.time._1m, + unit: Unit.index, + }), + line({ + metric: indicators.rsi._1y.rsi, + name: "1y", + color: colors.time._1y, + unit: Unit.index, + }), + priceLine({ unit: Unit.index, number: 70 }), + priceLine({ unit: Unit.index, number: 30 }), + ], + }, + { + name: "1 Day", + title: "RSI (1d)", + bottom: [ + line({ + metric: indicators.rsi._1d.rsi, + name: "RSI", + color: colors.indicator.main, + unit: Unit.index, + }), + line({ + metric: indicators.rsi._1d.rsiMax, + name: "Max", + color: colors.stat.max, + defaultActive: false, + unit: Unit.index, + }), + line({ + metric: indicators.rsi._1d.rsiMin, + name: "Min", + color: colors.stat.min, + defaultActive: false, + unit: Unit.index, + }), + priceLine({ unit: Unit.index, number: 70 }), + priceLine({ + unit: Unit.index, + number: 50, + defaultActive: false, + }), + priceLine({ unit: Unit.index, number: 30 }), + ], + }, + { + name: "1 Week", + title: "RSI (1w)", + bottom: [ + line({ + metric: indicators.rsi._1w.rsi, + name: "RSI", + color: colors.indicator.main, + unit: Unit.index, + }), + line({ + metric: indicators.rsi._1w.rsiMax, + name: "Max", + color: colors.stat.max, + defaultActive: false, + unit: Unit.index, + }), + line({ + metric: indicators.rsi._1w.rsiMin, + name: "Min", + color: colors.stat.min, + defaultActive: false, + unit: Unit.index, + }), + priceLine({ unit: Unit.index, number: 70 }), + priceLine({ + unit: Unit.index, + number: 50, + defaultActive: false, + }), + priceLine({ unit: Unit.index, number: 30 }), + ], + }, + { + name: "1 Month", + title: "RSI (1m)", + bottom: [ + line({ + metric: indicators.rsi._1m.rsi, + name: "RSI", + color: colors.indicator.main, + unit: Unit.index, + }), + line({ + metric: indicators.rsi._1m.rsiMax, + name: "Max", + color: colors.stat.max, + defaultActive: false, + unit: Unit.index, + }), + line({ + metric: indicators.rsi._1m.rsiMin, + name: "Min", + color: colors.stat.min, + defaultActive: false, + unit: Unit.index, + }), + priceLine({ unit: Unit.index, number: 70 }), + priceLine({ + unit: Unit.index, + number: 50, + defaultActive: false, + }), + priceLine({ unit: Unit.index, number: 30 }), + ], + }, + { + name: "1 Year", + title: "RSI (1y)", + bottom: [ + line({ + metric: indicators.rsi._1y.rsi, + name: "RSI", + color: colors.indicator.main, + unit: Unit.index, + }), + line({ + metric: indicators.rsi._1y.rsiMax, + name: "Max", + color: colors.stat.max, + defaultActive: false, + unit: Unit.index, + }), + line({ + metric: indicators.rsi._1y.rsiMin, + name: "Min", + color: colors.stat.min, + defaultActive: false, + unit: Unit.index, + }), + priceLine({ unit: Unit.index, number: 70 }), + priceLine({ + unit: Unit.index, + number: 50, + defaultActive: false, + }), + priceLine({ unit: Unit.index, number: 30 }), + ], + }, ], }, { name: "StochRSI", - title: "Stochastic RSI", + tree: [ + { + name: "Compare", + title: "Stochastic RSI Comparison", + bottom: [ + line({ + metric: indicators.rsi._1d.stochRsiK, + name: "1d K", + color: colors.time._24h, + unit: Unit.index, + }), + line({ + metric: indicators.rsi._1w.stochRsiK, + name: "1w K", + color: colors.time._1w, + unit: Unit.index, + }), + line({ + metric: indicators.rsi._1m.stochRsiK, + name: "1m K", + color: colors.time._1m, + unit: Unit.index, + }), + line({ + metric: indicators.rsi._1y.stochRsiK, + name: "1y K", + color: colors.time._1y, + unit: Unit.index, + }), + ...priceLines({ unit: Unit.index, numbers: [80, 20] }), + ], + }, + { + name: "1 Day", + title: "Stochastic RSI (1d)", + bottom: [ + line({ + metric: indicators.rsi._1d.stochRsiK, + name: "K", + color: colors.indicator.fast, + unit: Unit.index, + }), + line({ + metric: indicators.rsi._1d.stochRsiD, + name: "D", + color: colors.indicator.slow, + unit: Unit.index, + }), + ...priceLines({ unit: Unit.index, numbers: [80, 20] }), + ], + }, + { + name: "1 Week", + title: "Stochastic RSI (1w)", + bottom: [ + line({ + metric: indicators.rsi._1w.stochRsiK, + name: "K", + color: colors.indicator.fast, + unit: Unit.index, + }), + line({ + metric: indicators.rsi._1w.stochRsiD, + name: "D", + color: colors.indicator.slow, + unit: Unit.index, + }), + ...priceLines({ unit: Unit.index, numbers: [80, 20] }), + ], + }, + { + name: "1 Month", + title: "Stochastic RSI (1m)", + bottom: [ + line({ + metric: indicators.rsi._1m.stochRsiK, + name: "K", + color: colors.indicator.fast, + unit: Unit.index, + }), + line({ + metric: indicators.rsi._1m.stochRsiD, + name: "D", + color: colors.indicator.slow, + unit: Unit.index, + }), + ...priceLines({ unit: Unit.index, numbers: [80, 20] }), + ], + }, + { + name: "1 Year", + title: "Stochastic RSI (1y)", + bottom: [ + line({ + metric: indicators.rsi._1y.stochRsiK, + name: "K", + color: colors.indicator.fast, + unit: Unit.index, + }), + line({ + metric: indicators.rsi._1y.stochRsiD, + name: "D", + color: colors.indicator.slow, + unit: Unit.index, + }), + ...priceLines({ unit: Unit.index, numbers: [80, 20] }), + ], + }, + ], + }, + { + name: "Stochastic", + title: "Stochastic Oscillator", bottom: [ line({ - metric: indicators.rsi._1d.stochRsiK, + metric: indicators.stochK, name: "K", color: colors.indicator.fast, unit: Unit.index, }), line({ - metric: indicators.rsi._1d.stochRsiD, + metric: indicators.stochD, name: "D", color: colors.indicator.slow, unit: Unit.index, @@ -719,25 +972,129 @@ export function createMarketSection() { }, { name: "MACD", - title: "MACD", - bottom: [ - line({ - metric: indicators.macd._1d.line, - name: "MACD", - color: colors.indicator.fast, - unit: Unit.usd, - }), - line({ - metric: indicators.macd._1d.signal, - name: "Signal", - color: colors.indicator.slow, - unit: Unit.usd, - }), - histogram({ - metric: indicators.macd._1d.histogram, - name: "Histogram", - unit: Unit.usd, - }), + tree: [ + { + name: "Compare", + title: "MACD Comparison", + bottom: [ + line({ + metric: indicators.macd._1d.line, + name: "1d", + color: colors.time._24h, + unit: Unit.usd, + }), + line({ + metric: indicators.macd._1w.line, + name: "1w", + color: colors.time._1w, + unit: Unit.usd, + }), + line({ + metric: indicators.macd._1m.line, + name: "1m", + color: colors.time._1m, + unit: Unit.usd, + }), + line({ + metric: indicators.macd._1y.line, + name: "1y", + color: colors.time._1y, + unit: Unit.usd, + }), + ], + }, + { + name: "1 Day", + title: "MACD (1d)", + bottom: [ + line({ + metric: indicators.macd._1d.line, + name: "MACD", + color: colors.indicator.fast, + unit: Unit.usd, + }), + line({ + metric: indicators.macd._1d.signal, + name: "Signal", + color: colors.indicator.slow, + unit: Unit.usd, + }), + histogram({ + metric: indicators.macd._1d.histogram, + name: "Histogram", + unit: Unit.usd, + }), + ], + }, + { + name: "1 Week", + title: "MACD (1w)", + bottom: [ + line({ + metric: indicators.macd._1w.line, + name: "MACD", + color: colors.indicator.fast, + unit: Unit.usd, + }), + line({ + metric: indicators.macd._1w.signal, + name: "Signal", + color: colors.indicator.slow, + unit: Unit.usd, + }), + histogram({ + metric: indicators.macd._1w.histogram, + name: "Histogram", + unit: Unit.usd, + }), + ], + }, + { + name: "1 Month", + title: "MACD (1m)", + bottom: [ + line({ + metric: indicators.macd._1m.line, + name: "MACD", + color: colors.indicator.fast, + unit: Unit.usd, + }), + line({ + metric: indicators.macd._1m.signal, + name: "Signal", + color: colors.indicator.slow, + unit: Unit.usd, + }), + histogram({ + metric: indicators.macd._1m.histogram, + name: "Histogram", + unit: Unit.usd, + }), + ], + }, + { + name: "1 Year", + title: "MACD (1y)", + bottom: [ + line({ + metric: indicators.macd._1y.line, + name: "MACD", + color: colors.indicator.fast, + unit: Unit.usd, + }), + line({ + metric: indicators.macd._1y.signal, + name: "Signal", + color: colors.indicator.slow, + unit: Unit.usd, + }), + histogram({ + metric: indicators.macd._1y.histogram, + name: "Histogram", + unit: Unit.usd, + }), + ], + }, ], }, ], @@ -763,6 +1120,18 @@ export function createMarketSection() { ], }, + { + name: "DCA", + title: "Dollar Cost Average Sats/Day", + bottom: [ + line({ + metric: dca.dcaSatsPerDay, + name: "Sats/Day", + unit: Unit.sats, + }), + ], + }, + { name: "Indicators", tree: [ diff --git a/website/scripts/options/mining.js b/website/scripts/options/mining.js index c83e0ceb3..9b073c6ce 100644 --- a/website/scripts/options/mining.js +++ b/website/scripts/options/mining.js @@ -77,27 +77,27 @@ export function createMiningSection() { title: `Dominance: ${name}`, bottom: [ dots({ - metric: pool._24hDominance, + metric: pool.dominance24h, name: "24h", color: colors.time._24h, unit: Unit.percentage, defaultActive: false, }), line({ - metric: pool._1wDominance, + metric: pool.dominance1w, name: "1w", color: colors.time._1w, unit: Unit.percentage, defaultActive: false, }), line({ - metric: pool._1mDominance, + metric: pool.dominance1m, name: "1m", color: colors.time._1m, unit: Unit.percentage, }), line({ - metric: pool._1yDominance, + metric: pool.dominance1y, name: "1y", color: colors.time._1y, unit: Unit.percentage, @@ -125,28 +125,28 @@ export function createMiningSection() { unit: Unit.count, }), line({ - metric: pool._24hBlocksMined, + metric: pool.blocksMined24hSum, name: "24h", color: colors.time._24h, unit: Unit.count, defaultActive: false, }), line({ - metric: pool._1wBlocksMined, + metric: pool.blocksMined1wSum, name: "1w", color: colors.time._1w, unit: Unit.count, defaultActive: false, }), line({ - metric: pool._1mBlocksMined, + metric: pool.blocksMined1mSum, name: "1m", color: colors.time._1m, unit: Unit.count, defaultActive: false, }), line({ - metric: pool._1yBlocksMined, + metric: pool.blocksMined1ySum, name: "1y", color: colors.time._1y, unit: Unit.count, @@ -406,13 +406,80 @@ export function createMiningSection() { name: "sum", }), ...satsBtcUsd({ - pattern: mining.rewards._24hCoinbaseSum, + pattern: mining.rewards.coinbase24hSum, name: "24h", color: colors.time._24h, defaultActive: false, }), ], }, + { + name: "Rolling", + tree: [ + { + name: "Compare", + title: "Coinbase Rolling Sum", + bottom: [ + ...satsBtcUsd({ + pattern: mining.rewards.coinbase24hSum, + name: "24h", + color: colors.time._24h, + }), + ...satsBtcUsd({ + pattern: mining.rewards.coinbase7dSum, + name: "7d", + color: colors.time._1w, + }), + ...satsBtcUsd({ + pattern: mining.rewards.coinbase30dSum, + name: "30d", + color: colors.time._1m, + }), + ...satsBtcUsd({ + pattern: mining.rewards.coinbase1ySum, + name: "1y", + color: colors.time._1y, + }), + ], + }, + { + name: "24h", + title: "Coinbase 24h Rolling Sum", + bottom: satsBtcUsd({ + pattern: mining.rewards.coinbase24hSum, + name: "24h", + color: colors.time._24h, + }), + }, + { + name: "7d", + title: "Coinbase 7d Rolling Sum", + bottom: satsBtcUsd({ + pattern: mining.rewards.coinbase7dSum, + name: "7d", + color: colors.time._1w, + }), + }, + { + name: "30d", + title: "Coinbase 30d Rolling Sum", + bottom: satsBtcUsd({ + pattern: mining.rewards.coinbase30dSum, + name: "30d", + color: colors.time._1m, + }), + }, + { + name: "1y", + title: "Coinbase 1y Rolling Sum", + bottom: satsBtcUsd({ + pattern: mining.rewards.coinbase1ySum, + name: "1y", + color: colors.time._1y, + }), + }, + ], + }, { name: "Distribution", title: "Coinbase Rewards per Block Distribution", @@ -483,6 +550,73 @@ export function createMiningSection() { name: "sum", }), }, + { + name: "Rolling", + tree: [ + { + name: "Compare", + title: "Fee Rolling Sum", + bottom: [ + ...satsBtcUsd({ + pattern: mining.rewards.fee24hSum, + name: "24h", + color: colors.time._24h, + }), + ...satsBtcUsd({ + pattern: mining.rewards.fee7dSum, + name: "7d", + color: colors.time._1w, + }), + ...satsBtcUsd({ + pattern: mining.rewards.fee30dSum, + name: "30d", + color: colors.time._1m, + }), + ...satsBtcUsd({ + pattern: mining.rewards.fee1ySum, + name: "1y", + color: colors.time._1y, + }), + ], + }, + { + name: "24h", + title: "Fee 24h Rolling Sum", + bottom: satsBtcUsd({ + pattern: mining.rewards.fee24hSum, + name: "24h", + color: colors.time._24h, + }), + }, + { + name: "7d", + title: "Fee 7d Rolling Sum", + bottom: satsBtcUsd({ + pattern: mining.rewards.fee7dSum, + name: "7d", + color: colors.time._1w, + }), + }, + { + name: "30d", + title: "Fee 30d Rolling Sum", + bottom: satsBtcUsd({ + pattern: mining.rewards.fee30dSum, + name: "30d", + color: colors.time._1m, + }), + }, + { + name: "1y", + title: "Fee 1y Rolling Sum", + bottom: satsBtcUsd({ + pattern: mining.rewards.fee1ySum, + name: "1y", + color: colors.time._1y, + }), + }, + ], + }, { name: "Distribution", title: "Transaction Fee Revenue per Block Distribution", @@ -501,20 +635,174 @@ export function createMiningSection() { }, { name: "Dominance", - title: "Revenue Dominance", - bottom: [ - line({ - metric: mining.rewards.subsidyDominance, - name: "Subsidy", - color: colors.mining.subsidy, - unit: Unit.percentage, - }), - line({ - metric: mining.rewards.feeDominance24h, - name: "Fees", - color: colors.mining.fee, - unit: Unit.percentage, - }), + tree: [ + { + name: "Compare", + tree: [ + { + name: "Subsidy", + title: "Subsidy Dominance", + bottom: [ + line({ + metric: mining.rewards.subsidyDominance, + name: "All-time", + color: colors.time.all, + unit: Unit.percentage, + }), + line({ + metric: mining.rewards.subsidyDominance24h, + name: "24h", + color: colors.time._24h, + unit: Unit.percentage, + }), + line({ + metric: mining.rewards.subsidyDominance7d, + name: "7d", + color: colors.time._1w, + unit: Unit.percentage, + }), + line({ + metric: mining.rewards.subsidyDominance30d, + name: "30d", + color: colors.time._1m, + unit: Unit.percentage, + }), + line({ + metric: mining.rewards.subsidyDominance1y, + name: "1y", + color: colors.time._1y, + unit: Unit.percentage, + }), + ], + }, + { + name: "Fees", + title: "Fee Dominance", + bottom: [ + line({ + metric: mining.rewards.feeDominance, + name: "All-time", + color: colors.time.all, + unit: Unit.percentage, + }), + line({ + metric: mining.rewards.feeDominance24h, + name: "24h", + color: colors.time._24h, + unit: Unit.percentage, + }), + line({ + metric: mining.rewards.feeDominance7d, + name: "7d", + color: colors.time._1w, + unit: Unit.percentage, + }), + line({ + metric: mining.rewards.feeDominance30d, + name: "30d", + color: colors.time._1m, + unit: Unit.percentage, + }), + line({ + metric: mining.rewards.feeDominance1y, + name: "1y", + color: colors.time._1y, + unit: Unit.percentage, + }), + ], + }, + ], + }, + { + name: "All-time", + title: "Revenue Dominance (All-time)", + bottom: [ + line({ + metric: mining.rewards.subsidyDominance, + name: "Subsidy", + color: colors.mining.subsidy, + unit: Unit.percentage, + }), + line({ + metric: mining.rewards.feeDominance, + name: "Fees", + color: colors.mining.fee, + unit: Unit.percentage, + }), + ], + }, + { + name: "24h", + title: "Revenue Dominance (24h)", + bottom: [ + line({ + metric: mining.rewards.subsidyDominance24h, + name: "Subsidy", + color: colors.mining.subsidy, + unit: Unit.percentage, + }), + line({ + metric: mining.rewards.feeDominance24h, + name: "Fees", + color: colors.mining.fee, + unit: Unit.percentage, + }), + ], + }, + { + name: "7d", + title: "Revenue Dominance (7d)", + bottom: [ + line({ + metric: mining.rewards.subsidyDominance7d, + name: "Subsidy", + color: colors.mining.subsidy, + unit: Unit.percentage, + }), + line({ + metric: mining.rewards.feeDominance7d, + name: "Fees", + color: colors.mining.fee, + unit: Unit.percentage, + }), + ], + }, + { + name: "30d", + title: "Revenue Dominance (30d)", + bottom: [ + line({ + metric: mining.rewards.subsidyDominance30d, + name: "Subsidy", + color: colors.mining.subsidy, + unit: Unit.percentage, + }), + line({ + metric: mining.rewards.feeDominance30d, + name: "Fees", + color: colors.mining.fee, + unit: Unit.percentage, + }), + ], + }, + { + name: "1y", + title: "Revenue Dominance (1y)", + bottom: [ + line({ + metric: mining.rewards.subsidyDominance1y, + name: "Subsidy", + color: colors.mining.subsidy, + unit: Unit.percentage, + }), + line({ + metric: mining.rewards.feeDominance1y, + name: "Fees", + color: colors.mining.fee, + unit: Unit.percentage, + }), + ], + }, ], }, { @@ -675,7 +963,7 @@ export function createMiningSection() { title: "Dominance: Major Pools (1m)", bottom: majorPools.map((p, i) => line({ - metric: p.pool._1mDominance, + metric: p.pool.dominance1m, name: p.name, color: colors.at(i, majorPools.length), unit: Unit.percentage, @@ -687,7 +975,7 @@ export function createMiningSection() { title: "Blocks Mined: Major Pools (1m)", bottom: majorPools.map((p, i) => line({ - metric: p.pool._1mBlocksMined, + metric: p.pool.blocksMined1mSum, name: p.name, color: colors.at(i, majorPools.length), unit: Unit.count, @@ -717,7 +1005,7 @@ export function createMiningSection() { title: "Dominance: AntPool & Friends (1m)", bottom: antpoolFriends.map((p, i) => line({ - metric: p.pool._1mDominance, + metric: p.pool.dominance1m, name: p.name, color: colors.at(i, antpoolFriends.length), unit: Unit.percentage, @@ -729,7 +1017,7 @@ export function createMiningSection() { title: "Blocks Mined: AntPool & Friends (1m)", bottom: antpoolFriends.map((p, i) => line({ - metric: p.pool._1mBlocksMined, + metric: p.pool.blocksMined1mSum, name: p.name, color: colors.at(i, antpoolFriends.length), unit: Unit.count, diff --git a/website/scripts/options/network.js b/website/scripts/options/network.js index fcc6fa163..3d7586ed3 100644 --- a/website/scripts/options/network.js +++ b/website/scripts/options/network.js @@ -622,25 +622,25 @@ export function createNetworkSection() { title: "Block Count (Rolling)", bottom: [ line({ - metric: blocks.count._24hBlockCount, + metric: blocks.count.blockCount24hSum, name: "24h", color: colors.time._24h, unit: Unit.count, }), line({ - metric: blocks.count._1wBlockCount, + metric: blocks.count.blockCount1wSum, name: "1w", color: colors.time._1w, unit: Unit.count, }), line({ - metric: blocks.count._1mBlockCount, + metric: blocks.count.blockCount1mSum, name: "1m", color: colors.time._1m, unit: Unit.count, }), line({ - metric: blocks.count._1yBlockCount, + metric: blocks.count.blockCount1ySum, name: "1y", color: colors.time._1y, unit: Unit.count, diff --git a/website/scripts/types.js b/website/scripts/types.js index b02a0a073..0e76c6a53 100644 --- a/website/scripts/types.js +++ b/website/scripts/types.js @@ -54,7 +54,7 @@ * @typedef {Brk._0sdM0M1M1sdM2M2sdM3sdP0P1P1sdP2P2sdP3sdSdSmaZscorePattern} Ratio1ySdPattern * @typedef {Brk.Dollars} Dollars * CoinbasePattern: patterns with btc/sats/usd each having base + sum + cumulative + stats - * @typedef {Brk.BtcSatsUsdPattern4} CoinbasePattern + * @typedef {Brk.BtcSatsUsdPattern3} CoinbasePattern * ActivePriceRatioPattern: ratio pattern with price (extended) * @typedef {Brk.PriceRatioPattern} ActivePriceRatioPattern * AnyRatioPattern: full ratio patterns (with or without price) - has ratio, percentiles, z-scores @@ -62,7 +62,7 @@ * ValuePattern: patterns with minimal stats (sum, cumulative only) for btc/sats/usd * @typedef {Brk.BtcSatsUsdPattern5 | Brk.BtcSatsUsdPattern2} ValuePattern * FullValuePattern: patterns with full stats (base, sum, cumulative, average, percentiles) for btc/sats/usd - * @typedef {Brk.BtcSatsUsdPattern4} FullValuePattern + * @typedef {Brk.BtcSatsUsdPattern3} FullValuePattern * SumValuePattern: patterns with sum stats (sum, cumulative, average, percentiles - no base) for bitcoin/sats/dollars * @typedef {{btc: SumStatsPattern, sats: SumStatsPattern, usd: SumStatsPattern}} SumValuePattern * AnyValuePatternType: union of all value pattern types @@ -117,6 +117,10 @@ * @template T * @typedef {Brk.AverageCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern2} SumStatsPattern */ +/** + * Full stats pattern for Bitcoin (non-generic variant with btc-specific indexes) + * @typedef {Brk.AverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern2} BtcFullStatsPattern + */ /** * Count pattern: sum and cumulative only * @template T @@ -124,7 +128,7 @@ */ /** * Any stats pattern union - patterns with sum/cumulative + percentiles - * @typedef {SumStatsPattern | FullStatsPattern | BlockSizePattern} AnyStatsPattern + * @typedef {SumStatsPattern | FullStatsPattern | BtcFullStatsPattern | BlockSizePattern} AnyStatsPattern */ /** @@ -140,7 +144,7 @@ * @typedef {Brk.MetricsTree_Market_Dca} MarketDca * @typedef {Brk._10y2y3y4y5y6y8yPattern} PeriodCagrPattern * Full stats pattern union (both generic and non-generic variants) - * @typedef {Brk.AverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern | FullStatsPattern} AnyFullStatsPattern + * @typedef {FullStatsPattern | BtcFullStatsPattern} AnyFullStatsPattern * * DCA period keys - derived from pattern types * @typedef {keyof Brk._10y2y3y4y5y6y8yPattern} LongPeriodKey