mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 14:11:56 -07:00
global: snapshot
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
@@ -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`.
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user