global: snapshot

This commit is contained in:
nym21
2026-02-24 12:21:20 +01:00
parent 3b7aa8242a
commit cefc8cfd42
24 changed files with 3561 additions and 1073 deletions
@@ -128,20 +128,14 @@ function dateToIndex(index, d) {{
* Wrap raw metric data with helper methods.
* @template T
* @param {{MetricData<T>}} raw - Raw JSON response
* @returns {{MetricData<T>}}
* @returns {{DateMetricData<T>}}
*/
function _wrapMetricData(raw) {{
const {{ index, start, end, data }} = raw;
const _dateBased = _DATE_INDEXES.has(index);
return /** @type {{MetricData<T>}} */ ({{
return /** @type {{DateMetricData<T>}} */ ({{
...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<globalThis.Date | number, T>}} */
/** @type {{Map<number, T>}} */
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<globalThis.Date, T>}} */
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<globalThis.Date | number, T>}} 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<number, T>}} toMap - Convert to Map<index, value>
*/
/** @template T @typedef {{MetricDataBase<T> & 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<globalThis.Date, T>}} toDateMap - Convert to Map<date, value>
*/
/** @template T @typedef {{MetricData<T> & DateMetricDataExtras<T>}} DateMetricData */
/** @typedef {{MetricData<any>}} AnyMetricData */
/**
* Thenable interface for await support.
* @template T
* @typedef {{(onfulfilled?: (value: MetricData<T>) => MetricData<T>, onrejected?: (reason: Error) => never) => Promise<MetricData<T>>}} Thenable
*/
/** @template T @typedef {{(onfulfilled?: (value: MetricData<T>) => any, onrejected?: (reason: Error) => never) => Promise<MetricData<T>>}} Thenable */
/** @template T @typedef {{(onfulfilled?: (value: DateMetricData<T>) => any, onrejected?: (reason: Error) => never) => Promise<DateMetricData<T>>}} DateThenable */
/**
* Metric endpoint builder. Callable (returns itself) so both .by.day1 and .by.day1() work.
* @template T
* @typedef {{Object}} MetricEndpointBuilder
* @property {{(index: number) => SingleItemBuilder<T>}} get - Get single item at index
* @property {{(start?: number | globalThis.Date, end?: number | globalThis.Date) => RangeBuilder<T>}} slice - Slice by index or Date
* @property {{(start?: number, end?: number) => RangeBuilder<T>}} slice - Slice by index
* @property {{(n: number) => RangeBuilder<T>}} first - Get first n items
* @property {{(n: number) => RangeBuilder<T>}} last - Get last n items
* @property {{(n: number) => SkippedBuilder<T>}} skip - Skip first n items, chain with take()
@@ -220,38 +228,66 @@ function _wrapMetricData(raw) {{
* @property {{Thenable<T>}} then - Thenable (await endpoint)
* @property {{string}} path - The endpoint path
*/
/** @typedef {{MetricEndpointBuilder<any>}} AnyMetricEndpointBuilder */
/**
* @template T
* @typedef {{Object}} SingleItemBuilder
* @typedef {{Object}} DateMetricEndpointBuilder
* @property {{(index: number | globalThis.Date) => DateSingleItemBuilder<T>}} get - Get single item at index or Date
* @property {{(start?: number | globalThis.Date, end?: number | globalThis.Date) => DateRangeBuilder<T>}} slice - Slice by index or Date
* @property {{(n: number) => DateRangeBuilder<T>}} first - Get first n items
* @property {{(n: number) => DateRangeBuilder<T>}} last - Get last n items
* @property {{(n: number) => DateSkippedBuilder<T>}} skip - Skip first n items, chain with take()
* @property {{(onUpdate?: (value: DateMetricData<T>) => void) => Promise<DateMetricData<T>>}} fetch - Fetch all data
* @property {{() => Promise<string>}} fetchCsv - Fetch all data as CSV
* @property {{DateThenable<T>}} then - Thenable (await endpoint)
* @property {{string}} path - The endpoint path
*/
/** @typedef {{MetricEndpointBuilder<any>}} AnyMetricEndpointBuilder */
/** @template T @typedef {{Object}} SingleItemBuilder
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch the item
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/**
* @template T
* @typedef {{Object}} SkippedBuilder
/** @template T @typedef {{Object}} DateSingleItemBuilder
* @property {{(onUpdate?: (value: DateMetricData<T>) => void) => Promise<DateMetricData<T>>}} fetch - Fetch the item
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{DateThenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} SkippedBuilder
* @property {{(n: number) => RangeBuilder<T>}} take - Take n items after skipped position
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch from skipped position to end
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/**
* @template T
* @typedef {{Object}} RangeBuilder
/** @template T @typedef {{Object}} DateSkippedBuilder
* @property {{(n: number) => DateRangeBuilder<T>}} take - Take n items after skipped position
* @property {{(onUpdate?: (value: DateMetricData<T>) => void) => Promise<DateMetricData<T>>}} fetch - Fetch from skipped position to end
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{DateThenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} RangeBuilder
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch the range
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} DateRangeBuilder
* @property {{(onUpdate?: (value: DateMetricData<T>) => void) => Promise<DateMetricData<T>>}} fetch - Fetch the range
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{DateThenable<T>}} then - Thenable
*/
/**
* @template T
* @typedef {{Object}} MetricPattern
* @property {{string}} name - The metric name
* @property {{Readonly<Partial<Record<Index, MetricEndpointBuilder<T>>>>}} by - Index endpoints as lazy getters. Access via .by.day1 or .by['day1']
* @property {{Readonly<Partial<Record<Index, MetricEndpointBuilder<T>>>>}} by - Index endpoints as lazy getters
* @property {{() => readonly Index[]}} indexes - Get the list of available indexes
* @property {{(index: Index) => MetricEndpointBuilder<T>|undefined}} get - Get an endpoint for a specific index
*/
@@ -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<T>}}
* @returns {{DateMetricEndpointBuilder<T>}}
*/
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<T>}}
* @returns {{DateRangeBuilder<T>}}
*/
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<T>}}
* @returns {{DateSingleItemBuilder<T>}}
*/
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<T>}}
* @returns {{DateSkippedBuilder<T>}}
*/
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<T>}} */
/** @type {{DateMetricEndpointBuilder<T>}} */
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<Cache | null>}} */
this._cachePromise = _openCache(isString ? undefined : options.cache);
@@ -359,8 +396,7 @@ class BrkClientBase {{
* @returns {{Promise<Response>}}
*/
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<T>}}
*/
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<T>) => void}} [onUpdate]
* @returns {{Promise<MetricData<T>>}}
* @param {{(value: DateMetricData<T>) => void}} [onUpdate]
* @returns {{Promise<DateMetricData<T>>}}
*/
async _fetchMetricData(path, onUpdate) {{
const wrappedOnUpdate = onUpdate ? (/** @type {{MetricData<T>}} */ 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<T>|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<T>",
idx.name()
)
let builder = if idx.is_date_based() {
"DateMetricEndpointBuilder"
} else {
"MetricEndpointBuilder"
};
format!("readonly {}: {}<T>", 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 {{{}<T>}} */ (_mp(client, name, _i{})); }}",
pattern.name,
pattern.name,
i + 1
)
+180 -102
View File
@@ -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();
}
@@ -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!(
@@ -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<Metric>, index: Index) -> Result<DateMetricEndpointBuilder<serde_json::Value>> {{
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
)
@@ -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<T>`).
/// For date-based indexes, use `DateMetricEndpointBuilder<T>` which sets `D = DateMetricData<T>`.
///
/// # 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<T> {{
pub struct MetricEndpointBuilder<T, D = MetricData<T>> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<T>,
_marker: std::marker::PhantomData<fn() -> (T, D)>,
}}
impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
/// 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<T> = MetricEndpointBuilder<T, DateMetricData<T>>;
impl<T: DeserializeOwned, D: DeserializeOwned> MetricEndpointBuilder<T, D> {{
pub fn new(client: Arc<BrkClientBase>, name: Arc<str>, index: Index) -> Self {{
Self {{ config: EndpointConfig::new(client, name, index), _marker: std::marker::PhantomData }}
}}
/// Select a specific index position.
pub fn get(mut self, index: usize) -> SingleItemBuilder<T> {{
pub fn get(mut self, index: usize) -> SingleItemBuilder<T, D> {{
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<T: DeserializeOwned> MetricEndpointBuilder<T> {{
/// endpoint.range(100..110) // indices 100-109
/// endpoint.range(100..) // from 100 to end
/// ```
pub fn range<R: RangeBounds<usize>>(mut self, range: R) -> RangeBuilder<T> {{
pub fn range<R: RangeBounds<usize>>(mut self, range: R) -> RangeBuilder<T, D> {{
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<T: DeserializeOwned> MetricEndpointBuilder<T> {{
}}
/// Take the first n items.
pub fn take(self, n: usize) -> RangeBuilder<T> {{
pub fn take(self, n: usize) -> RangeBuilder<T, D> {{
self.range(..n)
}}
/// Take the last n items.
pub fn last(mut self, n: usize) -> RangeBuilder<T> {{
pub fn last(mut self, n: usize) -> RangeBuilder<T, D> {{
if n == 0 {{
self.config.end = Some(0);
}} else {{
@@ -282,13 +276,13 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
}}
/// Skip the first n items. Chain with `take(n)` to get a range.
pub fn skip(mut self, n: usize) -> SkippedBuilder<T> {{
pub fn skip(mut self, n: usize) -> SkippedBuilder<T, D> {{
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<MetricData<T>> {{
pub fn fetch(self) -> Result<D> {{
self.config.get_json(None)
}}
@@ -303,15 +297,47 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
}}
}}
/// Builder for single item access.
pub struct SingleItemBuilder<T> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<T>,
/// Date-specific methods available only on `DateMetricEndpointBuilder`.
impl<T: DeserializeOwned> MetricEndpointBuilder<T, DateMetricData<T>> {{
/// Select a specific date position (for day-precision or coarser indexes).
pub fn get_date(self, date: Date) -> SingleItemBuilder<T, DateMetricData<T>> {{
let index = self.config.index.date_to_index(date).unwrap_or(0);
self.get(index)
}}
/// Select a date range (for day-precision or coarser indexes).
pub fn date_range(self, start: Date, end: Date) -> RangeBuilder<T, DateMetricData<T>> {{
let s = self.config.index.date_to_index(start).unwrap_or(0);
let e = self.config.index.date_to_index(end).unwrap_or(0);
self.range(s..e)
}}
/// Select a specific timestamp position (works for all date-based indexes including sub-daily).
pub fn get_timestamp(self, ts: Timestamp) -> SingleItemBuilder<T, DateMetricData<T>> {{
let index = self.config.index.timestamp_to_index(ts).unwrap_or(0);
self.get(index)
}}
/// Select a timestamp range (works for all date-based indexes including sub-daily).
pub fn timestamp_range(self, start: Timestamp, end: Timestamp) -> RangeBuilder<T, DateMetricData<T>> {{
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<T: DeserializeOwned> SingleItemBuilder<T> {{
/// Builder for single item access.
pub struct SingleItemBuilder<T, D = MetricData<T>> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<fn() -> (T, D)>,
}}
/// Date-aware single item builder.
pub type DateSingleItemBuilder<T> = SingleItemBuilder<T, DateMetricData<T>>;
impl<T: DeserializeOwned, D: DeserializeOwned> SingleItemBuilder<T, D> {{
/// Fetch the single item.
pub fn fetch(self) -> Result<MetricData<T>> {{
pub fn fetch(self) -> Result<D> {{
self.config.get_json(None)
}}
@@ -322,21 +348,24 @@ impl<T: DeserializeOwned> SingleItemBuilder<T> {{
}}
/// Builder after calling `skip(n)`. Chain with `take(n)` to specify count.
pub struct SkippedBuilder<T> {{
pub struct SkippedBuilder<T, D = MetricData<T>> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<T>,
_marker: std::marker::PhantomData<fn() -> (T, D)>,
}}
impl<T: DeserializeOwned> SkippedBuilder<T> {{
/// Date-aware skipped builder.
pub type DateSkippedBuilder<T> = SkippedBuilder<T, DateMetricData<T>>;
impl<T: DeserializeOwned, D: DeserializeOwned> SkippedBuilder<T, D> {{
/// Take n items after the skipped position.
pub fn take(mut self, n: usize) -> RangeBuilder<T> {{
pub fn take(mut self, n: usize) -> RangeBuilder<T, D> {{
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<MetricData<T>> {{
pub fn fetch(self) -> Result<D> {{
self.config.get_json(None)
}}
@@ -347,14 +376,17 @@ impl<T: DeserializeOwned> SkippedBuilder<T> {{
}}
/// Builder with range fully specified.
pub struct RangeBuilder<T> {{
pub struct RangeBuilder<T, D = MetricData<T>> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<T>,
_marker: std::marker::PhantomData<fn() -> (T, D)>,
}}
impl<T: DeserializeOwned> RangeBuilder<T> {{
/// Date-aware range builder.
pub type DateRangeBuilder<T> = RangeBuilder<T, DateMetricData<T>>;
impl<T: DeserializeOwned, D: DeserializeOwned> RangeBuilder<T, D> {{
/// Fetch the range as parsed JSON.
pub fn fetch(self) -> Result<MetricData<T>> {{
pub fn fetch(self) -> Result<D> {{
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<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) -> MetricEndpointBuilder<T> {{
MetricEndpointBuilder::new(c.clone(), n.clone(), i)
}}
#[inline]
fn _dep<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) -> DateMetricEndpointBuilder<T> {{
DateMetricEndpointBuilder::new(c.clone(), n.clone(), i)
}}
"#
)
.unwrap();
@@ -412,12 +449,21 @@ fn _ep<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) -> M
writeln!(output, "impl<T: DeserializeOwned> {}<T> {{", by_name).unwrap();
for index in &pattern.indexes {
let method_name = index_to_field_name(index);
writeln!(
output,
" pub fn {}(&self) -> MetricEndpointBuilder<T> {{ _ep(&self.client, &self.name, Index::{}) }}",
method_name, index
)
.unwrap();
if index.is_date_based() {
writeln!(
output,
" pub fn {}(&self) -> DateMetricEndpointBuilder<T> {{ _dep(&self.client, &self.name, Index::{}) }}",
method_name, index
)
.unwrap();
} else {
writeln!(
output,
" pub fn {}(&self) -> MetricEndpointBuilder<T> {{ _ep(&self.client, &self.name, Index::{}) }}",
method_name, index
)
.unwrap();
}
}
writeln!(output, "}}\n").unwrap();
+28 -7
View File
@@ -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,
+142 -91
View File
@@ -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<T>`).
/// For date-based indexes, use `DateMetricEndpointBuilder<T>` which sets `D = DateMetricData<T>`.
///
/// # 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<T> {
pub struct MetricEndpointBuilder<T, D = MetricData<T>> {
config: EndpointConfig,
_marker: std::marker::PhantomData<T>,
_marker: std::marker::PhantomData<fn() -> (T, D)>,
}
impl<T: DeserializeOwned> MetricEndpointBuilder<T> {
/// 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<T> = MetricEndpointBuilder<T, DateMetricData<T>>;
impl<T: DeserializeOwned, D: DeserializeOwned> MetricEndpointBuilder<T, D> {
pub fn new(client: Arc<BrkClientBase>, name: Arc<str>, index: Index) -> Self {
Self { config: EndpointConfig::new(client, name, index), _marker: std::marker::PhantomData }
}
/// Select a specific index position.
pub fn get(mut self, index: usize) -> SingleItemBuilder<T> {
pub fn get(mut self, index: usize) -> SingleItemBuilder<T, D> {
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<T: DeserializeOwned> MetricEndpointBuilder<T> {
/// endpoint.range(100..110) // indices 100-109
/// endpoint.range(100..) // from 100 to end
/// ```
pub fn range<R: RangeBounds<usize>>(mut self, range: R) -> RangeBuilder<T> {
pub fn range<R: RangeBounds<usize>>(mut self, range: R) -> RangeBuilder<T, D> {
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<T: DeserializeOwned> MetricEndpointBuilder<T> {
}
/// Take the first n items.
pub fn take(self, n: usize) -> RangeBuilder<T> {
pub fn take(self, n: usize) -> RangeBuilder<T, D> {
self.range(..n)
}
/// Take the last n items.
pub fn last(mut self, n: usize) -> RangeBuilder<T> {
pub fn last(mut self, n: usize) -> RangeBuilder<T, D> {
if n == 0 {
self.config.end = Some(0);
} else {
@@ -254,13 +248,13 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T> {
}
/// Skip the first n items. Chain with `take(n)` to get a range.
pub fn skip(mut self, n: usize) -> SkippedBuilder<T> {
pub fn skip(mut self, n: usize) -> SkippedBuilder<T, D> {
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<MetricData<T>> {
pub fn fetch(self) -> Result<D> {
self.config.get_json(None)
}
@@ -275,15 +269,47 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T> {
}
}
/// Builder for single item access.
pub struct SingleItemBuilder<T> {
config: EndpointConfig,
_marker: std::marker::PhantomData<T>,
/// Date-specific methods available only on `DateMetricEndpointBuilder`.
impl<T: DeserializeOwned> MetricEndpointBuilder<T, DateMetricData<T>> {
/// Select a specific date position (for day-precision or coarser indexes).
pub fn get_date(self, date: Date) -> SingleItemBuilder<T, DateMetricData<T>> {
let index = self.config.index.date_to_index(date).unwrap_or(0);
self.get(index)
}
/// Select a date range (for day-precision or coarser indexes).
pub fn date_range(self, start: Date, end: Date) -> RangeBuilder<T, DateMetricData<T>> {
let s = self.config.index.date_to_index(start).unwrap_or(0);
let e = self.config.index.date_to_index(end).unwrap_or(0);
self.range(s..e)
}
/// Select a specific timestamp position (works for all date-based indexes including sub-daily).
pub fn get_timestamp(self, ts: Timestamp) -> SingleItemBuilder<T, DateMetricData<T>> {
let index = self.config.index.timestamp_to_index(ts).unwrap_or(0);
self.get(index)
}
/// Select a timestamp range (works for all date-based indexes including sub-daily).
pub fn timestamp_range(self, start: Timestamp, end: Timestamp) -> RangeBuilder<T, DateMetricData<T>> {
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<T: DeserializeOwned> SingleItemBuilder<T> {
/// Builder for single item access.
pub struct SingleItemBuilder<T, D = MetricData<T>> {
config: EndpointConfig,
_marker: std::marker::PhantomData<fn() -> (T, D)>,
}
/// Date-aware single item builder.
pub type DateSingleItemBuilder<T> = SingleItemBuilder<T, DateMetricData<T>>;
impl<T: DeserializeOwned, D: DeserializeOwned> SingleItemBuilder<T, D> {
/// Fetch the single item.
pub fn fetch(self) -> Result<MetricData<T>> {
pub fn fetch(self) -> Result<D> {
self.config.get_json(None)
}
@@ -294,21 +320,24 @@ impl<T: DeserializeOwned> SingleItemBuilder<T> {
}
/// Builder after calling `skip(n)`. Chain with `take(n)` to specify count.
pub struct SkippedBuilder<T> {
pub struct SkippedBuilder<T, D = MetricData<T>> {
config: EndpointConfig,
_marker: std::marker::PhantomData<T>,
_marker: std::marker::PhantomData<fn() -> (T, D)>,
}
impl<T: DeserializeOwned> SkippedBuilder<T> {
/// Date-aware skipped builder.
pub type DateSkippedBuilder<T> = SkippedBuilder<T, DateMetricData<T>>;
impl<T: DeserializeOwned, D: DeserializeOwned> SkippedBuilder<T, D> {
/// Take n items after the skipped position.
pub fn take(mut self, n: usize) -> RangeBuilder<T> {
pub fn take(mut self, n: usize) -> RangeBuilder<T, D> {
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<MetricData<T>> {
pub fn fetch(self) -> Result<D> {
self.config.get_json(None)
}
@@ -319,14 +348,17 @@ impl<T: DeserializeOwned> SkippedBuilder<T> {
}
/// Builder with range fully specified.
pub struct RangeBuilder<T> {
pub struct RangeBuilder<T, D = MetricData<T>> {
config: EndpointConfig,
_marker: std::marker::PhantomData<T>,
_marker: std::marker::PhantomData<fn() -> (T, D)>,
}
impl<T: DeserializeOwned> RangeBuilder<T> {
/// Date-aware range builder.
pub type DateRangeBuilder<T> = RangeBuilder<T, DateMetricData<T>>;
impl<T: DeserializeOwned, D: DeserializeOwned> RangeBuilder<T, D> {
/// Fetch the range as parsed JSON.
pub fn fetch(self) -> Result<MetricData<T>> {
pub fn fetch(self) -> Result<D> {
self.config.get_json(None)
}
@@ -381,25 +413,30 @@ fn _ep<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) -> M
MetricEndpointBuilder::new(c.clone(), n.clone(), i)
}
#[inline]
fn _dep<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) -> DateMetricEndpointBuilder<T> {
DateMetricEndpointBuilder::new(c.clone(), n.clone(), i)
}
// Index accessor structs
pub struct MetricPattern1By<T> { client: Arc<BrkClientBase>, name: Arc<str>, _marker: std::marker::PhantomData<T> }
impl<T: DeserializeOwned> MetricPattern1By<T> {
pub fn minute1(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Minute1) }
pub fn minute5(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Minute5) }
pub fn minute10(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Minute10) }
pub fn minute30(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Minute30) }
pub fn hour1(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Hour1) }
pub fn hour4(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Hour4) }
pub fn hour12(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Hour12) }
pub fn day1(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Day1) }
pub fn day3(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Day3) }
pub fn week1(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Week1) }
pub fn month1(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Month1) }
pub fn month3(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Month3) }
pub fn month6(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Month6) }
pub fn year1(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Year1) }
pub fn year10(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Year10) }
pub fn minute1(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Minute1) }
pub fn minute5(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Minute5) }
pub fn minute10(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Minute10) }
pub fn minute30(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Minute30) }
pub fn hour1(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Hour1) }
pub fn hour4(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Hour4) }
pub fn hour12(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Hour12) }
pub fn day1(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Day1) }
pub fn day3(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Day3) }
pub fn week1(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Week1) }
pub fn month1(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Month1) }
pub fn month3(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Month3) }
pub fn month6(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Month6) }
pub fn year1(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Year1) }
pub fn year10(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Year10) }
pub fn halvingepoch(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::HalvingEpoch) }
pub fn difficultyepoch(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::DifficultyEpoch) }
pub fn height(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Height) }
@@ -416,21 +453,21 @@ impl<T: DeserializeOwned> MetricPattern<T> for MetricPattern1<T> { fn get(&self,
pub struct MetricPattern2By<T> { client: Arc<BrkClientBase>, name: Arc<str>, _marker: std::marker::PhantomData<T> }
impl<T: DeserializeOwned> MetricPattern2By<T> {
pub fn minute1(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Minute1) }
pub fn minute5(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Minute5) }
pub fn minute10(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Minute10) }
pub fn minute30(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Minute30) }
pub fn hour1(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Hour1) }
pub fn hour4(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Hour4) }
pub fn hour12(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Hour12) }
pub fn day1(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Day1) }
pub fn day3(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Day3) }
pub fn week1(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Week1) }
pub fn month1(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Month1) }
pub fn month3(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Month3) }
pub fn month6(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Month6) }
pub fn year1(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Year1) }
pub fn year10(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Year10) }
pub fn minute1(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Minute1) }
pub fn minute5(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Minute5) }
pub fn minute10(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Minute10) }
pub fn minute30(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Minute30) }
pub fn hour1(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Hour1) }
pub fn hour4(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Hour4) }
pub fn hour12(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Hour12) }
pub fn day1(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Day1) }
pub fn day3(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Day3) }
pub fn week1(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Week1) }
pub fn month1(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Month1) }
pub fn month3(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Month3) }
pub fn month6(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Month6) }
pub fn year1(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Year1) }
pub fn year10(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Year10) }
pub fn halvingepoch(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::HalvingEpoch) }
pub fn difficultyepoch(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::DifficultyEpoch) }
}
@@ -446,7 +483,7 @@ impl<T: DeserializeOwned> MetricPattern<T> for MetricPattern2<T> { fn get(&self,
pub struct MetricPattern3By<T> { client: Arc<BrkClientBase>, name: Arc<str>, _marker: std::marker::PhantomData<T> }
impl<T: DeserializeOwned> MetricPattern3By<T> {
pub fn minute1(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Minute1) }
pub fn minute1(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Minute1) }
}
pub struct MetricPattern3<T> { name: Arc<str>, pub by: MetricPattern3By<T> }
@@ -460,7 +497,7 @@ impl<T: DeserializeOwned> MetricPattern<T> for MetricPattern3<T> { fn get(&self,
pub struct MetricPattern4By<T> { client: Arc<BrkClientBase>, name: Arc<str>, _marker: std::marker::PhantomData<T> }
impl<T: DeserializeOwned> MetricPattern4By<T> {
pub fn minute5(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Minute5) }
pub fn minute5(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Minute5) }
}
pub struct MetricPattern4<T> { name: Arc<str>, pub by: MetricPattern4By<T> }
@@ -474,7 +511,7 @@ impl<T: DeserializeOwned> MetricPattern<T> for MetricPattern4<T> { fn get(&self,
pub struct MetricPattern5By<T> { client: Arc<BrkClientBase>, name: Arc<str>, _marker: std::marker::PhantomData<T> }
impl<T: DeserializeOwned> MetricPattern5By<T> {
pub fn minute10(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Minute10) }
pub fn minute10(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Minute10) }
}
pub struct MetricPattern5<T> { name: Arc<str>, pub by: MetricPattern5By<T> }
@@ -488,7 +525,7 @@ impl<T: DeserializeOwned> MetricPattern<T> for MetricPattern5<T> { fn get(&self,
pub struct MetricPattern6By<T> { client: Arc<BrkClientBase>, name: Arc<str>, _marker: std::marker::PhantomData<T> }
impl<T: DeserializeOwned> MetricPattern6By<T> {
pub fn minute30(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Minute30) }
pub fn minute30(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Minute30) }
}
pub struct MetricPattern6<T> { name: Arc<str>, pub by: MetricPattern6By<T> }
@@ -502,7 +539,7 @@ impl<T: DeserializeOwned> MetricPattern<T> for MetricPattern6<T> { fn get(&self,
pub struct MetricPattern7By<T> { client: Arc<BrkClientBase>, name: Arc<str>, _marker: std::marker::PhantomData<T> }
impl<T: DeserializeOwned> MetricPattern7By<T> {
pub fn hour1(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Hour1) }
pub fn hour1(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Hour1) }
}
pub struct MetricPattern7<T> { name: Arc<str>, pub by: MetricPattern7By<T> }
@@ -516,7 +553,7 @@ impl<T: DeserializeOwned> MetricPattern<T> for MetricPattern7<T> { fn get(&self,
pub struct MetricPattern8By<T> { client: Arc<BrkClientBase>, name: Arc<str>, _marker: std::marker::PhantomData<T> }
impl<T: DeserializeOwned> MetricPattern8By<T> {
pub fn hour4(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Hour4) }
pub fn hour4(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Hour4) }
}
pub struct MetricPattern8<T> { name: Arc<str>, pub by: MetricPattern8By<T> }
@@ -530,7 +567,7 @@ impl<T: DeserializeOwned> MetricPattern<T> for MetricPattern8<T> { fn get(&self,
pub struct MetricPattern9By<T> { client: Arc<BrkClientBase>, name: Arc<str>, _marker: std::marker::PhantomData<T> }
impl<T: DeserializeOwned> MetricPattern9By<T> {
pub fn hour12(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Hour12) }
pub fn hour12(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Hour12) }
}
pub struct MetricPattern9<T> { name: Arc<str>, pub by: MetricPattern9By<T> }
@@ -544,7 +581,7 @@ impl<T: DeserializeOwned> MetricPattern<T> for MetricPattern9<T> { fn get(&self,
pub struct MetricPattern10By<T> { client: Arc<BrkClientBase>, name: Arc<str>, _marker: std::marker::PhantomData<T> }
impl<T: DeserializeOwned> MetricPattern10By<T> {
pub fn day1(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Day1) }
pub fn day1(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Day1) }
}
pub struct MetricPattern10<T> { name: Arc<str>, pub by: MetricPattern10By<T> }
@@ -558,7 +595,7 @@ impl<T: DeserializeOwned> MetricPattern<T> for MetricPattern10<T> { fn get(&self
pub struct MetricPattern11By<T> { client: Arc<BrkClientBase>, name: Arc<str>, _marker: std::marker::PhantomData<T> }
impl<T: DeserializeOwned> MetricPattern11By<T> {
pub fn day3(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Day3) }
pub fn day3(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Day3) }
}
pub struct MetricPattern11<T> { name: Arc<str>, pub by: MetricPattern11By<T> }
@@ -572,7 +609,7 @@ impl<T: DeserializeOwned> MetricPattern<T> for MetricPattern11<T> { fn get(&self
pub struct MetricPattern12By<T> { client: Arc<BrkClientBase>, name: Arc<str>, _marker: std::marker::PhantomData<T> }
impl<T: DeserializeOwned> MetricPattern12By<T> {
pub fn week1(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Week1) }
pub fn week1(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Week1) }
}
pub struct MetricPattern12<T> { name: Arc<str>, pub by: MetricPattern12By<T> }
@@ -586,7 +623,7 @@ impl<T: DeserializeOwned> MetricPattern<T> for MetricPattern12<T> { fn get(&self
pub struct MetricPattern13By<T> { client: Arc<BrkClientBase>, name: Arc<str>, _marker: std::marker::PhantomData<T> }
impl<T: DeserializeOwned> MetricPattern13By<T> {
pub fn month1(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Month1) }
pub fn month1(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Month1) }
}
pub struct MetricPattern13<T> { name: Arc<str>, pub by: MetricPattern13By<T> }
@@ -600,7 +637,7 @@ impl<T: DeserializeOwned> MetricPattern<T> for MetricPattern13<T> { fn get(&self
pub struct MetricPattern14By<T> { client: Arc<BrkClientBase>, name: Arc<str>, _marker: std::marker::PhantomData<T> }
impl<T: DeserializeOwned> MetricPattern14By<T> {
pub fn month3(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Month3) }
pub fn month3(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Month3) }
}
pub struct MetricPattern14<T> { name: Arc<str>, pub by: MetricPattern14By<T> }
@@ -614,7 +651,7 @@ impl<T: DeserializeOwned> MetricPattern<T> for MetricPattern14<T> { fn get(&self
pub struct MetricPattern15By<T> { client: Arc<BrkClientBase>, name: Arc<str>, _marker: std::marker::PhantomData<T> }
impl<T: DeserializeOwned> MetricPattern15By<T> {
pub fn month6(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Month6) }
pub fn month6(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Month6) }
}
pub struct MetricPattern15<T> { name: Arc<str>, pub by: MetricPattern15By<T> }
@@ -628,7 +665,7 @@ impl<T: DeserializeOwned> MetricPattern<T> for MetricPattern15<T> { fn get(&self
pub struct MetricPattern16By<T> { client: Arc<BrkClientBase>, name: Arc<str>, _marker: std::marker::PhantomData<T> }
impl<T: DeserializeOwned> MetricPattern16By<T> {
pub fn year1(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Year1) }
pub fn year1(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Year1) }
}
pub struct MetricPattern16<T> { name: Arc<str>, pub by: MetricPattern16By<T> }
@@ -642,7 +679,7 @@ impl<T: DeserializeOwned> MetricPattern<T> for MetricPattern16<T> { fn get(&self
pub struct MetricPattern17By<T> { client: Arc<BrkClientBase>, name: Arc<str>, _marker: std::marker::PhantomData<T> }
impl<T: DeserializeOwned> MetricPattern17By<T> {
pub fn year10(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::Year10) }
pub fn year10(&self) -> DateMetricEndpointBuilder<T> { _dep(&self.client, &self.name, Index::Year10) }
}
pub struct MetricPattern17<T> { name: Arc<str>, pub by: MetricPattern17By<T> }
@@ -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<Metric>, index: Index) -> Result<DateMetricEndpointBuilder<serde_json::Value>> {
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`.
+106 -2
View File
@@ -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<usize> {
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<usize> {
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<Date> {
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());
}
}
+365 -21
View File
@@ -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<T> MetricData<T> {
}
/// Returns an iterator over dates for the index range.
/// Panics if the index is not date-based.
pub fn dates(&self) -> impl Iterator<Item = super::Date> + '_ {
/// Returns `None` for non-date-based and sub-daily indexes (use `timestamps()` instead).
pub fn dates(&self) -> Option<impl Iterator<Item = Date> + '_> {
// 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<impl Iterator<Item = Timestamp> + '_> {
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<T> MetricData<T> {
}
/// Iterate over (date, &value) pairs.
/// Panics if the index is not date-based.
pub fn iter_dates(&self) -> impl Iterator<Item = (super::Date, &T)> + '_ {
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<impl Iterator<Item = (Date, &T)> + '_> {
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<impl Iterator<Item = (Timestamp, &T)> + '_> {
Some(self.timestamps()?.zip(self.data.iter()))
}
}
/// Metric data that is guaranteed to use a date-based index.
///
/// This is a newtype around `MetricData<T>` that guarantees `is_date_based()` is true,
/// making date methods infallible.
#[derive(Debug)]
pub struct DateMetricData<T>(MetricData<T>);
impl<T> DateMetricData<T> {
/// Create a `DateMetricData` from a `MetricData`, returning `Err` if the index is not date-based.
pub fn try_new(inner: MetricData<T>) -> Result<Self, MetricData<T>> {
if inner.is_date_based() {
Ok(Self(inner))
} else {
Err(inner)
}
}
/// Consume and return the inner `MetricData`.
pub fn into_inner(self) -> MetricData<T> {
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<impl Iterator<Item = Date> + '_> {
self.0.dates()
}
/// Iterate over (date, &value) pairs.
/// Returns `None` for sub-daily indexes (use `iter_timestamps()` instead).
pub fn iter_dates(&self) -> Option<impl Iterator<Item = (Date, &T)> + '_> {
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<Item = Timestamp> + '_ {
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<Item = (Timestamp, &T)> + '_ {
self.0.iter_timestamps().expect("DateMetricData is always date-based")
}
}
impl<T> Deref for DateMetricData<T> {
type Target = MetricData<T>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'de, T: DeserializeOwned> Deserialize<'de> for DateMetricData<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let inner = MetricData::<T>::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<f64> {
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<i32> {
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<i32> {
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<DateMetricData<i32>, _> = 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<DateMetricData<i32>, _> = 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));
}
}
+152 -116
View File
@@ -998,20 +998,14 @@ function dateToIndex(index, d) {
* Wrap raw metric data with helper methods.
* @template T
* @param {MetricData<T>} raw - Raw JSON response
* @returns {MetricData<T>}
* @returns {DateMetricData<T>}
*/
function _wrapMetricData(raw) {
const { index, start, end, data } = raw;
const _dateBased = _DATE_INDEXES.has(index);
return /** @type {MetricData<T>} */ ({
return /** @type {DateMetricData<T>} */ ({
...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<globalThis.Date | number, T>} */
/** @type {Map<number, T>} */
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<globalThis.Date, T>} */
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<globalThis.Date | number, T>} 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<number, T>} toMap - Convert to Map<index, value>
*/
/** @template T @typedef {MetricDataBase<T> & 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<globalThis.Date, T>} toDateMap - Convert to Map<date, value>
*/
/** @template T @typedef {MetricData<T> & DateMetricDataExtras<T>} DateMetricData */
/** @typedef {MetricData<any>} AnyMetricData */
/**
* Thenable interface for await support.
* @template T
* @typedef {(onfulfilled?: (value: MetricData<T>) => MetricData<T>, onrejected?: (reason: Error) => never) => Promise<MetricData<T>>} Thenable
*/
/** @template T @typedef {(onfulfilled?: (value: MetricData<T>) => any, onrejected?: (reason: Error) => never) => Promise<MetricData<T>>} Thenable */
/** @template T @typedef {(onfulfilled?: (value: DateMetricData<T>) => any, onrejected?: (reason: Error) => never) => Promise<DateMetricData<T>>} DateThenable */
/**
* Metric endpoint builder. Callable (returns itself) so both .by.day1 and .by.day1() work.
* @template T
* @typedef {Object} MetricEndpointBuilder
* @property {(index: number) => SingleItemBuilder<T>} get - Get single item at index
* @property {(start?: number | globalThis.Date, end?: number | globalThis.Date) => RangeBuilder<T>} slice - Slice by index or Date
* @property {(start?: number, end?: number) => RangeBuilder<T>} slice - Slice by index
* @property {(n: number) => RangeBuilder<T>} first - Get first n items
* @property {(n: number) => RangeBuilder<T>} last - Get last n items
* @property {(n: number) => SkippedBuilder<T>} skip - Skip first n items, chain with take()
@@ -1090,38 +1098,66 @@ function _wrapMetricData(raw) {
* @property {Thenable<T>} then - Thenable (await endpoint)
* @property {string} path - The endpoint path
*/
/** @typedef {MetricEndpointBuilder<any>} AnyMetricEndpointBuilder */
/**
* @template T
* @typedef {Object} SingleItemBuilder
* @typedef {Object} DateMetricEndpointBuilder
* @property {(index: number | globalThis.Date) => DateSingleItemBuilder<T>} get - Get single item at index or Date
* @property {(start?: number | globalThis.Date, end?: number | globalThis.Date) => DateRangeBuilder<T>} slice - Slice by index or Date
* @property {(n: number) => DateRangeBuilder<T>} first - Get first n items
* @property {(n: number) => DateRangeBuilder<T>} last - Get last n items
* @property {(n: number) => DateSkippedBuilder<T>} skip - Skip first n items, chain with take()
* @property {(onUpdate?: (value: DateMetricData<T>) => void) => Promise<DateMetricData<T>>} fetch - Fetch all data
* @property {() => Promise<string>} fetchCsv - Fetch all data as CSV
* @property {DateThenable<T>} then - Thenable (await endpoint)
* @property {string} path - The endpoint path
*/
/** @typedef {MetricEndpointBuilder<any>} AnyMetricEndpointBuilder */
/** @template T @typedef {Object} SingleItemBuilder
* @property {(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>} fetch - Fetch the item
* @property {() => Promise<string>} fetchCsv - Fetch as CSV
* @property {Thenable<T>} then - Thenable
*/
/**
* @template T
* @typedef {Object} SkippedBuilder
/** @template T @typedef {Object} DateSingleItemBuilder
* @property {(onUpdate?: (value: DateMetricData<T>) => void) => Promise<DateMetricData<T>>} fetch - Fetch the item
* @property {() => Promise<string>} fetchCsv - Fetch as CSV
* @property {DateThenable<T>} then - Thenable
*/
/** @template T @typedef {Object} SkippedBuilder
* @property {(n: number) => RangeBuilder<T>} take - Take n items after skipped position
* @property {(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>} fetch - Fetch from skipped position to end
* @property {() => Promise<string>} fetchCsv - Fetch as CSV
* @property {Thenable<T>} then - Thenable
*/
/**
* @template T
* @typedef {Object} RangeBuilder
/** @template T @typedef {Object} DateSkippedBuilder
* @property {(n: number) => DateRangeBuilder<T>} take - Take n items after skipped position
* @property {(onUpdate?: (value: DateMetricData<T>) => void) => Promise<DateMetricData<T>>} fetch - Fetch from skipped position to end
* @property {() => Promise<string>} fetchCsv - Fetch as CSV
* @property {DateThenable<T>} then - Thenable
*/
/** @template T @typedef {Object} RangeBuilder
* @property {(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>} fetch - Fetch the range
* @property {() => Promise<string>} fetchCsv - Fetch as CSV
* @property {Thenable<T>} then - Thenable
*/
/** @template T @typedef {Object} DateRangeBuilder
* @property {(onUpdate?: (value: DateMetricData<T>) => void) => Promise<DateMetricData<T>>} fetch - Fetch the range
* @property {() => Promise<string>} fetchCsv - Fetch as CSV
* @property {DateThenable<T>} then - Thenable
*/
/**
* @template T
* @typedef {Object} MetricPattern
* @property {string} name - The metric name
* @property {Readonly<Partial<Record<Index, MetricEndpointBuilder<T>>>>} by - Index endpoints as lazy getters. Access via .by.day1 or .by['day1']
* @property {Readonly<Partial<Record<Index, MetricEndpointBuilder<T>>>>} by - Index endpoints as lazy getters
* @property {() => readonly Index[]} indexes - Get the list of available indexes
* @property {(index: Index) => MetricEndpointBuilder<T>|undefined} get - Get an endpoint for a specific index
*/
@@ -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<T>}
* @returns {DateMetricEndpointBuilder<T>}
*/
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<T>}
* @returns {DateRangeBuilder<T>}
*/
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<T>}
* @returns {DateSingleItemBuilder<T>}
*/
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<T>}
* @returns {DateSkippedBuilder<T>}
*/
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<T>} */
/** @type {DateMetricEndpointBuilder<T>} */
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<Cache | null>} */
this._cachePromise = _openCache(isString ? undefined : options.cache);
@@ -1229,8 +1266,7 @@ class BrkClientBase {
* @returns {Promise<Response>}
*/
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<T>}
*/
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<T>) => void} [onUpdate]
* @returns {Promise<MetricData<T>>}
* @param {(value: DateMetricData<T>) => void} [onUpdate]
* @returns {Promise<DateMetricData<T>>}
*/
async _fetchMetricData(path, onUpdate) {
const wrappedOnUpdate = onUpdate ? (/** @type {MetricData<T>} */ 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<T>|undefined} */
get(index) { return indexes.includes(index) ? _endpoint(client, name, index) : undefined; }
};
}
/** @template T @typedef {{ name: string, by: { readonly minute1: MetricEndpointBuilder<T>, readonly minute5: MetricEndpointBuilder<T>, readonly minute10: MetricEndpointBuilder<T>, readonly minute30: MetricEndpointBuilder<T>, readonly hour1: MetricEndpointBuilder<T>, readonly hour4: MetricEndpointBuilder<T>, readonly hour12: MetricEndpointBuilder<T>, readonly day1: MetricEndpointBuilder<T>, readonly day3: MetricEndpointBuilder<T>, readonly week1: MetricEndpointBuilder<T>, readonly month1: MetricEndpointBuilder<T>, readonly month3: MetricEndpointBuilder<T>, readonly month6: MetricEndpointBuilder<T>, readonly year1: MetricEndpointBuilder<T>, readonly year10: MetricEndpointBuilder<T>, readonly halvingepoch: MetricEndpointBuilder<T>, readonly difficultyepoch: MetricEndpointBuilder<T>, readonly height: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern1 */
/** @template T @typedef {{ name: string, by: { readonly minute1: DateMetricEndpointBuilder<T>, readonly minute5: DateMetricEndpointBuilder<T>, readonly minute10: DateMetricEndpointBuilder<T>, readonly minute30: DateMetricEndpointBuilder<T>, readonly hour1: DateMetricEndpointBuilder<T>, readonly hour4: DateMetricEndpointBuilder<T>, readonly hour12: DateMetricEndpointBuilder<T>, readonly day1: DateMetricEndpointBuilder<T>, readonly day3: DateMetricEndpointBuilder<T>, readonly week1: DateMetricEndpointBuilder<T>, readonly month1: DateMetricEndpointBuilder<T>, readonly month3: DateMetricEndpointBuilder<T>, readonly month6: DateMetricEndpointBuilder<T>, readonly year1: DateMetricEndpointBuilder<T>, readonly year10: DateMetricEndpointBuilder<T>, readonly halvingepoch: MetricEndpointBuilder<T>, readonly difficultyepoch: MetricEndpointBuilder<T>, readonly height: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern1 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern1<T>} */
function createMetricPattern1(client, name) { return _mp(client, name, _i1); }
/** @template T @typedef {{ name: string, by: { readonly minute1: MetricEndpointBuilder<T>, readonly minute5: MetricEndpointBuilder<T>, readonly minute10: MetricEndpointBuilder<T>, readonly minute30: MetricEndpointBuilder<T>, readonly hour1: MetricEndpointBuilder<T>, readonly hour4: MetricEndpointBuilder<T>, readonly hour12: MetricEndpointBuilder<T>, readonly day1: MetricEndpointBuilder<T>, readonly day3: MetricEndpointBuilder<T>, readonly week1: MetricEndpointBuilder<T>, readonly month1: MetricEndpointBuilder<T>, readonly month3: MetricEndpointBuilder<T>, readonly month6: MetricEndpointBuilder<T>, readonly year1: MetricEndpointBuilder<T>, readonly year10: MetricEndpointBuilder<T>, readonly halvingepoch: MetricEndpointBuilder<T>, readonly difficultyepoch: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern2 */
function createMetricPattern1(client, name) { return /** @type {MetricPattern1<T>} */ (_mp(client, name, _i1)); }
/** @template T @typedef {{ name: string, by: { readonly minute1: DateMetricEndpointBuilder<T>, readonly minute5: DateMetricEndpointBuilder<T>, readonly minute10: DateMetricEndpointBuilder<T>, readonly minute30: DateMetricEndpointBuilder<T>, readonly hour1: DateMetricEndpointBuilder<T>, readonly hour4: DateMetricEndpointBuilder<T>, readonly hour12: DateMetricEndpointBuilder<T>, readonly day1: DateMetricEndpointBuilder<T>, readonly day3: DateMetricEndpointBuilder<T>, readonly week1: DateMetricEndpointBuilder<T>, readonly month1: DateMetricEndpointBuilder<T>, readonly month3: DateMetricEndpointBuilder<T>, readonly month6: DateMetricEndpointBuilder<T>, readonly year1: DateMetricEndpointBuilder<T>, readonly year10: DateMetricEndpointBuilder<T>, readonly halvingepoch: MetricEndpointBuilder<T>, readonly difficultyepoch: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern2 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern2<T>} */
function createMetricPattern2(client, name) { return _mp(client, name, _i2); }
/** @template T @typedef {{ name: string, by: { readonly minute1: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern3 */
function createMetricPattern2(client, name) { return /** @type {MetricPattern2<T>} */ (_mp(client, name, _i2)); }
/** @template T @typedef {{ name: string, by: { readonly minute1: DateMetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern3 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern3<T>} */
function createMetricPattern3(client, name) { return _mp(client, name, _i3); }
/** @template T @typedef {{ name: string, by: { readonly minute5: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern4 */
function createMetricPattern3(client, name) { return /** @type {MetricPattern3<T>} */ (_mp(client, name, _i3)); }
/** @template T @typedef {{ name: string, by: { readonly minute5: DateMetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern4 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern4<T>} */
function createMetricPattern4(client, name) { return _mp(client, name, _i4); }
/** @template T @typedef {{ name: string, by: { readonly minute10: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern5 */
function createMetricPattern4(client, name) { return /** @type {MetricPattern4<T>} */ (_mp(client, name, _i4)); }
/** @template T @typedef {{ name: string, by: { readonly minute10: DateMetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern5 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern5<T>} */
function createMetricPattern5(client, name) { return _mp(client, name, _i5); }
/** @template T @typedef {{ name: string, by: { readonly minute30: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern6 */
function createMetricPattern5(client, name) { return /** @type {MetricPattern5<T>} */ (_mp(client, name, _i5)); }
/** @template T @typedef {{ name: string, by: { readonly minute30: DateMetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern6 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern6<T>} */
function createMetricPattern6(client, name) { return _mp(client, name, _i6); }
/** @template T @typedef {{ name: string, by: { readonly hour1: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern7 */
function createMetricPattern6(client, name) { return /** @type {MetricPattern6<T>} */ (_mp(client, name, _i6)); }
/** @template T @typedef {{ name: string, by: { readonly hour1: DateMetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern7 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern7<T>} */
function createMetricPattern7(client, name) { return _mp(client, name, _i7); }
/** @template T @typedef {{ name: string, by: { readonly hour4: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern8 */
function createMetricPattern7(client, name) { return /** @type {MetricPattern7<T>} */ (_mp(client, name, _i7)); }
/** @template T @typedef {{ name: string, by: { readonly hour4: DateMetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern8 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern8<T>} */
function createMetricPattern8(client, name) { return _mp(client, name, _i8); }
/** @template T @typedef {{ name: string, by: { readonly hour12: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern9 */
function createMetricPattern8(client, name) { return /** @type {MetricPattern8<T>} */ (_mp(client, name, _i8)); }
/** @template T @typedef {{ name: string, by: { readonly hour12: DateMetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern9 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern9<T>} */
function createMetricPattern9(client, name) { return _mp(client, name, _i9); }
/** @template T @typedef {{ name: string, by: { readonly day1: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern10 */
function createMetricPattern9(client, name) { return /** @type {MetricPattern9<T>} */ (_mp(client, name, _i9)); }
/** @template T @typedef {{ name: string, by: { readonly day1: DateMetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern10 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern10<T>} */
function createMetricPattern10(client, name) { return _mp(client, name, _i10); }
/** @template T @typedef {{ name: string, by: { readonly day3: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern11 */
function createMetricPattern10(client, name) { return /** @type {MetricPattern10<T>} */ (_mp(client, name, _i10)); }
/** @template T @typedef {{ name: string, by: { readonly day3: DateMetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern11 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern11<T>} */
function createMetricPattern11(client, name) { return _mp(client, name, _i11); }
/** @template T @typedef {{ name: string, by: { readonly week1: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern12 */
function createMetricPattern11(client, name) { return /** @type {MetricPattern11<T>} */ (_mp(client, name, _i11)); }
/** @template T @typedef {{ name: string, by: { readonly week1: DateMetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern12 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern12<T>} */
function createMetricPattern12(client, name) { return _mp(client, name, _i12); }
/** @template T @typedef {{ name: string, by: { readonly month1: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern13 */
function createMetricPattern12(client, name) { return /** @type {MetricPattern12<T>} */ (_mp(client, name, _i12)); }
/** @template T @typedef {{ name: string, by: { readonly month1: DateMetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern13 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern13<T>} */
function createMetricPattern13(client, name) { return _mp(client, name, _i13); }
/** @template T @typedef {{ name: string, by: { readonly month3: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern14 */
function createMetricPattern13(client, name) { return /** @type {MetricPattern13<T>} */ (_mp(client, name, _i13)); }
/** @template T @typedef {{ name: string, by: { readonly month3: DateMetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern14 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern14<T>} */
function createMetricPattern14(client, name) { return _mp(client, name, _i14); }
/** @template T @typedef {{ name: string, by: { readonly month6: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern15 */
function createMetricPattern14(client, name) { return /** @type {MetricPattern14<T>} */ (_mp(client, name, _i14)); }
/** @template T @typedef {{ name: string, by: { readonly month6: DateMetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern15 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern15<T>} */
function createMetricPattern15(client, name) { return _mp(client, name, _i15); }
/** @template T @typedef {{ name: string, by: { readonly year1: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern16 */
function createMetricPattern15(client, name) { return /** @type {MetricPattern15<T>} */ (_mp(client, name, _i15)); }
/** @template T @typedef {{ name: string, by: { readonly year1: DateMetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern16 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern16<T>} */
function createMetricPattern16(client, name) { return _mp(client, name, _i16); }
/** @template T @typedef {{ name: string, by: { readonly year10: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern17 */
function createMetricPattern16(client, name) { return /** @type {MetricPattern16<T>} */ (_mp(client, name, _i16)); }
/** @template T @typedef {{ name: string, by: { readonly year10: DateMetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern17 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern17<T>} */
function createMetricPattern17(client, name) { return _mp(client, name, _i17); }
function createMetricPattern17(client, name) { return /** @type {MetricPattern17<T>} */ (_mp(client, name, _i17)); }
/** @template T @typedef {{ name: string, by: { readonly halvingepoch: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern18 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern18<T>} */
function createMetricPattern18(client, name) { return _mp(client, name, _i18); }
function createMetricPattern18(client, name) { return /** @type {MetricPattern18<T>} */ (_mp(client, name, _i18)); }
/** @template T @typedef {{ name: string, by: { readonly difficultyepoch: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern19 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern19<T>} */
function createMetricPattern19(client, name) { return _mp(client, name, _i19); }
function createMetricPattern19(client, name) { return /** @type {MetricPattern19<T>} */ (_mp(client, name, _i19)); }
/** @template T @typedef {{ name: string, by: { readonly height: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern20 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern20<T>} */
function createMetricPattern20(client, name) { return _mp(client, name, _i20); }
function createMetricPattern20(client, name) { return /** @type {MetricPattern20<T>} */ (_mp(client, name, _i20)); }
/** @template T @typedef {{ name: string, by: { readonly txindex: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern21 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern21<T>} */
function createMetricPattern21(client, name) { return _mp(client, name, _i21); }
function createMetricPattern21(client, name) { return /** @type {MetricPattern21<T>} */ (_mp(client, name, _i21)); }
/** @template T @typedef {{ name: string, by: { readonly txinindex: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern22 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern22<T>} */
function createMetricPattern22(client, name) { return _mp(client, name, _i22); }
function createMetricPattern22(client, name) { return /** @type {MetricPattern22<T>} */ (_mp(client, name, _i22)); }
/** @template T @typedef {{ name: string, by: { readonly txoutindex: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern23 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern23<T>} */
function createMetricPattern23(client, name) { return _mp(client, name, _i23); }
function createMetricPattern23(client, name) { return /** @type {MetricPattern23<T>} */ (_mp(client, name, _i23)); }
/** @template T @typedef {{ name: string, by: { readonly emptyoutputindex: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern24 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern24<T>} */
function createMetricPattern24(client, name) { return _mp(client, name, _i24); }
function createMetricPattern24(client, name) { return /** @type {MetricPattern24<T>} */ (_mp(client, name, _i24)); }
/** @template T @typedef {{ name: string, by: { readonly opreturnindex: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern25 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern25<T>} */
function createMetricPattern25(client, name) { return _mp(client, name, _i25); }
function createMetricPattern25(client, name) { return /** @type {MetricPattern25<T>} */ (_mp(client, name, _i25)); }
/** @template T @typedef {{ name: string, by: { readonly p2aaddressindex: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern26 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern26<T>} */
function createMetricPattern26(client, name) { return _mp(client, name, _i26); }
function createMetricPattern26(client, name) { return /** @type {MetricPattern26<T>} */ (_mp(client, name, _i26)); }
/** @template T @typedef {{ name: string, by: { readonly p2msoutputindex: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern27 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern27<T>} */
function createMetricPattern27(client, name) { return _mp(client, name, _i27); }
function createMetricPattern27(client, name) { return /** @type {MetricPattern27<T>} */ (_mp(client, name, _i27)); }
/** @template T @typedef {{ name: string, by: { readonly p2pk33addressindex: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern28 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern28<T>} */
function createMetricPattern28(client, name) { return _mp(client, name, _i28); }
function createMetricPattern28(client, name) { return /** @type {MetricPattern28<T>} */ (_mp(client, name, _i28)); }
/** @template T @typedef {{ name: string, by: { readonly p2pk65addressindex: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern29 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern29<T>} */
function createMetricPattern29(client, name) { return _mp(client, name, _i29); }
function createMetricPattern29(client, name) { return /** @type {MetricPattern29<T>} */ (_mp(client, name, _i29)); }
/** @template T @typedef {{ name: string, by: { readonly p2pkhaddressindex: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern30 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern30<T>} */
function createMetricPattern30(client, name) { return _mp(client, name, _i30); }
function createMetricPattern30(client, name) { return /** @type {MetricPattern30<T>} */ (_mp(client, name, _i30)); }
/** @template T @typedef {{ name: string, by: { readonly p2shaddressindex: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern31 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern31<T>} */
function createMetricPattern31(client, name) { return _mp(client, name, _i31); }
function createMetricPattern31(client, name) { return /** @type {MetricPattern31<T>} */ (_mp(client, name, _i31)); }
/** @template T @typedef {{ name: string, by: { readonly p2traddressindex: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern32 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern32<T>} */
function createMetricPattern32(client, name) { return _mp(client, name, _i32); }
function createMetricPattern32(client, name) { return /** @type {MetricPattern32<T>} */ (_mp(client, name, _i32)); }
/** @template T @typedef {{ name: string, by: { readonly p2wpkhaddressindex: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern33 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern33<T>} */
function createMetricPattern33(client, name) { return _mp(client, name, _i33); }
function createMetricPattern33(client, name) { return /** @type {MetricPattern33<T>} */ (_mp(client, name, _i33)); }
/** @template T @typedef {{ name: string, by: { readonly p2wshaddressindex: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern34 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern34<T>} */
function createMetricPattern34(client, name) { return _mp(client, name, _i34); }
function createMetricPattern34(client, name) { return /** @type {MetricPattern34<T>} */ (_mp(client, name, _i34)); }
/** @template T @typedef {{ name: string, by: { readonly unknownoutputindex: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern35 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern35<T>} */
function createMetricPattern35(client, name) { return _mp(client, name, _i35); }
function createMetricPattern35(client, name) { return /** @type {MetricPattern35<T>} */ (_mp(client, name, _i35)); }
/** @template T @typedef {{ name: string, by: { readonly fundedaddressindex: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern36 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern36<T>} */
function createMetricPattern36(client, name) { return _mp(client, name, _i36); }
function createMetricPattern36(client, name) { return /** @type {MetricPattern36<T>} */ (_mp(client, name, _i36)); }
/** @template T @typedef {{ name: string, by: { readonly emptyaddressindex: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern37 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern37<T>} */
function createMetricPattern37(client, name) { return _mp(client, name, _i37); }
function createMetricPattern37(client, name) { return /** @type {MetricPattern37<T>} */ (_mp(client, name, _i37)); }
// Reusable structural pattern factories
+52 -32
View File
@@ -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<number, value>
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<number, value>
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");
+218 -145
View File
@@ -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]
+404 -162
View File
@@ -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"]
+477 -125
View File
@@ -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
@@ -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: [
+9 -11
View File
@@ -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) => {
+113 -20
View File
@@ -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: [
+81 -14
View File
@@ -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",
+24 -4
View File
@@ -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,
}),
],
};
}
@@ -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),
],
};
+416 -47
View File
@@ -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: [
+315 -27
View File
@@ -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,
+4 -4
View File
@@ -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,
+8 -4
View File
@@ -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<any>, sats: SumStatsPattern<any>, usd: SumStatsPattern<any>}} SumValuePattern
* AnyValuePatternType: union of all value pattern types
@@ -117,6 +117,10 @@
* @template T
* @typedef {Brk.AverageCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern2<T>} 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<any> | FullStatsPattern<any> | BlockSizePattern} AnyStatsPattern
* @typedef {SumStatsPattern<any> | FullStatsPattern<any> | 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<any> | FullStatsPattern<any>} AnyFullStatsPattern
* @typedef {FullStatsPattern<any> | BtcFullStatsPattern} AnyFullStatsPattern
*
* DCA period keys - derived from pattern types
* @typedef {keyof Brk._10y2y3y4y5y6y8yPattern} LongPeriodKey