mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 14:11:56 -07:00
global: snapshot
This commit is contained in:
@@ -310,6 +310,9 @@ class BrkClientBase {{
|
||||
this.timeout = isString ? 5000 : (options.timeout ?? 5000);
|
||||
/** @type {{Promise<Cache | null>}} */
|
||||
this._cachePromise = _openCache(isString ? undefined : options.cache);
|
||||
/** @type {{Cache | null}} */
|
||||
this._cache = null;
|
||||
this._cachePromise.then(c => this._cache = c);
|
||||
}}
|
||||
|
||||
/**
|
||||
@@ -325,35 +328,57 @@ class BrkClientBase {{
|
||||
}}
|
||||
|
||||
/**
|
||||
* Make a GET request with stale-while-revalidate caching
|
||||
* Make a GET request - races cache vs network, first to resolve calls onUpdate
|
||||
* @template T
|
||||
* @param {{string}} path
|
||||
* @param {{(value: T) => void}} [onUpdate] - Called when data is available
|
||||
* @param {{(value: T) => void}} [onUpdate] - Called when data is available (may be called twice: cache then network)
|
||||
* @returns {{Promise<T>}}
|
||||
*/
|
||||
async getJson(path, onUpdate) {{
|
||||
const base = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
||||
const url = `${{base}}${{path}}`;
|
||||
const cache = await this._cachePromise;
|
||||
const cachedRes = await cache?.match(url);
|
||||
const cachedJson = cachedRes ? await cachedRes.json() : null;
|
||||
const cache = this._cache ?? await this._cachePromise;
|
||||
|
||||
if (cachedJson) onUpdate?.(cachedJson);
|
||||
if (globalThis.navigator?.onLine === false) {{
|
||||
if (cachedJson) return cachedJson;
|
||||
throw new BrkError('Offline and no cached data available');
|
||||
}}
|
||||
let resolved = false;
|
||||
/** @type {{Response | null}} */
|
||||
let cachedRes = null;
|
||||
|
||||
try {{
|
||||
const res = await this.get(path);
|
||||
if (cachedRes?.headers.get('ETag') === res.headers.get('ETag')) return cachedJson;
|
||||
// Race cache vs network - first to resolve calls onUpdate
|
||||
const cachePromise = cache?.match(url).then(async (res) => {{
|
||||
cachedRes = res ?? null;
|
||||
if (!res) return null;
|
||||
const json = await res.json();
|
||||
if (!resolved && onUpdate) {{
|
||||
resolved = true;
|
||||
onUpdate(json);
|
||||
}}
|
||||
return json;
|
||||
}});
|
||||
|
||||
const networkPromise = this.get(path).then(async (res) => {{
|
||||
const cloned = res.clone();
|
||||
const json = await res.json();
|
||||
onUpdate?.(json);
|
||||
// Skip update if ETag matches and cache already delivered
|
||||
if (cachedRes?.headers.get('ETag') === res.headers.get('ETag')) {{
|
||||
if (!resolved && onUpdate) {{
|
||||
resolved = true;
|
||||
onUpdate(json);
|
||||
}}
|
||||
return json;
|
||||
}}
|
||||
resolved = true;
|
||||
if (onUpdate) {{
|
||||
onUpdate(json);
|
||||
}}
|
||||
if (cache) _runIdle(() => cache.put(url, cloned));
|
||||
return json;
|
||||
}});
|
||||
|
||||
try {{
|
||||
return await networkPromise;
|
||||
}} catch (e) {{
|
||||
// Network failed - wait for cache
|
||||
const cachedJson = await cachePromise?.catch(() => null);
|
||||
if (cachedJson) return cachedJson;
|
||||
throw e;
|
||||
}}
|
||||
|
||||
@@ -56,10 +56,10 @@ impl AddressCohorts {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Apply a function to each aggregate cohort with its source cohorts.
|
||||
fn for_each_aggregate<F>(&mut self, mut f: F) -> Result<()>
|
||||
/// Apply a function to each aggregate cohort with its source cohorts (in parallel).
|
||||
fn for_each_aggregate<F>(&mut self, f: F) -> Result<()>
|
||||
where
|
||||
F: FnMut(&mut AddressCohortVecs, Vec<&AddressCohortVecs>) -> Result<()>,
|
||||
F: Fn(&mut AddressCohortVecs, Vec<&AddressCohortVecs>) -> Result<()> + Sync,
|
||||
{
|
||||
let by_amount_range = &self.0.amount_range;
|
||||
|
||||
@@ -80,10 +80,9 @@ impl AddressCohorts {
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (vecs, sources) in pairs {
|
||||
f(vecs, sources)?;
|
||||
}
|
||||
Ok(())
|
||||
pairs
|
||||
.into_par_iter()
|
||||
.try_for_each(|(vecs, sources)| f(vecs, sources))
|
||||
}
|
||||
|
||||
/// Compute overlapping cohorts from component amount_range cohorts.
|
||||
@@ -105,22 +104,24 @@ impl AddressCohorts {
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
// 1. Compute all metrics except net_sentiment
|
||||
self.par_iter_mut()
|
||||
.try_for_each(|v| v.compute_rest_part1(indexes, price, starting_indexes, exit))
|
||||
}
|
||||
.try_for_each(|v| v.compute_rest_part1(indexes, price, starting_indexes, exit))?;
|
||||
|
||||
/// Recompute net_sentiment for aggregate cohorts as weighted average of source cohorts.
|
||||
pub fn compute_aggregate_net_sentiment(
|
||||
&mut self,
|
||||
indexes: &indexes::Vecs,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
// 2. Compute net_sentiment.height for separate cohorts (greed - pain)
|
||||
self.par_iter_separate_mut()
|
||||
.try_for_each(|v| v.metrics.compute_net_sentiment_height(starting_indexes, exit))?;
|
||||
|
||||
// 3. Compute net_sentiment.height for aggregate cohorts (weighted average)
|
||||
self.for_each_aggregate(|vecs, sources| {
|
||||
let metrics: Vec<_> = sources.iter().map(|v| &v.metrics).collect();
|
||||
vecs.metrics
|
||||
.compute_net_sentiment_from_others(starting_indexes, &metrics, indexes, exit)
|
||||
})
|
||||
.compute_net_sentiment_from_others(starting_indexes, &metrics, exit)
|
||||
})?;
|
||||
|
||||
// 4. Compute net_sentiment dateindex for ALL cohorts
|
||||
self.par_iter_mut()
|
||||
.try_for_each(|v| v.metrics.compute_net_sentiment_rest(indexes, starting_indexes, exit))
|
||||
}
|
||||
|
||||
/// Second phase of post-processing: compute relative metrics.
|
||||
|
||||
@@ -152,10 +152,10 @@ impl UTXOCohorts {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Apply a function to each aggregate cohort with its source cohorts.
|
||||
fn for_each_aggregate<F>(&mut self, mut f: F) -> Result<()>
|
||||
/// Apply a function to each aggregate cohort with its source cohorts (in parallel).
|
||||
fn for_each_aggregate<F>(&mut self, f: F) -> Result<()>
|
||||
where
|
||||
F: FnMut(&mut UTXOCohortVecs, Vec<&UTXOCohortVecs>) -> Result<()>,
|
||||
F: Fn(&mut UTXOCohortVecs, Vec<&UTXOCohortVecs>) -> Result<()> + Sync,
|
||||
{
|
||||
let by_age_range = &self.0.age_range;
|
||||
let by_amount_range = &self.0.amount_range;
|
||||
@@ -215,10 +215,9 @@ impl UTXOCohorts {
|
||||
}))
|
||||
.collect();
|
||||
|
||||
for (vecs, sources) in pairs {
|
||||
f(vecs, sources)?;
|
||||
}
|
||||
Ok(())
|
||||
pairs
|
||||
.into_par_iter()
|
||||
.try_for_each(|(vecs, sources)| f(vecs, sources))
|
||||
}
|
||||
|
||||
/// Compute overlapping cohorts from component age/amount range cohorts.
|
||||
@@ -240,26 +239,24 @@ impl UTXOCohorts {
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
// 1. Compute all metrics except net_sentiment
|
||||
self.par_iter_mut()
|
||||
.try_for_each(|v| v.compute_rest_part1(indexes, price, starting_indexes, exit))
|
||||
}
|
||||
.try_for_each(|v| v.compute_rest_part1(indexes, price, starting_indexes, exit))?;
|
||||
|
||||
/// Recompute net_sentiment for aggregate cohorts as weighted average of source cohorts.
|
||||
pub fn compute_aggregate_net_sentiment(
|
||||
&mut self,
|
||||
indexes: &indexes::Vecs,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
// 2. Compute net_sentiment.height for separate cohorts (greed - pain)
|
||||
self.par_iter_separate_mut()
|
||||
.try_for_each(|v| v.metrics.compute_net_sentiment_height(starting_indexes, exit))?;
|
||||
|
||||
// 3. Compute net_sentiment.height for aggregate cohorts (weighted average)
|
||||
self.for_each_aggregate(|vecs, sources| {
|
||||
let metrics: Vec<_> = sources.iter().map(|v| &v.metrics).collect();
|
||||
vecs.metrics.compute_net_sentiment_from_others(
|
||||
starting_indexes,
|
||||
&metrics,
|
||||
indexes,
|
||||
exit,
|
||||
)
|
||||
})
|
||||
vecs.metrics
|
||||
.compute_net_sentiment_from_others(starting_indexes, &metrics, exit)
|
||||
})?;
|
||||
|
||||
// 4. Compute net_sentiment dateindex for ALL cohorts
|
||||
self.par_iter_mut()
|
||||
.try_for_each(|v| v.metrics.compute_net_sentiment_rest(indexes, starting_indexes, exit))
|
||||
}
|
||||
|
||||
/// Second phase of post-processing: compute relative metrics.
|
||||
|
||||
@@ -42,10 +42,6 @@ pub fn compute_rest_part1(
|
||||
utxo_cohorts.compute_rest_part1(indexes, price, starting_indexes, exit)?;
|
||||
address_cohorts.compute_rest_part1(indexes, price, starting_indexes, exit)?;
|
||||
|
||||
// Recompute net_sentiment for aggregate cohorts as weighted average
|
||||
utxo_cohorts.compute_aggregate_net_sentiment(indexes, starting_indexes, exit)?;
|
||||
address_cohorts.compute_aggregate_net_sentiment(indexes, starting_indexes, exit)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -296,16 +296,17 @@ impl CohortMetrics {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute net_sentiment as capital-weighted average of component cohorts.
|
||||
/// Compute net_sentiment.height as capital-weighted average of component cohorts.
|
||||
///
|
||||
/// For aggregate cohorts, the simple greed-pain formula produces values outside
|
||||
/// the range of components due to asymmetric weighting. This recomputes net_sentiment
|
||||
/// the range of components due to asymmetric weighting. This computes net_sentiment
|
||||
/// as a proper weighted average using realized_cap as weight.
|
||||
///
|
||||
/// Only computes height; dateindex derivation is done separately via compute_net_sentiment_rest.
|
||||
pub fn compute_net_sentiment_from_others(
|
||||
&mut self,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
others: &[&Self],
|
||||
indexes: &indexes::Vecs,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let Some(unrealized) = self.unrealized.as_mut() else {
|
||||
@@ -325,14 +326,10 @@ impl CohortMetrics {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
unrealized
|
||||
Ok(unrealized
|
||||
.net_sentiment
|
||||
.height
|
||||
.compute_weighted_average_of_others(starting_indexes.height, &weights, &values, exit)?;
|
||||
|
||||
unrealized
|
||||
.net_sentiment
|
||||
.compute_rest(indexes, starting_indexes, exit)
|
||||
.compute_weighted_average_of_others(starting_indexes.height, &weights, &values, exit)?)
|
||||
}
|
||||
|
||||
/// First phase of computed metrics (indexes from height).
|
||||
@@ -389,4 +386,31 @@ impl CohortMetrics {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute net_sentiment.height for separate cohorts (greed - pain).
|
||||
/// Called only for separate cohorts; aggregates compute via weighted average in compute_from_stateful.
|
||||
pub fn compute_net_sentiment_height(
|
||||
&mut self,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
if let Some(unrealized) = self.unrealized.as_mut() {
|
||||
unrealized.compute_net_sentiment_height(starting_indexes, exit)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute net_sentiment dateindex derivation from height.
|
||||
/// Called for ALL cohorts after height is computed.
|
||||
pub fn compute_net_sentiment_rest(
|
||||
&mut self,
|
||||
indexes: &indexes::Vecs,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
if let Some(unrealized) = self.unrealized.as_mut() {
|
||||
unrealized.compute_net_sentiment_rest(indexes, starting_indexes, exit)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,17 +598,32 @@ impl UnrealizedMetrics {
|
||||
)?)
|
||||
})?;
|
||||
|
||||
// Net sentiment: greed - pain
|
||||
self.net_sentiment
|
||||
.compute_all(indexes, starting_indexes, exit, |vec| {
|
||||
Ok(vec.compute_subtract(
|
||||
starting_indexes.height,
|
||||
&self.greed_index.height,
|
||||
&self.pain_index.height,
|
||||
exit,
|
||||
)?)
|
||||
})?;
|
||||
// Net sentiment height (greed - pain) computed separately for separate cohorts only
|
||||
// Aggregate cohorts compute it via weighted average in compute_from_stateful
|
||||
// Dateindex derivation for ALL cohorts happens in compute_net_sentiment_rest
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute net_sentiment.height for separate cohorts (greed - pain).
|
||||
/// Aggregate cohorts skip this - their height is computed via weighted average in compute_from_stateful.
|
||||
pub fn compute_net_sentiment_height(&mut self, starting_indexes: &ComputeIndexes, exit: &Exit) -> Result<()> {
|
||||
Ok(self.net_sentiment.height.compute_subtract(
|
||||
starting_indexes.height,
|
||||
&self.greed_index.height,
|
||||
&self.pain_index.height,
|
||||
exit,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Compute net_sentiment dateindex derivation from height.
|
||||
/// Called for ALL cohorts after height is computed (either via greed-pain or weighted avg).
|
||||
pub fn compute_net_sentiment_rest(
|
||||
&mut self,
|
||||
indexes: &indexes::Vecs,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.net_sentiment.compute_rest(indexes, starting_indexes, exit)
|
||||
}
|
||||
}
|
||||
|
||||
+39
-14
@@ -1125,6 +1125,9 @@ class BrkClientBase {
|
||||
this.timeout = isString ? 5000 : (options.timeout ?? 5000);
|
||||
/** @type {Promise<Cache | null>} */
|
||||
this._cachePromise = _openCache(isString ? undefined : options.cache);
|
||||
/** @type {Cache | null} */
|
||||
this._cache = null;
|
||||
this._cachePromise.then(c => this._cache = c);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1140,35 +1143,57 @@ class BrkClientBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a GET request with stale-while-revalidate caching
|
||||
* Make a GET request - races cache vs network, first to resolve calls onUpdate
|
||||
* @template T
|
||||
* @param {string} path
|
||||
* @param {(value: T) => void} [onUpdate] - Called when data is available
|
||||
* @param {(value: T) => void} [onUpdate] - Called when data is available (may be called twice: cache then network)
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
async getJson(path, onUpdate) {
|
||||
const base = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
||||
const url = `${base}${path}`;
|
||||
const cache = await this._cachePromise;
|
||||
const cachedRes = await cache?.match(url);
|
||||
const cachedJson = cachedRes ? await cachedRes.json() : null;
|
||||
const cache = this._cache ?? await this._cachePromise;
|
||||
|
||||
if (cachedJson) onUpdate?.(cachedJson);
|
||||
if (globalThis.navigator?.onLine === false) {
|
||||
if (cachedJson) return cachedJson;
|
||||
throw new BrkError('Offline and no cached data available');
|
||||
}
|
||||
let resolved = false;
|
||||
/** @type {Response | null} */
|
||||
let cachedRes = null;
|
||||
|
||||
try {
|
||||
const res = await this.get(path);
|
||||
if (cachedRes?.headers.get('ETag') === res.headers.get('ETag')) return cachedJson;
|
||||
// Race cache vs network - first to resolve calls onUpdate
|
||||
const cachePromise = cache?.match(url).then(async (res) => {
|
||||
cachedRes = res ?? null;
|
||||
if (!res) return null;
|
||||
const json = await res.json();
|
||||
if (!resolved && onUpdate) {
|
||||
resolved = true;
|
||||
onUpdate(json);
|
||||
}
|
||||
return json;
|
||||
});
|
||||
|
||||
const networkPromise = this.get(path).then(async (res) => {
|
||||
const cloned = res.clone();
|
||||
const json = await res.json();
|
||||
onUpdate?.(json);
|
||||
// Skip update if ETag matches and cache already delivered
|
||||
if (cachedRes?.headers.get('ETag') === res.headers.get('ETag')) {
|
||||
if (!resolved && onUpdate) {
|
||||
resolved = true;
|
||||
onUpdate(json);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
resolved = true;
|
||||
if (onUpdate) {
|
||||
onUpdate(json);
|
||||
}
|
||||
if (cache) _runIdle(() => cache.put(url, cloned));
|
||||
return json;
|
||||
});
|
||||
|
||||
try {
|
||||
return await networkPromise;
|
||||
} catch (e) {
|
||||
// Network failed - wait for cache
|
||||
const cachedJson = await cachePromise?.catch(() => null);
|
||||
if (cachedJson) return cachedJson;
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ cd "$ROOT_DIR/modules/brk-client"
|
||||
sed -i '' 's/"version": "[^"]*"/"version": "'"$VERSION"'"/' package.json
|
||||
echo "Updated package.json to $VERSION"
|
||||
|
||||
# Update VERSION in index.js
|
||||
sed -i '' 's/VERSION = "v[^"]*"/VERSION = "v'"$VERSION"'"/' index.js
|
||||
echo "Updated index.js VERSION to v$VERSION"
|
||||
|
||||
# Determine npm tag based on version
|
||||
if [[ "$VERSION" == *"-alpha"* ]]; then
|
||||
NPM_TAG="alpha"
|
||||
|
||||
@@ -16,6 +16,10 @@ cd "$ROOT_DIR/packages/brk_client"
|
||||
sed -i '' 's/^version = "[^"]*"/version = "'"$VERSION"'"/' pyproject.toml
|
||||
echo "Updated pyproject.toml to $VERSION"
|
||||
|
||||
# Update VERSION in __init__.py
|
||||
sed -i '' 's/VERSION = "v[^"]*"/VERSION = "v'"$VERSION"'"/' brk_client/__init__.py
|
||||
echo "Updated __init__.py VERSION to v$VERSION"
|
||||
|
||||
# Clean old build artifacts
|
||||
rm -rf dist
|
||||
|
||||
|
||||
@@ -114,6 +114,12 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
|
||||
// Used to detect and ignore stale operations (in-flight fetches, etc.)
|
||||
let generation = 0;
|
||||
|
||||
// Shared time - fetched once per rebuild, all series register callbacks
|
||||
/** @type {number[] | null} */
|
||||
let sharedTimeData = null;
|
||||
/** @type {Set<(data: number[]) => void>} */
|
||||
let timeCallbacks = new Set();
|
||||
|
||||
// Range state: localStorage stores all ranges per-index, URL stores current range only
|
||||
/** @typedef {{ from: number, to: number }} Range */
|
||||
const ranges = createPersistedValue({
|
||||
@@ -519,13 +525,16 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
|
||||
active.value ? show() : hide();
|
||||
|
||||
const seriesGeneration = generation;
|
||||
let hasData = false;
|
||||
let lastTime = -Infinity;
|
||||
/** @type {string | null} */
|
||||
let lastStamp = null;
|
||||
|
||||
/** @type {VoidFunction | null} */
|
||||
let _fetch = null;
|
||||
const state = {
|
||||
hasData: false,
|
||||
lastTime: -Infinity,
|
||||
/** @type {string | null} */
|
||||
lastStamp: null,
|
||||
/** @type {VoidFunction | null} */
|
||||
fetch: null,
|
||||
/** @type {((data: number[]) => void) | null} */
|
||||
onTime: null,
|
||||
};
|
||||
|
||||
/** @type {AnySeries} */
|
||||
const series = {
|
||||
@@ -535,7 +544,7 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
|
||||
active.set(value);
|
||||
value ? show() : hide();
|
||||
if (value && !wasActive) {
|
||||
_fetch?.();
|
||||
state.fetch?.();
|
||||
}
|
||||
panes.updateVisibility();
|
||||
},
|
||||
@@ -544,14 +553,15 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
|
||||
hide,
|
||||
highlight,
|
||||
tame,
|
||||
hasData: () => hasData,
|
||||
fetch: () => _fetch?.(),
|
||||
hasData: () => state.hasData,
|
||||
fetch: () => state.fetch?.(),
|
||||
id,
|
||||
paneIndex,
|
||||
url: null,
|
||||
getData,
|
||||
update,
|
||||
remove() {
|
||||
if (state.onTime) timeCallbacks.delete(state.onTime);
|
||||
onRemove();
|
||||
serieses.all.delete(series);
|
||||
panes.seriesByHome.get(paneIndex)?.delete(series);
|
||||
@@ -563,9 +573,9 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
|
||||
/** @param {ChartableIndex} idx */
|
||||
function setupIndexEffect(idx) {
|
||||
// Reset data state for new index
|
||||
hasData = false;
|
||||
lastTime = -Infinity;
|
||||
_fetch = null;
|
||||
state.hasData = false;
|
||||
state.lastTime = -Infinity;
|
||||
state.fetch = null;
|
||||
|
||||
const _valuesEndpoint = metric.by[idx];
|
||||
// Gracefully skip - series may be about to be removed by option change
|
||||
@@ -590,13 +600,13 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
|
||||
|
||||
// Find start index for processing
|
||||
let startIdx = 0;
|
||||
if (hasData) {
|
||||
// Binary search to find first index where time >= lastTime
|
||||
if (state.hasData) {
|
||||
// Binary search to find first index where time >= state.lastTime
|
||||
let lo = 0;
|
||||
let hi = length;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi) >>> 1;
|
||||
if (indexes[mid] < lastTime) {
|
||||
if (indexes[mid] < state.lastTime) {
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid;
|
||||
@@ -625,7 +635,7 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasData) {
|
||||
if (!state.hasData) {
|
||||
// Initial load: build full array
|
||||
const data = /** @type {LineData[] | CandlestickData[]} */ (
|
||||
Array.from({ length })
|
||||
@@ -655,8 +665,9 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
|
||||
data.length -= timeOffset;
|
||||
|
||||
setData(data);
|
||||
hasData = true;
|
||||
lastTime = /** @type {number} */ (data.at(-1)?.time) ?? -Infinity;
|
||||
state.hasData = true;
|
||||
state.lastTime =
|
||||
/** @type {number} */ (data.at(-1)?.time) ?? -Infinity;
|
||||
|
||||
// Restore saved range or use defaults
|
||||
const savedRange = getRange();
|
||||
@@ -685,26 +696,45 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
|
||||
for (let i = startIdx; i < length; i++) {
|
||||
const point = buildDataPoint(i);
|
||||
update(point);
|
||||
lastTime = /** @type {number} */ (point.time);
|
||||
state.lastTime = /** @type {number} */ (point.time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAndProcess() {
|
||||
const [timeResult, valuesResult] = await Promise.all([
|
||||
getTimeEndpoint(idx).slice(-10000).fetch(),
|
||||
valuesEndpoint.slice(-10000).fetch(),
|
||||
]);
|
||||
// Ignore stale fetches from series that have been replaced
|
||||
if (seriesGeneration !== generation) return;
|
||||
if (valuesResult.stamp === lastStamp) return;
|
||||
lastStamp = valuesResult.stamp;
|
||||
if (timeResult.data.length && valuesResult.data.length) {
|
||||
processData(timeResult.data, valuesResult.data);
|
||||
/** @type {number[] | null} */
|
||||
let timeData = null;
|
||||
/** @type {(number | null | [number, number, number, number])[] | null} */
|
||||
let valuesData = null;
|
||||
/** @type {string | null} */
|
||||
let valuesStamp = null;
|
||||
|
||||
function tryProcess() {
|
||||
if (seriesGeneration !== generation) return;
|
||||
if (!timeData || !valuesData) return;
|
||||
if (valuesStamp === state.lastStamp) return;
|
||||
state.lastStamp = valuesStamp;
|
||||
if (timeData.length && valuesData.length) {
|
||||
processData(timeData, valuesData);
|
||||
}
|
||||
}
|
||||
|
||||
// Register for shared time data (fetched once in rebuild)
|
||||
state.onTime = (data) => {
|
||||
timeData = data;
|
||||
tryProcess();
|
||||
};
|
||||
timeCallbacks.add(state.onTime);
|
||||
if (sharedTimeData) state.onTime(sharedTimeData);
|
||||
|
||||
await valuesEndpoint.slice(-10000).fetch((result) => {
|
||||
valuesData = result.data;
|
||||
valuesStamp = result.stamp;
|
||||
tryProcess();
|
||||
});
|
||||
}
|
||||
|
||||
_fetch = fetchAndProcess;
|
||||
state.fetch = fetchAndProcess;
|
||||
|
||||
// Initial fetch if active
|
||||
if (active.value) {
|
||||
@@ -1597,6 +1627,17 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
|
||||
|
||||
rebuild() {
|
||||
generation++;
|
||||
const currentGen = generation;
|
||||
const idx = index.get();
|
||||
sharedTimeData = null;
|
||||
timeCallbacks = new Set();
|
||||
getTimeEndpoint(idx)
|
||||
.slice(-10000)
|
||||
.fetch((result) => {
|
||||
if (currentGen !== generation) return; // Ignore stale fetch
|
||||
sharedTimeData = result.data;
|
||||
timeCallbacks.forEach((cb) => cb(result.data));
|
||||
});
|
||||
this.rebuildPane(0);
|
||||
this.rebuildPane(1);
|
||||
},
|
||||
|
||||
@@ -132,7 +132,7 @@ function valueBreakdownTree(list, all, title) {
|
||||
{
|
||||
name: "Created",
|
||||
title: title("Profit Value Created"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.realized.profitValueCreated,
|
||||
name,
|
||||
@@ -144,7 +144,7 @@ function valueBreakdownTree(list, all, title) {
|
||||
{
|
||||
name: "Destroyed",
|
||||
title: title("Profit Value Destroyed"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.realized.profitValueDestroyed,
|
||||
name,
|
||||
@@ -161,7 +161,7 @@ function valueBreakdownTree(list, all, title) {
|
||||
{
|
||||
name: "Created",
|
||||
title: title("Loss Value Created"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.realized.lossValueCreated,
|
||||
name,
|
||||
@@ -173,7 +173,7 @@ function valueBreakdownTree(list, all, title) {
|
||||
{
|
||||
name: "Destroyed",
|
||||
title: title("Loss Value Destroyed"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.realized.lossValueDestroyed,
|
||||
name,
|
||||
@@ -200,7 +200,7 @@ function coinsDestroyedTree(list, all, title) {
|
||||
{
|
||||
name: "Sum",
|
||||
title: title("Coins Destroyed"),
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ color, name, tree }) => [
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [
|
||||
line({
|
||||
metric: tree.activity.coinblocksDestroyed.sum,
|
||||
name,
|
||||
@@ -218,7 +218,7 @@ function coinsDestroyedTree(list, all, title) {
|
||||
{
|
||||
name: "Cumulative",
|
||||
title: title("Cumulative Coins Destroyed"),
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ color, name, tree }) => [
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [
|
||||
line({
|
||||
metric: tree.activity.coinblocksDestroyed.cumulative,
|
||||
name,
|
||||
@@ -594,7 +594,7 @@ function groupedFlowsTree(list, all, title) {
|
||||
{
|
||||
name: "Profit",
|
||||
title: title("Profit Flow"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.realized.profitFlow,
|
||||
name,
|
||||
@@ -606,7 +606,7 @@ function groupedFlowsTree(list, all, title) {
|
||||
{
|
||||
name: "Capitulation",
|
||||
title: title("Capitulation Flow"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.realized.capitulationFlow,
|
||||
name,
|
||||
@@ -632,7 +632,7 @@ function createGroupedValueTree(list, all, title) {
|
||||
{
|
||||
name: "Created",
|
||||
title: title("Value Created"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.realized.valueCreated,
|
||||
name,
|
||||
@@ -644,7 +644,7 @@ function createGroupedValueTree(list, all, title) {
|
||||
{
|
||||
name: "Destroyed",
|
||||
title: title("Value Destroyed"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.realized.valueDestroyed,
|
||||
name,
|
||||
@@ -678,14 +678,14 @@ export function createGroupedActivitySection({
|
||||
{
|
||||
name: "14d EMA",
|
||||
title: title("Sent Volume 14d EMA"),
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
satsBtcUsd({ pattern: tree.activity.sent14dEma, name, color }),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Sum",
|
||||
title: title("Sent Volume"),
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
satsBtcUsd({
|
||||
pattern: {
|
||||
sats: tree.activity.sent.sats.sum,
|
||||
@@ -733,7 +733,7 @@ function createGroupedValueTreeWithAdjusted(list, all, title) {
|
||||
{
|
||||
name: "Created",
|
||||
title: title("Value Created"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.realized.valueCreated,
|
||||
name,
|
||||
@@ -745,7 +745,7 @@ function createGroupedValueTreeWithAdjusted(list, all, title) {
|
||||
{
|
||||
name: "Destroyed",
|
||||
title: title("Value Destroyed"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.realized.valueDestroyed,
|
||||
name,
|
||||
@@ -762,7 +762,7 @@ function createGroupedValueTreeWithAdjusted(list, all, title) {
|
||||
{
|
||||
name: "Created",
|
||||
title: title("Adjusted Value Created"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.realized.adjustedValueCreated,
|
||||
name,
|
||||
@@ -774,7 +774,7 @@ function createGroupedValueTreeWithAdjusted(list, all, title) {
|
||||
{
|
||||
name: "Destroyed",
|
||||
title: title("Adjusted Value Destroyed"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.realized.adjustedValueDestroyed,
|
||||
name,
|
||||
@@ -839,7 +839,7 @@ function createSingleSellSideRiskSeries(tree) {
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
*/
|
||||
function createGroupedSellSideRiskSeries(list, all) {
|
||||
return flatMapCohortsWithAll(list, all, ({ color, name, tree }) => [
|
||||
return flatMapCohortsWithAll(list, all, ({ name, color, tree }) => [
|
||||
line({
|
||||
metric: tree.realized.sellSideRiskRatio,
|
||||
name,
|
||||
|
||||
@@ -1205,7 +1205,7 @@ function groupedPnlCharts(list, all, title) {
|
||||
{
|
||||
name: "Profit",
|
||||
title: title("Unrealized Profit"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.unrealized.unrealizedProfit,
|
||||
name,
|
||||
@@ -1217,7 +1217,7 @@ function groupedPnlCharts(list, all, title) {
|
||||
{
|
||||
name: "Loss",
|
||||
title: title("Unrealized Loss"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.unrealized.negUnrealizedLoss,
|
||||
name,
|
||||
@@ -1229,7 +1229,7 @@ function groupedPnlCharts(list, all, title) {
|
||||
{
|
||||
name: "Net P&L",
|
||||
title: title("Net Unrealized P&L"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.unrealized.netUnrealizedPnl,
|
||||
name,
|
||||
@@ -1254,7 +1254,7 @@ function groupedPnlChartsWithMarketCap(list, all, title) {
|
||||
name: "Profit",
|
||||
title: title("Unrealized Profit"),
|
||||
bottom: [
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.unrealized.unrealizedProfit,
|
||||
name,
|
||||
@@ -1262,7 +1262,7 @@ function groupedPnlChartsWithMarketCap(list, all, title) {
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.relative.unrealizedProfitRelToMarketCap,
|
||||
name,
|
||||
@@ -1276,7 +1276,7 @@ function groupedPnlChartsWithMarketCap(list, all, title) {
|
||||
name: "Loss",
|
||||
title: title("Unrealized Loss"),
|
||||
bottom: [
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.unrealized.negUnrealizedLoss,
|
||||
name,
|
||||
@@ -1284,7 +1284,7 @@ function groupedPnlChartsWithMarketCap(list, all, title) {
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.relative.negUnrealizedLossRelToMarketCap,
|
||||
name,
|
||||
@@ -1298,7 +1298,7 @@ function groupedPnlChartsWithMarketCap(list, all, title) {
|
||||
name: "Net P&L",
|
||||
title: title("Net Unrealized P&L"),
|
||||
bottom: [
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.unrealized.netUnrealizedPnl,
|
||||
name,
|
||||
@@ -1306,7 +1306,7 @@ function groupedPnlChartsWithMarketCap(list, all, title) {
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.relative.netUnrealizedPnlRelToMarketCap,
|
||||
name,
|
||||
@@ -1332,7 +1332,7 @@ function groupedPnlChartsWithOwnMarketCap(list, all, title) {
|
||||
name: "Profit",
|
||||
title: title("Unrealized Profit"),
|
||||
bottom: [
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.unrealized.unrealizedProfit,
|
||||
name,
|
||||
@@ -1341,7 +1341,7 @@ function groupedPnlChartsWithOwnMarketCap(list, all, title) {
|
||||
}),
|
||||
),
|
||||
// OwnMarketCap properties don't exist on CohortAll - use mapCohorts
|
||||
...mapCohorts(list, ({ color, name, tree }) =>
|
||||
...mapCohorts(list, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.relative.unrealizedProfitRelToOwnMarketCap,
|
||||
name,
|
||||
@@ -1349,7 +1349,7 @@ function groupedPnlChartsWithOwnMarketCap(list, all, title) {
|
||||
unit: Unit.pctOwnMcap,
|
||||
}),
|
||||
),
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.relative.unrealizedProfitRelToOwnTotalUnrealizedPnl,
|
||||
name,
|
||||
@@ -1363,7 +1363,7 @@ function groupedPnlChartsWithOwnMarketCap(list, all, title) {
|
||||
name: "Loss",
|
||||
title: title("Unrealized Loss"),
|
||||
bottom: [
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.unrealized.negUnrealizedLoss,
|
||||
name,
|
||||
@@ -1372,7 +1372,7 @@ function groupedPnlChartsWithOwnMarketCap(list, all, title) {
|
||||
}),
|
||||
),
|
||||
// OwnMarketCap properties don't exist on CohortAll - use mapCohorts
|
||||
...mapCohorts(list, ({ color, name, tree }) =>
|
||||
...mapCohorts(list, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.relative.negUnrealizedLossRelToOwnMarketCap,
|
||||
name,
|
||||
@@ -1380,7 +1380,7 @@ function groupedPnlChartsWithOwnMarketCap(list, all, title) {
|
||||
unit: Unit.pctOwnMcap,
|
||||
}),
|
||||
),
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.relative.negUnrealizedLossRelToOwnTotalUnrealizedPnl,
|
||||
name,
|
||||
@@ -1394,7 +1394,7 @@ function groupedPnlChartsWithOwnMarketCap(list, all, title) {
|
||||
name: "Net P&L",
|
||||
title: title("Net Unrealized P&L"),
|
||||
bottom: [
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.unrealized.netUnrealizedPnl,
|
||||
name,
|
||||
@@ -1403,7 +1403,7 @@ function groupedPnlChartsWithOwnMarketCap(list, all, title) {
|
||||
}),
|
||||
),
|
||||
// OwnMarketCap properties don't exist on CohortAll - use mapCohorts
|
||||
...mapCohorts(list, ({ color, name, tree }) =>
|
||||
...mapCohorts(list, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.relative.netUnrealizedPnlRelToOwnMarketCap,
|
||||
name,
|
||||
@@ -1411,7 +1411,7 @@ function groupedPnlChartsWithOwnMarketCap(list, all, title) {
|
||||
unit: Unit.pctOwnMcap,
|
||||
}),
|
||||
),
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.relative.netUnrealizedPnlRelToOwnTotalUnrealizedPnl,
|
||||
name,
|
||||
@@ -1437,7 +1437,7 @@ function groupedPnlChartsLongTerm(list, all, title) {
|
||||
name: "Profit",
|
||||
title: title("Unrealized Profit"),
|
||||
bottom: [
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.unrealized.unrealizedProfit,
|
||||
name,
|
||||
@@ -1446,7 +1446,7 @@ function groupedPnlChartsLongTerm(list, all, title) {
|
||||
}),
|
||||
),
|
||||
// OwnMarketCap properties don't exist on CohortAll - use mapCohorts
|
||||
...mapCohorts(list, ({ color, name, tree }) =>
|
||||
...mapCohorts(list, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.relative.unrealizedProfitRelToOwnMarketCap,
|
||||
name,
|
||||
@@ -1454,7 +1454,7 @@ function groupedPnlChartsLongTerm(list, all, title) {
|
||||
unit: Unit.pctOwnMcap,
|
||||
}),
|
||||
),
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.relative.unrealizedProfitRelToOwnTotalUnrealizedPnl,
|
||||
name,
|
||||
@@ -1468,7 +1468,7 @@ function groupedPnlChartsLongTerm(list, all, title) {
|
||||
name: "Loss",
|
||||
title: title("Unrealized Loss"),
|
||||
bottom: [
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.unrealized.negUnrealizedLoss,
|
||||
name,
|
||||
@@ -1476,7 +1476,7 @@ function groupedPnlChartsLongTerm(list, all, title) {
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.relative.unrealizedLossRelToMarketCap,
|
||||
name,
|
||||
@@ -1485,7 +1485,7 @@ function groupedPnlChartsLongTerm(list, all, title) {
|
||||
}),
|
||||
),
|
||||
// OwnMarketCap properties don't exist on CohortAll - use mapCohorts
|
||||
...mapCohorts(list, ({ color, name, tree }) =>
|
||||
...mapCohorts(list, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.relative.negUnrealizedLossRelToOwnMarketCap,
|
||||
name,
|
||||
@@ -1493,7 +1493,7 @@ function groupedPnlChartsLongTerm(list, all, title) {
|
||||
unit: Unit.pctOwnMcap,
|
||||
}),
|
||||
),
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.relative.negUnrealizedLossRelToOwnTotalUnrealizedPnl,
|
||||
name,
|
||||
@@ -1507,7 +1507,7 @@ function groupedPnlChartsLongTerm(list, all, title) {
|
||||
name: "Net P&L",
|
||||
title: title("Net Unrealized P&L"),
|
||||
bottom: [
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.unrealized.netUnrealizedPnl,
|
||||
name,
|
||||
@@ -1516,7 +1516,7 @@ function groupedPnlChartsLongTerm(list, all, title) {
|
||||
}),
|
||||
),
|
||||
// OwnMarketCap properties don't exist on CohortAll - use mapCohorts
|
||||
...mapCohorts(list, ({ color, name, tree }) =>
|
||||
...mapCohorts(list, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.relative.netUnrealizedPnlRelToOwnMarketCap,
|
||||
name,
|
||||
@@ -1524,7 +1524,7 @@ function groupedPnlChartsLongTerm(list, all, title) {
|
||||
unit: Unit.pctOwnMcap,
|
||||
}),
|
||||
),
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.relative.netUnrealizedPnlRelToOwnTotalUnrealizedPnl,
|
||||
name,
|
||||
@@ -1549,7 +1549,7 @@ function groupedInvestedCapitalAbsolute(list, all, title) {
|
||||
{
|
||||
name: "In Profit",
|
||||
title: title("Invested Capital In Profit"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.unrealized.investedCapitalInProfit,
|
||||
name,
|
||||
@@ -1561,7 +1561,7 @@ function groupedInvestedCapitalAbsolute(list, all, title) {
|
||||
{
|
||||
name: "In Loss",
|
||||
title: title("Invested Capital In Loss"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.unrealized.investedCapitalInLoss,
|
||||
name,
|
||||
@@ -1586,7 +1586,7 @@ function groupedInvestedCapital(list, all, title) {
|
||||
name: "In Profit",
|
||||
title: title("Invested Capital In Profit"),
|
||||
bottom: [
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.unrealized.investedCapitalInProfit,
|
||||
name,
|
||||
@@ -1594,7 +1594,7 @@ function groupedInvestedCapital(list, all, title) {
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.relative.investedCapitalInProfitPct,
|
||||
name,
|
||||
@@ -1609,7 +1609,7 @@ function groupedInvestedCapital(list, all, title) {
|
||||
name: "In Loss",
|
||||
title: title("Invested Capital In Loss"),
|
||||
bottom: [
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.unrealized.investedCapitalInLoss,
|
||||
name,
|
||||
@@ -1617,7 +1617,7 @@ function groupedInvestedCapital(list, all, title) {
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.relative.investedCapitalInLossPct,
|
||||
name,
|
||||
@@ -1643,7 +1643,7 @@ function groupedRealizedPnlSum(list, all, title) {
|
||||
{
|
||||
name: "Profit",
|
||||
title: title("Realized Profit"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.realized.realizedProfit.sum,
|
||||
name,
|
||||
@@ -1655,7 +1655,7 @@ function groupedRealizedPnlSum(list, all, title) {
|
||||
{
|
||||
name: "Loss",
|
||||
title: title("Realized Loss"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.realized.negRealizedLoss.sum,
|
||||
name,
|
||||
@@ -1667,7 +1667,7 @@ function groupedRealizedPnlSum(list, all, title) {
|
||||
{
|
||||
name: "Total",
|
||||
title: title("Total Realized P&L"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.realized.totalRealizedPnl,
|
||||
name,
|
||||
@@ -1679,7 +1679,7 @@ function groupedRealizedPnlSum(list, all, title) {
|
||||
{
|
||||
name: "Value",
|
||||
title: title("Realized Value"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.realized.realizedValue,
|
||||
name,
|
||||
@@ -1704,7 +1704,7 @@ function groupedRealizedPnlSumWithExtras(list, all, title) {
|
||||
{
|
||||
name: "P/L Ratio",
|
||||
title: title("Realized Profit/Loss Ratio"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.realized.realizedProfitToLossRatio,
|
||||
name,
|
||||
@@ -1728,7 +1728,7 @@ function groupedRealizedPnlCumulative(list, all, title) {
|
||||
{
|
||||
name: "Profit",
|
||||
title: title("Cumulative Realized Profit"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.realized.realizedProfit.cumulative,
|
||||
name,
|
||||
@@ -1740,7 +1740,7 @@ function groupedRealizedPnlCumulative(list, all, title) {
|
||||
{
|
||||
name: "Loss",
|
||||
title: title("Cumulative Realized Loss"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.realized.negRealizedLoss.cumulative,
|
||||
name,
|
||||
@@ -1768,7 +1768,7 @@ function groupedSentInPnl(list, all, title) {
|
||||
name: "In Profit",
|
||||
title: title("Sent In Profit"),
|
||||
bottom: [
|
||||
...flatMapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
satsBtcUsd({
|
||||
pattern: tree.realized.sentInProfit14dEma,
|
||||
name: `${name} 14d EMA`,
|
||||
@@ -1776,7 +1776,7 @@ function groupedSentInPnl(list, all, title) {
|
||||
defaultActive: false,
|
||||
}),
|
||||
),
|
||||
...flatMapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
satsBtcUsdFrom({
|
||||
source: tree.realized.sentInProfit,
|
||||
key: "sum",
|
||||
@@ -1790,7 +1790,7 @@ function groupedSentInPnl(list, all, title) {
|
||||
name: "In Loss",
|
||||
title: title("Sent In Loss"),
|
||||
bottom: [
|
||||
...flatMapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
satsBtcUsd({
|
||||
pattern: tree.realized.sentInLoss14dEma,
|
||||
name: `${name} 14d EMA`,
|
||||
@@ -1798,7 +1798,7 @@ function groupedSentInPnl(list, all, title) {
|
||||
defaultActive: false,
|
||||
}),
|
||||
),
|
||||
...flatMapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
satsBtcUsdFrom({
|
||||
source: tree.realized.sentInLoss,
|
||||
key: "sum",
|
||||
@@ -1816,7 +1816,7 @@ function groupedSentInPnl(list, all, title) {
|
||||
{
|
||||
name: "In Profit",
|
||||
title: title("Cumulative Sent In Profit"),
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
satsBtcUsdFrom({
|
||||
source: tree.realized.sentInProfit,
|
||||
key: "cumulative",
|
||||
@@ -1828,7 +1828,7 @@ function groupedSentInPnl(list, all, title) {
|
||||
{
|
||||
name: "In Loss",
|
||||
title: title("Cumulative Sent In Loss"),
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: flatMapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
satsBtcUsdFrom({
|
||||
source: tree.realized.sentInLoss,
|
||||
key: "cumulative",
|
||||
@@ -1856,7 +1856,7 @@ function groupedSentiment(list, all, title) {
|
||||
{
|
||||
name: "Net",
|
||||
title: title("Net Sentiment"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.unrealized.netSentiment,
|
||||
name,
|
||||
@@ -1868,7 +1868,7 @@ function groupedSentiment(list, all, title) {
|
||||
{
|
||||
name: "Greed",
|
||||
title: title("Greed Index"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.unrealized.greedIndex,
|
||||
name,
|
||||
@@ -1880,7 +1880,7 @@ function groupedSentiment(list, all, title) {
|
||||
{
|
||||
name: "Pain",
|
||||
title: title("Pain Index"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.unrealized.painIndex,
|
||||
name,
|
||||
@@ -1908,7 +1908,7 @@ function groupedRealizedSubfolder(list, all, title) {
|
||||
{
|
||||
name: "Net",
|
||||
title: title("Net Realized P&L"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.realized.netRealizedPnl.sum,
|
||||
name,
|
||||
@@ -1920,7 +1920,7 @@ function groupedRealizedSubfolder(list, all, title) {
|
||||
{
|
||||
name: "30d Change",
|
||||
title: title("Realized P&L 30d Change"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.realized.netRealizedPnlCumulative30dDelta,
|
||||
name,
|
||||
@@ -1936,7 +1936,7 @@ function groupedRealizedSubfolder(list, all, title) {
|
||||
{
|
||||
name: "Net",
|
||||
title: title("Cumulative Net Realized P&L"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.realized.netRealizedPnl.cumulative,
|
||||
name,
|
||||
@@ -1966,7 +1966,7 @@ function groupedRealizedSubfolderWithExtras(list, all, title) {
|
||||
{
|
||||
name: "Net",
|
||||
title: title("Net Realized P&L"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.realized.netRealizedPnl.sum,
|
||||
name,
|
||||
@@ -1978,7 +1978,7 @@ function groupedRealizedSubfolderWithExtras(list, all, title) {
|
||||
{
|
||||
name: "30d Change",
|
||||
title: title("Realized P&L 30d Change"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.realized.netRealizedPnlCumulative30dDelta,
|
||||
name,
|
||||
@@ -1994,7 +1994,7 @@ function groupedRealizedSubfolderWithExtras(list, all, title) {
|
||||
{
|
||||
name: "Net",
|
||||
title: title("Cumulative Net Realized P&L"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.realized.netRealizedPnl.cumulative,
|
||||
name,
|
||||
@@ -2076,7 +2076,7 @@ export function createGroupedProfitabilitySectionWithInvestedCapitalPct({
|
||||
{
|
||||
name: "Peak Regret",
|
||||
title: title("Unrealized Peak Regret"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.unrealized.peakRegret,
|
||||
name,
|
||||
@@ -2111,7 +2111,7 @@ export function createGroupedProfitabilitySectionWithNupl({ list, all, title })
|
||||
{
|
||||
name: "NUPL",
|
||||
title: title("NUPL"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.relative.nupl,
|
||||
name,
|
||||
@@ -2146,7 +2146,7 @@ export function createGroupedProfitabilitySectionLongTerm({ list, all, title })
|
||||
{
|
||||
name: "NUPL",
|
||||
title: title("NUPL"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.relative.nupl,
|
||||
name,
|
||||
@@ -2159,7 +2159,7 @@ export function createGroupedProfitabilitySectionLongTerm({ list, all, title })
|
||||
name: "Peak Regret",
|
||||
title: title("Unrealized Peak Regret"),
|
||||
bottom: [
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.unrealized.peakRegret,
|
||||
name,
|
||||
@@ -2167,7 +2167,7 @@ export function createGroupedProfitabilitySectionLongTerm({ list, all, title })
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.relative.unrealizedPeakRegretRelToMarketCap,
|
||||
name,
|
||||
@@ -2207,7 +2207,7 @@ export function createGroupedProfitabilitySectionWithPeakRegret({
|
||||
{
|
||||
name: "NUPL",
|
||||
title: title("NUPL"),
|
||||
bottom: mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
bottom: mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.relative.nupl,
|
||||
name,
|
||||
@@ -2220,7 +2220,7 @@ export function createGroupedProfitabilitySectionWithPeakRegret({
|
||||
name: "Peak Regret",
|
||||
title: title("Unrealized Peak Regret"),
|
||||
bottom: [
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
line({
|
||||
metric: tree.unrealized.peakRegret,
|
||||
name,
|
||||
@@ -2228,7 +2228,7 @@ export function createGroupedProfitabilitySectionWithPeakRegret({
|
||||
unit: Unit.usd,
|
||||
}),
|
||||
),
|
||||
...mapCohortsWithAll(list, all, ({ color, name, tree }) =>
|
||||
...mapCohortsWithAll(list, all, ({ name, color, tree }) =>
|
||||
baseline({
|
||||
metric: tree.relative.unrealizedPeakRegretRelToMarketCap,
|
||||
name,
|
||||
|
||||
Reference in New Issue
Block a user