diff --git a/Cargo.lock b/Cargo.lock index d2c67baea..ff9544a44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -307,7 +307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aedd23ae0fd321affb4bbbc36126c6f49a32818dc6b979395d24da8c9d4e80ee" dependencies = [ "bitcoincore-rpc-json", - "jsonrpc 0.18.0", + "jsonrpc", "log", "serde", "serde_json", @@ -361,6 +361,42 @@ dependencies = [ "brk_types", ] +[[package]] +name = "brk-corepc-client" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c15bc86010adb0c3118d88a531a7d2633b726fe97a6c2888ce200946d198e7b" +dependencies = [ + "bitcoin", + "brk-corepc-jsonrpc", + "brk-corepc-types", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "brk-corepc-jsonrpc" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb72c73b4c3aafd6ab3d0a0ad07b2961543929452d7c5d8f11b88e7a1ca7725" +dependencies = [ + "base64 0.22.1", + "serde", + "serde_json", +] + +[[package]] +name = "brk-corepc-types" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca4662e4e22838c09a6f80d1677f6177295eddbb524515be9156a8f8412c4147" +dependencies = [ + "bitcoin", + "serde", + "serde_json", +] + [[package]] name = "brk_alloc" version = "0.1.9" @@ -484,7 +520,7 @@ version = "0.1.9" dependencies = [ "bitcoin", "bitcoincore-rpc", - "corepc-client", + "brk-corepc-client", "fjall", "jiff", "pco", @@ -623,11 +659,11 @@ version = "0.1.9" dependencies = [ "bitcoin", "bitcoincore-rpc", + "brk-corepc-client", + "brk-corepc-jsonrpc", "brk_error", "brk_logger", "brk_types", - "corepc-client", - "jsonrpc 0.19.0", "parking_lot", "serde", "serde_json", @@ -997,27 +1033,6 @@ dependencies = [ "libc", ] -[[package]] -name = "corepc-client" -version = "0.11.0" -dependencies = [ - "bitcoin", - "corepc-types", - "jsonrpc 0.19.0", - "log", - "serde", - "serde_json", -] - -[[package]] -name = "corepc-types" -version = "0.11.0" -dependencies = [ - "bitcoin", - "serde", - "serde_json", -] - [[package]] name = "crc32fast" version = "1.5.0" @@ -1971,15 +1986,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "jsonrpc" -version = "0.19.0" -dependencies = [ - "base64 0.22.1", - "serde", - "serde_json", -] - [[package]] name = "lazy_static" version = "1.5.0" diff --git a/Cargo.toml b/Cargo.toml index 95275b8f2..017e4b717 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,8 +65,8 @@ brk_types = { version = "0.1.9", path = "crates/brk_types" } brk_website = { version = "0.1.9", path = "crates/brk_website" } byteview = "0.10.1" color-eyre = "0.6.5" -corepc-client = { path = "/Users/k/Developer/corepc/client", features = ["client-sync"] } -corepc-jsonrpc = { package = "jsonrpc", path = "/Users/k/Developer/corepc/jsonrpc", features = ["simple_http"], default-features = false } +corepc-client = { package = "brk-corepc-client", version = "0.11.0", features = ["client-sync"] } +corepc-jsonrpc = { package = "brk-corepc-jsonrpc", version = "0.19.0", features = ["simple_http"], default-features = false } derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] } fjall = "3.1.2" indexmap = { version = "2.13.0", features = ["serde"] } diff --git a/crates/brk_computer/src/market/dca/compute.rs b/crates/brk_computer/src/market/dca/compute.rs index a97d675f4..c8cf5c5f1 100644 --- a/crates/brk_computer/src/market/dca/compute.rs +++ b/crates/brk_computer/src/market/dca/compute.rs @@ -60,6 +60,11 @@ impl Vecs { )?; } + // DCA by period - stack cents (sats × price) + for stack in self.period.stack.iter_mut() { + stack.compute(prices, starting_indexes.height, exit)?; + } + // DCA by period - average price (derived from stack) let starting_height = starting_indexes.height.to_usize(); for (average_price, stack, days) in @@ -146,6 +151,11 @@ impl Vecs { )?; } + // Lump sum by period - stack cents (sats × price) + for stack in self.period.lump_sum_stack.iter_mut() { + stack.compute(prices, starting_indexes.height, exit)?; + } + // Lump sum by period - returns (compute from lookback price) for (returns, (lookback_price, _)) in self .period @@ -213,6 +223,11 @@ impl Vecs { )?; } + // DCA by year class - stack cents (sats × price) + for stack in self.class.stack.iter_mut() { + stack.compute(prices, starting_indexes.height, exit)?; + } + // DCA by year class - average price (derived from stack) let start_days = super::ByDcaClass::<()>::start_days(); for ((average_price, stack), from) in self diff --git a/modules/brk-client/tests/consistency.js b/modules/brk-client/tests/consistency.js new file mode 100644 index 000000000..ff5fe6821 --- /dev/null +++ b/modules/brk-client/tests/consistency.js @@ -0,0 +1,132 @@ +/** + * Consistency test: verifies that all series sharing the same index have the same length. + * Useful for catching stale/inconsistent state after a reorg rollback. + */ + +import { BrkClient } from "../index.js"; + +/** + * @typedef {import('../index.js').AnySeriesPattern} AnyMetricPattern + */ + +/** + * @param {any} obj + * @returns {obj is AnyMetricPattern} + */ +function isMetricPattern(obj) { + return ( + obj && + typeof obj === "object" && + typeof obj.indexes === "function" && + obj.by && + typeof obj.by === "object" + ); +} + +/** + * Recursively collect all metric patterns from the tree. + * @param {Record} obj + * @param {string} path + * @returns {Array<{path: string, metric: AnyMetricPattern}>} + */ +function getAllMetrics(obj, path = "") { + /** @type {Array<{path: string, metric: AnyMetricPattern}>} */ + const metrics = []; + + for (const key of Object.keys(obj)) { + const attr = obj[key]; + if (!attr || typeof attr !== "object") continue; + + const currentPath = path ? `${path}.${key}` : key; + + if (isMetricPattern(attr)) { + metrics.push({ path: currentPath, metric: attr }); + } + + if (typeof attr === "object" && !Array.isArray(attr)) { + metrics.push(...getAllMetrics(attr, currentPath)); + } + } + + return metrics; +} + +async function testConsistency() { + const client = new BrkClient({ + baseUrl: "http://localhost:3110", + timeout: 15000, + }); + + const metrics = getAllMetrics(client.series); + console.log(`\nFound ${metrics.length} metrics`); + + /** @type {Map>} */ + const byIndex = new Map(); + + for (const { path, metric } of metrics) { + const indexes = metric.indexes(); + + for (const idxName of indexes) { + const fullPath = `${path}.by.${idxName}`; + const endpoint = metric.by[idxName]; + + if (!endpoint) { + console.log(`SKIP: ${fullPath} (undefined endpoint)`); + continue; + } + + try { + const result = await endpoint.last(0); + const total = result.total; + + if (!byIndex.has(idxName)) { + byIndex.set(idxName, []); + } + byIndex.get(idxName).push({ path: fullPath, total }); + } catch (e) { + console.log( + `FAIL: ${fullPath} -> ${e instanceof Error ? e.message : e}`, + ); + return; + } + } + } + + let failed = false; + + for (const [index, entries] of byIndex) { + const totals = new Set(entries.map((e) => e.total)); + + if (totals.size === 1) { + const [total] = totals; + console.log(`OK: ${index} — ${entries.length} series, all length ${total}`); + continue; + } + + failed = true; + console.log(`\nMISMATCH: ${index} — ${entries.length} series with ${totals.size} different lengths:`); + + /** @type {Map} */ + const grouped = new Map(); + for (const { path, total } of entries) { + if (!grouped.has(total)) grouped.set(total, []); + grouped.get(total).push(path); + } + + for (const [total, paths] of [...grouped].sort((a, b) => b[0] - a[0])) { + console.log(` length ${total}: (${paths.length} series)`); + for (const p of paths) { + console.log(` ${p}`); + } + } + } + + if (failed) { + console.log("\nFAILED: length mismatches detected"); + process.exit(1); + } else { + console.log("\nPASSED: all indexes consistent"); + } +} + +testConsistency();