mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-08-12 02:03:09 -07:00
global: massive columnar rework part 1
This commit is contained in:
Generated
+37
-36
@@ -184,9 +184,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.91"
|
||||
version = "0.1.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec"
|
||||
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -544,6 +544,7 @@ dependencies = [
|
||||
"brk_traversable",
|
||||
"brk_types",
|
||||
"rayon",
|
||||
"schemars",
|
||||
"serde",
|
||||
"vecdb",
|
||||
]
|
||||
@@ -963,9 +964,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.4.0"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9"
|
||||
checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@@ -1158,9 +1159,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.1"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||
checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
@@ -1630,9 +1631,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
@@ -2321,9 +2322,9 @@ checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.103"
|
||||
version = "0.3.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
||||
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
@@ -2796,9 +2797,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.14.0"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
|
||||
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
@@ -3142,9 +3143,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rmcp"
|
||||
version = "3.1.1"
|
||||
version = "3.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "094c075f6698deef5a657cf4df6b684dff65157d255978b92b552ec22503f17a"
|
||||
checksum = "c8dddc5b1924b9a59fba420166160ca2c4663a4e01803e52eda33070f56d63c8"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.23.1",
|
||||
@@ -3403,9 +3404,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_qs"
|
||||
version = "1.1.2"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67d525c8ff68aa99e5818302259bdd02d86d0303710616f39c0f44846ff6d332"
|
||||
checksum = "475b9310e272827d34113bf8679b6dfb7fa0023ee049764ba003f8f984e8b5d1"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"itoa",
|
||||
@@ -3711,18 +3712,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.19"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
|
||||
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.19"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
|
||||
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4051,11 +4052,11 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.3.0"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
|
||||
checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"base64 0.23.1",
|
||||
"cookie_store",
|
||||
"flate2",
|
||||
"log",
|
||||
@@ -4071,11 +4072,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ureq-proto"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
|
||||
checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"base64 0.23.1",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
@@ -4202,9 +4203,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.126"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -4215,9 +4216,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.126"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -4225,9 +4226,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.126"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
@@ -4238,18 +4239,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.126"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.103"
|
||||
version = "0.3.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
|
||||
checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ tokio = { version = "1.53.1", features = ["rt-multi-thread"] }
|
||||
tower-http = { version = "0.7.0", features = ["catch-panic", "compression-br", "compression-gzip", "compression-zstd", "cors", "normalize-path", "timeout", "trace"] }
|
||||
tower-layer = "0.3"
|
||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||
ureq = { version = "3.3.0", features = ["json"] }
|
||||
ureq = { version = "3.4.0", features = ["json"] }
|
||||
vecdb = { path = "crates/vecdb", version = "0.11.2", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
vecdb_derive = { path = "crates/vecdb_derive", version = "0.11.2" }
|
||||
|
||||
|
||||
+2240
-547
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ brk_types = { workspace = true }
|
||||
brk_traversable = { workspace = true }
|
||||
vecdb = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
|
||||
+224
-168
@@ -6,7 +6,7 @@ use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use serde::Serialize;
|
||||
use vecdb::{ColumnId, VecValue, Version};
|
||||
|
||||
use super::{CohortName, Filter, TimeFilter};
|
||||
use super::{CohortContext, CohortName, Filter, TimeFilter};
|
||||
|
||||
// Age boundary constants in hours
|
||||
pub const HOURS_1H: usize = 1;
|
||||
@@ -164,6 +164,120 @@ impl ColumnId for AgeRangeId {
|
||||
}
|
||||
}
|
||||
|
||||
impl AgeRangeId {
|
||||
#[inline]
|
||||
pub fn bounds(self) -> &'static Range<usize> {
|
||||
self.select(&AGE_RANGE_BOUNDS)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn filter(self) -> &'static Filter {
|
||||
self.select(&AGE_RANGE_FILTERS)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn name(self) -> &'static CohortName {
|
||||
self.select(&AGE_RANGE_NAMES)
|
||||
}
|
||||
|
||||
pub fn select<T>(self, values: &AgeRange<T>) -> &T {
|
||||
match self {
|
||||
Self::Under1H => &values.under_1h,
|
||||
Self::From1HTo1D => &values._1h_to_1d,
|
||||
Self::From1DTo1W => &values._1d_to_1w,
|
||||
Self::From1WTo1M => &values._1w_to_1m,
|
||||
Self::From1MTo2M => &values._1m_to_2m,
|
||||
Self::From2MTo3M => &values._2m_to_3m,
|
||||
Self::From3MTo4M => &values._3m_to_4m,
|
||||
Self::From4MTo5M => &values._4m_to_5m,
|
||||
Self::From5MTo6M => &values._5m_to_6m,
|
||||
Self::From6MTo9M => &values._6m_to_9m,
|
||||
Self::From9MTo1Y => &values._9m_to_1y,
|
||||
Self::From1YTo18M => &values._1y_to_18m,
|
||||
Self::From18MTo2Y => &values._18m_to_2y,
|
||||
Self::From2YTo3Y => &values._2y_to_3y,
|
||||
Self::From3YTo4Y => &values._3y_to_4y,
|
||||
Self::From4YTo5Y => &values._4y_to_5y,
|
||||
Self::From5YTo6Y => &values._5y_to_6y,
|
||||
Self::From6YTo7Y => &values._6y_to_7y,
|
||||
Self::From7YTo8Y => &values._7y_to_8y,
|
||||
Self::From8YTo10Y => &values._8y_to_10y,
|
||||
Self::From10YTo12Y => &values._10y_to_12y,
|
||||
Self::From12YTo15Y => &values._12y_to_15y,
|
||||
Self::Over15Y => &values.over_15y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_mut<T>(self, values: &mut AgeRange<T>) -> &mut T {
|
||||
match self {
|
||||
Self::Under1H => &mut values.under_1h,
|
||||
Self::From1HTo1D => &mut values._1h_to_1d,
|
||||
Self::From1DTo1W => &mut values._1d_to_1w,
|
||||
Self::From1WTo1M => &mut values._1w_to_1m,
|
||||
Self::From1MTo2M => &mut values._1m_to_2m,
|
||||
Self::From2MTo3M => &mut values._2m_to_3m,
|
||||
Self::From3MTo4M => &mut values._3m_to_4m,
|
||||
Self::From4MTo5M => &mut values._4m_to_5m,
|
||||
Self::From5MTo6M => &mut values._5m_to_6m,
|
||||
Self::From6MTo9M => &mut values._6m_to_9m,
|
||||
Self::From9MTo1Y => &mut values._9m_to_1y,
|
||||
Self::From1YTo18M => &mut values._1y_to_18m,
|
||||
Self::From18MTo2Y => &mut values._18m_to_2y,
|
||||
Self::From2YTo3Y => &mut values._2y_to_3y,
|
||||
Self::From3YTo4Y => &mut values._3y_to_4y,
|
||||
Self::From4YTo5Y => &mut values._4y_to_5y,
|
||||
Self::From5YTo6Y => &mut values._5y_to_6y,
|
||||
Self::From6YTo7Y => &mut values._6y_to_7y,
|
||||
Self::From7YTo8Y => &mut values._7y_to_8y,
|
||||
Self::From8YTo10Y => &mut values._8y_to_10y,
|
||||
Self::From10YTo12Y => &mut values._10y_to_12y,
|
||||
Self::From12YTo15Y => &mut values._12y_to_15y,
|
||||
Self::Over15Y => &mut values.over_15y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn series<T>(
|
||||
context: CohortContext,
|
||||
mut create: impl FnMut(Self, &str) -> T,
|
||||
) -> AgeRange<T> {
|
||||
AgeRange::from_fn(|column| {
|
||||
let name = context.prefixed(column.name().id);
|
||||
create(column, &name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Age> for AgeRangeId {
|
||||
#[inline]
|
||||
fn from(age: Age) -> Self {
|
||||
match age.hours() {
|
||||
0..HOURS_1H => Self::Under1H,
|
||||
HOURS_1H..HOURS_1D => Self::From1HTo1D,
|
||||
HOURS_1D..HOURS_1W => Self::From1DTo1W,
|
||||
HOURS_1W..HOURS_1M => Self::From1WTo1M,
|
||||
HOURS_1M..HOURS_2M => Self::From1MTo2M,
|
||||
HOURS_2M..HOURS_3M => Self::From2MTo3M,
|
||||
HOURS_3M..HOURS_4M => Self::From3MTo4M,
|
||||
HOURS_4M..HOURS_5M => Self::From4MTo5M,
|
||||
HOURS_5M..HOURS_6M => Self::From5MTo6M,
|
||||
HOURS_6M..HOURS_9M => Self::From6MTo9M,
|
||||
HOURS_9M..HOURS_1Y => Self::From9MTo1Y,
|
||||
HOURS_1Y..HOURS_18M => Self::From1YTo18M,
|
||||
HOURS_18M..HOURS_2Y => Self::From18MTo2Y,
|
||||
HOURS_2Y..HOURS_3Y => Self::From2YTo3Y,
|
||||
HOURS_3Y..HOURS_4Y => Self::From3YTo4Y,
|
||||
HOURS_4Y..HOURS_5Y => Self::From4YTo5Y,
|
||||
HOURS_5Y..HOURS_6Y => Self::From5YTo6Y,
|
||||
HOURS_6Y..HOURS_7Y => Self::From6YTo7Y,
|
||||
HOURS_7Y..HOURS_8Y => Self::From7YTo8Y,
|
||||
HOURS_8Y..HOURS_10Y => Self::From8YTo10Y,
|
||||
HOURS_10Y..HOURS_12Y => Self::From10YTo12Y,
|
||||
HOURS_12Y..HOURS_15Y => Self::From12YTo15Y,
|
||||
_ => Self::Over15Y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Age boundaries in hours. Defines the cohort ranges:
|
||||
/// [0, 1h), [1h, 1d), [1d, 1w), [1w, 1m), ..., [15y, ∞)
|
||||
pub const AGE_BOUNDARIES: [usize; AGE_RANGE_COUNT - 1] = [
|
||||
@@ -253,12 +367,6 @@ pub const AGE_RANGE_NAMES: AgeRange<CohortName> = AgeRange {
|
||||
over_15y: CohortName::new("over_15y_old", "15y+", "15+ Years Old"),
|
||||
};
|
||||
|
||||
impl AgeRange<CohortName> {
|
||||
pub const fn names() -> &'static Self {
|
||||
&AGE_RANGE_NAMES
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Traversable, Serialize)]
|
||||
pub struct AgeRange<T> {
|
||||
pub under_1h: T,
|
||||
@@ -287,183 +395,86 @@ pub struct AgeRange<T> {
|
||||
}
|
||||
|
||||
impl<T> AgeRange<T> {
|
||||
pub fn from_fn(mut create: impl FnMut(AgeRangeId) -> T) -> Self {
|
||||
Self {
|
||||
under_1h: create(AgeRangeId::Under1H),
|
||||
_1h_to_1d: create(AgeRangeId::From1HTo1D),
|
||||
_1d_to_1w: create(AgeRangeId::From1DTo1W),
|
||||
_1w_to_1m: create(AgeRangeId::From1WTo1M),
|
||||
_1m_to_2m: create(AgeRangeId::From1MTo2M),
|
||||
_2m_to_3m: create(AgeRangeId::From2MTo3M),
|
||||
_3m_to_4m: create(AgeRangeId::From3MTo4M),
|
||||
_4m_to_5m: create(AgeRangeId::From4MTo5M),
|
||||
_5m_to_6m: create(AgeRangeId::From5MTo6M),
|
||||
_6m_to_9m: create(AgeRangeId::From6MTo9M),
|
||||
_9m_to_1y: create(AgeRangeId::From9MTo1Y),
|
||||
_1y_to_18m: create(AgeRangeId::From1YTo18M),
|
||||
_18m_to_2y: create(AgeRangeId::From18MTo2Y),
|
||||
_2y_to_3y: create(AgeRangeId::From2YTo3Y),
|
||||
_3y_to_4y: create(AgeRangeId::From3YTo4Y),
|
||||
_4y_to_5y: create(AgeRangeId::From4YTo5Y),
|
||||
_5y_to_6y: create(AgeRangeId::From5YTo6Y),
|
||||
_6y_to_7y: create(AgeRangeId::From6YTo7Y),
|
||||
_7y_to_8y: create(AgeRangeId::From7YTo8Y),
|
||||
_8y_to_10y: create(AgeRangeId::From8YTo10Y),
|
||||
_10y_to_12y: create(AgeRangeId::From10YTo12Y),
|
||||
_12y_to_15y: create(AgeRangeId::From12YTo15Y),
|
||||
over_15y: create(AgeRangeId::Over15Y),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_fn<E>(mut create: impl FnMut(AgeRangeId) -> Result<T, E>) -> Result<Self, E> {
|
||||
Ok(Self {
|
||||
under_1h: create(AgeRangeId::Under1H)?,
|
||||
_1h_to_1d: create(AgeRangeId::From1HTo1D)?,
|
||||
_1d_to_1w: create(AgeRangeId::From1DTo1W)?,
|
||||
_1w_to_1m: create(AgeRangeId::From1WTo1M)?,
|
||||
_1m_to_2m: create(AgeRangeId::From1MTo2M)?,
|
||||
_2m_to_3m: create(AgeRangeId::From2MTo3M)?,
|
||||
_3m_to_4m: create(AgeRangeId::From3MTo4M)?,
|
||||
_4m_to_5m: create(AgeRangeId::From4MTo5M)?,
|
||||
_5m_to_6m: create(AgeRangeId::From5MTo6M)?,
|
||||
_6m_to_9m: create(AgeRangeId::From6MTo9M)?,
|
||||
_9m_to_1y: create(AgeRangeId::From9MTo1Y)?,
|
||||
_1y_to_18m: create(AgeRangeId::From1YTo18M)?,
|
||||
_18m_to_2y: create(AgeRangeId::From18MTo2Y)?,
|
||||
_2y_to_3y: create(AgeRangeId::From2YTo3Y)?,
|
||||
_3y_to_4y: create(AgeRangeId::From3YTo4Y)?,
|
||||
_4y_to_5y: create(AgeRangeId::From4YTo5Y)?,
|
||||
_5y_to_6y: create(AgeRangeId::From5YTo6Y)?,
|
||||
_6y_to_7y: create(AgeRangeId::From6YTo7Y)?,
|
||||
_7y_to_8y: create(AgeRangeId::From7YTo8Y)?,
|
||||
_8y_to_10y: create(AgeRangeId::From8YTo10Y)?,
|
||||
_10y_to_12y: create(AgeRangeId::From10YTo12Y)?,
|
||||
_12y_to_15y: create(AgeRangeId::From12YTo15Y)?,
|
||||
over_15y: create(AgeRangeId::Over15Y)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get mutable reference by Age. O(1).
|
||||
#[inline]
|
||||
pub fn get_mut(&mut self, age: Age) -> &mut T {
|
||||
match age.hours() {
|
||||
0..HOURS_1H => &mut self.under_1h,
|
||||
HOURS_1H..HOURS_1D => &mut self._1h_to_1d,
|
||||
HOURS_1D..HOURS_1W => &mut self._1d_to_1w,
|
||||
HOURS_1W..HOURS_1M => &mut self._1w_to_1m,
|
||||
HOURS_1M..HOURS_2M => &mut self._1m_to_2m,
|
||||
HOURS_2M..HOURS_3M => &mut self._2m_to_3m,
|
||||
HOURS_3M..HOURS_4M => &mut self._3m_to_4m,
|
||||
HOURS_4M..HOURS_5M => &mut self._4m_to_5m,
|
||||
HOURS_5M..HOURS_6M => &mut self._5m_to_6m,
|
||||
HOURS_6M..HOURS_9M => &mut self._6m_to_9m,
|
||||
HOURS_9M..HOURS_1Y => &mut self._9m_to_1y,
|
||||
HOURS_1Y..HOURS_18M => &mut self._1y_to_18m,
|
||||
HOURS_18M..HOURS_2Y => &mut self._18m_to_2y,
|
||||
HOURS_2Y..HOURS_3Y => &mut self._2y_to_3y,
|
||||
HOURS_3Y..HOURS_4Y => &mut self._3y_to_4y,
|
||||
HOURS_4Y..HOURS_5Y => &mut self._4y_to_5y,
|
||||
HOURS_5Y..HOURS_6Y => &mut self._5y_to_6y,
|
||||
HOURS_6Y..HOURS_7Y => &mut self._6y_to_7y,
|
||||
HOURS_7Y..HOURS_8Y => &mut self._7y_to_8y,
|
||||
HOURS_8Y..HOURS_10Y => &mut self._8y_to_10y,
|
||||
HOURS_10Y..HOURS_12Y => &mut self._10y_to_12y,
|
||||
HOURS_12Y..HOURS_15Y => &mut self._12y_to_15y,
|
||||
_ => &mut self.over_15y,
|
||||
}
|
||||
AgeRangeId::from(age).select_mut(self)
|
||||
}
|
||||
|
||||
/// Get reference by Age. O(1).
|
||||
#[inline]
|
||||
pub fn get(&self, age: Age) -> &T {
|
||||
match age.hours() {
|
||||
0..HOURS_1H => &self.under_1h,
|
||||
HOURS_1H..HOURS_1D => &self._1h_to_1d,
|
||||
HOURS_1D..HOURS_1W => &self._1d_to_1w,
|
||||
HOURS_1W..HOURS_1M => &self._1w_to_1m,
|
||||
HOURS_1M..HOURS_2M => &self._1m_to_2m,
|
||||
HOURS_2M..HOURS_3M => &self._2m_to_3m,
|
||||
HOURS_3M..HOURS_4M => &self._3m_to_4m,
|
||||
HOURS_4M..HOURS_5M => &self._4m_to_5m,
|
||||
HOURS_5M..HOURS_6M => &self._5m_to_6m,
|
||||
HOURS_6M..HOURS_9M => &self._6m_to_9m,
|
||||
HOURS_9M..HOURS_1Y => &self._9m_to_1y,
|
||||
HOURS_1Y..HOURS_18M => &self._1y_to_18m,
|
||||
HOURS_18M..HOURS_2Y => &self._18m_to_2y,
|
||||
HOURS_2Y..HOURS_3Y => &self._2y_to_3y,
|
||||
HOURS_3Y..HOURS_4Y => &self._3y_to_4y,
|
||||
HOURS_4Y..HOURS_5Y => &self._4y_to_5y,
|
||||
HOURS_5Y..HOURS_6Y => &self._5y_to_6y,
|
||||
HOURS_6Y..HOURS_7Y => &self._6y_to_7y,
|
||||
HOURS_7Y..HOURS_8Y => &self._7y_to_8y,
|
||||
HOURS_8Y..HOURS_10Y => &self._8y_to_10y,
|
||||
HOURS_10Y..HOURS_12Y => &self._10y_to_12y,
|
||||
HOURS_12Y..HOURS_15Y => &self._12y_to_15y,
|
||||
_ => &self.over_15y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_array(arr: [T; AGE_RANGE_COUNT]) -> Self {
|
||||
let [
|
||||
a0,
|
||||
a1,
|
||||
a2,
|
||||
a3,
|
||||
a4,
|
||||
a5,
|
||||
a6,
|
||||
a7,
|
||||
a8,
|
||||
a9,
|
||||
a10,
|
||||
a11,
|
||||
a12,
|
||||
a13,
|
||||
a14,
|
||||
a15,
|
||||
a16,
|
||||
a17,
|
||||
a18,
|
||||
a19,
|
||||
a20,
|
||||
a21,
|
||||
a22,
|
||||
] = arr;
|
||||
Self {
|
||||
under_1h: a0,
|
||||
_1h_to_1d: a1,
|
||||
_1d_to_1w: a2,
|
||||
_1w_to_1m: a3,
|
||||
_1m_to_2m: a4,
|
||||
_2m_to_3m: a5,
|
||||
_3m_to_4m: a6,
|
||||
_4m_to_5m: a7,
|
||||
_5m_to_6m: a8,
|
||||
_6m_to_9m: a9,
|
||||
_9m_to_1y: a10,
|
||||
_1y_to_18m: a11,
|
||||
_18m_to_2y: a12,
|
||||
_2y_to_3y: a13,
|
||||
_3y_to_4y: a14,
|
||||
_4y_to_5y: a15,
|
||||
_5y_to_6y: a16,
|
||||
_6y_to_7y: a17,
|
||||
_7y_to_8y: a18,
|
||||
_8y_to_10y: a19,
|
||||
_10y_to_12y: a20,
|
||||
_12y_to_15y: a21,
|
||||
over_15y: a22,
|
||||
}
|
||||
AgeRangeId::from(age).select(self)
|
||||
}
|
||||
|
||||
pub fn new<F>(mut create: F) -> Self
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> T,
|
||||
{
|
||||
let f = AGE_RANGE_FILTERS;
|
||||
let n = AGE_RANGE_NAMES;
|
||||
Self {
|
||||
under_1h: create(f.under_1h.clone(), n.under_1h.id),
|
||||
_1h_to_1d: create(f._1h_to_1d.clone(), n._1h_to_1d.id),
|
||||
_1d_to_1w: create(f._1d_to_1w.clone(), n._1d_to_1w.id),
|
||||
_1w_to_1m: create(f._1w_to_1m.clone(), n._1w_to_1m.id),
|
||||
_1m_to_2m: create(f._1m_to_2m.clone(), n._1m_to_2m.id),
|
||||
_2m_to_3m: create(f._2m_to_3m.clone(), n._2m_to_3m.id),
|
||||
_3m_to_4m: create(f._3m_to_4m.clone(), n._3m_to_4m.id),
|
||||
_4m_to_5m: create(f._4m_to_5m.clone(), n._4m_to_5m.id),
|
||||
_5m_to_6m: create(f._5m_to_6m.clone(), n._5m_to_6m.id),
|
||||
_6m_to_9m: create(f._6m_to_9m.clone(), n._6m_to_9m.id),
|
||||
_9m_to_1y: create(f._9m_to_1y.clone(), n._9m_to_1y.id),
|
||||
_1y_to_18m: create(f._1y_to_18m.clone(), n._1y_to_18m.id),
|
||||
_18m_to_2y: create(f._18m_to_2y.clone(), n._18m_to_2y.id),
|
||||
_2y_to_3y: create(f._2y_to_3y.clone(), n._2y_to_3y.id),
|
||||
_3y_to_4y: create(f._3y_to_4y.clone(), n._3y_to_4y.id),
|
||||
_4y_to_5y: create(f._4y_to_5y.clone(), n._4y_to_5y.id),
|
||||
_5y_to_6y: create(f._5y_to_6y.clone(), n._5y_to_6y.id),
|
||||
_6y_to_7y: create(f._6y_to_7y.clone(), n._6y_to_7y.id),
|
||||
_7y_to_8y: create(f._7y_to_8y.clone(), n._7y_to_8y.id),
|
||||
_8y_to_10y: create(f._8y_to_10y.clone(), n._8y_to_10y.id),
|
||||
_10y_to_12y: create(f._10y_to_12y.clone(), n._10y_to_12y.id),
|
||||
_12y_to_15y: create(f._12y_to_15y.clone(), n._12y_to_15y.id),
|
||||
over_15y: create(f.over_15y.clone(), n.over_15y.id),
|
||||
}
|
||||
Self::from_fn(|id| create(id.filter().clone(), id.name().id))
|
||||
}
|
||||
|
||||
pub fn try_new<F, E>(mut create: F) -> Result<Self, E>
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> Result<T, E>,
|
||||
{
|
||||
let f = AGE_RANGE_FILTERS;
|
||||
let n = AGE_RANGE_NAMES;
|
||||
Ok(Self {
|
||||
under_1h: create(f.under_1h.clone(), n.under_1h.id)?,
|
||||
_1h_to_1d: create(f._1h_to_1d.clone(), n._1h_to_1d.id)?,
|
||||
_1d_to_1w: create(f._1d_to_1w.clone(), n._1d_to_1w.id)?,
|
||||
_1w_to_1m: create(f._1w_to_1m.clone(), n._1w_to_1m.id)?,
|
||||
_1m_to_2m: create(f._1m_to_2m.clone(), n._1m_to_2m.id)?,
|
||||
_2m_to_3m: create(f._2m_to_3m.clone(), n._2m_to_3m.id)?,
|
||||
_3m_to_4m: create(f._3m_to_4m.clone(), n._3m_to_4m.id)?,
|
||||
_4m_to_5m: create(f._4m_to_5m.clone(), n._4m_to_5m.id)?,
|
||||
_5m_to_6m: create(f._5m_to_6m.clone(), n._5m_to_6m.id)?,
|
||||
_6m_to_9m: create(f._6m_to_9m.clone(), n._6m_to_9m.id)?,
|
||||
_9m_to_1y: create(f._9m_to_1y.clone(), n._9m_to_1y.id)?,
|
||||
_1y_to_18m: create(f._1y_to_18m.clone(), n._1y_to_18m.id)?,
|
||||
_18m_to_2y: create(f._18m_to_2y.clone(), n._18m_to_2y.id)?,
|
||||
_2y_to_3y: create(f._2y_to_3y.clone(), n._2y_to_3y.id)?,
|
||||
_3y_to_4y: create(f._3y_to_4y.clone(), n._3y_to_4y.id)?,
|
||||
_4y_to_5y: create(f._4y_to_5y.clone(), n._4y_to_5y.id)?,
|
||||
_5y_to_6y: create(f._5y_to_6y.clone(), n._5y_to_6y.id)?,
|
||||
_6y_to_7y: create(f._6y_to_7y.clone(), n._6y_to_7y.id)?,
|
||||
_7y_to_8y: create(f._7y_to_8y.clone(), n._7y_to_8y.id)?,
|
||||
_8y_to_10y: create(f._8y_to_10y.clone(), n._8y_to_10y.id)?,
|
||||
_10y_to_12y: create(f._10y_to_12y.clone(), n._10y_to_12y.id)?,
|
||||
_12y_to_15y: create(f._12y_to_15y.clone(), n._12y_to_15y.id)?,
|
||||
over_15y: create(f.over_15y.clone(), n.over_15y.id)?,
|
||||
})
|
||||
Self::try_from_fn(|id| create(id.filter().clone(), id.name().id))
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
@@ -559,6 +570,8 @@ impl<T> AgeRange<T> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use brk_types::Timestamp;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@@ -599,6 +612,29 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_ids_select_named_fields_and_metadata() {
|
||||
let mut named = AgeRange::from_fn(|column| column.index());
|
||||
for &column in &AGE_RANGE_IDS {
|
||||
assert_eq!(*column.select(&named), column.index());
|
||||
*column.select_mut(&mut named) += AGE_RANGE_COUNT;
|
||||
assert_eq!(*column.select(&named), column.index() + AGE_RANGE_COUNT);
|
||||
assert_eq!(column.bounds(), column.select(&AGE_RANGE_BOUNDS));
|
||||
assert_eq!(column.filter(), column.select(&AGE_RANGE_FILTERS));
|
||||
assert_eq!(column.name().id, column.select(&AGE_RANGE_NAMES).id);
|
||||
}
|
||||
|
||||
let series = AgeRangeId::series(CohortContext::Utxo, |column, name| {
|
||||
(column, name.to_owned())
|
||||
});
|
||||
for (((column, name), expected_column), expected_name) in
|
||||
series.iter().zip(AGE_RANGE_IDS).zip(AGE_RANGE_NAMES.iter())
|
||||
{
|
||||
assert_eq!(*column, expected_column);
|
||||
assert_eq!(name, &CohortContext::Utxo.prefixed(expected_name.id));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_range_names_and_boundaries_match() {
|
||||
assert_eq!(HOURS_9M, HOURS_6M + HOURS_3M);
|
||||
@@ -609,15 +645,35 @@ mod tests {
|
||||
assert_eq!(AGE_RANGE_NAMES._18m_to_2y.id, "18m_to_2y_old");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifier_matches_every_typed_range_boundary() {
|
||||
for &id in &AGE_RANGE_IDS {
|
||||
let bounds = id.bounds();
|
||||
let at_start = Age::new(
|
||||
Timestamp::new((bounds.start * 60 * 60) as u32),
|
||||
Timestamp::ZERO,
|
||||
);
|
||||
assert_eq!(AgeRangeId::from(at_start), id);
|
||||
|
||||
if bounds.end != usize::MAX {
|
||||
let before_end = Age::new(
|
||||
Timestamp::new(((bounds.end - 1) * 60 * 60) as u32),
|
||||
Timestamp::ZERO,
|
||||
);
|
||||
assert_eq!(AgeRangeId::from(before_end), id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eighteen_month_classifier_stays_anchored_to_one_year_plus_six_months() {
|
||||
let age_at_540_days = Age::new(
|
||||
brk_types::Timestamp::new((HOURS_6M * 3 * 60 * 60) as u32),
|
||||
brk_types::Timestamp::ZERO,
|
||||
Timestamp::new((HOURS_6M * 3 * 60 * 60) as u32),
|
||||
Timestamp::ZERO,
|
||||
);
|
||||
let age_at_18m = Age::new(
|
||||
brk_types::Timestamp::new((HOURS_18M * 60 * 60) as u32),
|
||||
brk_types::Timestamp::ZERO,
|
||||
Timestamp::new((HOURS_18M * 60 * 60) as u32),
|
||||
Timestamp::ZERO,
|
||||
);
|
||||
|
||||
assert_eq!(AGE_RANGE_NAMES.get(age_at_540_days).id, "1y_to_18m_old");
|
||||
|
||||
@@ -4,6 +4,7 @@ use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::OutputType;
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{ColumnId, VecValue, Version};
|
||||
|
||||
use super::Filter;
|
||||
|
||||
@@ -16,6 +17,116 @@ pub const P2WSH: &str = "p2wsh";
|
||||
pub const P2TR: &str = "p2tr";
|
||||
pub const P2A: &str = "p2a";
|
||||
|
||||
pub const ADDR_TYPE_COUNT: usize = 8;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[repr(u8)]
|
||||
pub enum AddrTypeId {
|
||||
P2PK65,
|
||||
P2PK33,
|
||||
P2PKH,
|
||||
P2SH,
|
||||
P2WPKH,
|
||||
P2WSH,
|
||||
P2TR,
|
||||
P2A,
|
||||
}
|
||||
|
||||
pub const ADDR_TYPE_IDS: [AddrTypeId; ADDR_TYPE_COUNT] = [
|
||||
AddrTypeId::P2PK65,
|
||||
AddrTypeId::P2PK33,
|
||||
AddrTypeId::P2PKH,
|
||||
AddrTypeId::P2SH,
|
||||
AddrTypeId::P2WPKH,
|
||||
AddrTypeId::P2WSH,
|
||||
AddrTypeId::P2TR,
|
||||
AddrTypeId::P2A,
|
||||
];
|
||||
|
||||
impl ColumnId for AddrTypeId {
|
||||
type Row<T>
|
||||
= [T; ADDR_TYPE_COUNT]
|
||||
where
|
||||
T: VecValue;
|
||||
|
||||
const VERSION: Version = Version::ONE;
|
||||
const ALL: &'static [Self] = &ADDR_TYPE_IDS;
|
||||
|
||||
#[inline]
|
||||
fn index(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
|
||||
&row[self.index()]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
|
||||
&mut row[self.index()]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn from_fn<T, F>(mut create: F) -> Self::Row<T>
|
||||
where
|
||||
T: VecValue,
|
||||
F: FnMut(Self) -> T,
|
||||
{
|
||||
std::array::from_fn(|index| create(ADDR_TYPE_IDS[index]))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn map<T, U, F>(row: Self::Row<T>, create: F) -> Self::Row<U>
|
||||
where
|
||||
T: VecValue,
|
||||
U: VecValue,
|
||||
F: FnMut(T) -> U,
|
||||
{
|
||||
row.map(create)
|
||||
}
|
||||
}
|
||||
|
||||
impl AddrTypeId {
|
||||
pub const fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::P2PK65 => P2PK65,
|
||||
Self::P2PK33 => P2PK33,
|
||||
Self::P2PKH => P2PKH,
|
||||
Self::P2SH => P2SH,
|
||||
Self::P2WPKH => P2WPKH,
|
||||
Self::P2WSH => P2WSH,
|
||||
Self::P2TR => P2TR,
|
||||
Self::P2A => P2A,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn output_type(self) -> OutputType {
|
||||
match self {
|
||||
Self::P2PK65 => OutputType::P2PK65,
|
||||
Self::P2PK33 => OutputType::P2PK33,
|
||||
Self::P2PKH => OutputType::P2PKH,
|
||||
Self::P2SH => OutputType::P2SH,
|
||||
Self::P2WPKH => OutputType::P2WPKH,
|
||||
Self::P2WSH => OutputType::P2WSH,
|
||||
Self::P2TR => OutputType::P2TR,
|
||||
Self::P2A => OutputType::P2A,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select<T>(self, values: &ByAddrType<T>) -> &T {
|
||||
values.get_unwrap(self.output_type())
|
||||
}
|
||||
|
||||
pub fn select_mut<T>(self, values: &mut ByAddrType<T>) -> &mut T {
|
||||
values.get_mut_unwrap(self.output_type())
|
||||
}
|
||||
|
||||
pub fn series<T>(mut create: impl FnMut(Self, &'static str) -> T) -> ByAddrType<T> {
|
||||
ByAddrType::from_fn(|id| create(id, id.name()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug, Traversable)]
|
||||
pub struct ByAddrType<T> {
|
||||
pub p2pk65: T,
|
||||
@@ -29,36 +140,44 @@ pub struct ByAddrType<T> {
|
||||
}
|
||||
|
||||
impl<T> ByAddrType<T> {
|
||||
pub fn from_fn(mut create: impl FnMut(AddrTypeId) -> T) -> Self {
|
||||
Self {
|
||||
p2pk65: create(AddrTypeId::P2PK65),
|
||||
p2pk33: create(AddrTypeId::P2PK33),
|
||||
p2pkh: create(AddrTypeId::P2PKH),
|
||||
p2sh: create(AddrTypeId::P2SH),
|
||||
p2wpkh: create(AddrTypeId::P2WPKH),
|
||||
p2wsh: create(AddrTypeId::P2WSH),
|
||||
p2tr: create(AddrTypeId::P2TR),
|
||||
p2a: create(AddrTypeId::P2A),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_fn<E>(mut create: impl FnMut(AddrTypeId) -> Result<T, E>) -> Result<Self, E> {
|
||||
Ok(Self {
|
||||
p2pk65: create(AddrTypeId::P2PK65)?,
|
||||
p2pk33: create(AddrTypeId::P2PK33)?,
|
||||
p2pkh: create(AddrTypeId::P2PKH)?,
|
||||
p2sh: create(AddrTypeId::P2SH)?,
|
||||
p2wpkh: create(AddrTypeId::P2WPKH)?,
|
||||
p2wsh: create(AddrTypeId::P2WSH)?,
|
||||
p2tr: create(AddrTypeId::P2TR)?,
|
||||
p2a: create(AddrTypeId::P2A)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new<F>(mut create: F) -> Self
|
||||
where
|
||||
F: FnMut(Filter) -> T,
|
||||
{
|
||||
Self {
|
||||
p2pk65: create(Filter::Type(OutputType::P2PK65)),
|
||||
p2pk33: create(Filter::Type(OutputType::P2PK33)),
|
||||
p2pkh: create(Filter::Type(OutputType::P2PKH)),
|
||||
p2sh: create(Filter::Type(OutputType::P2SH)),
|
||||
p2wpkh: create(Filter::Type(OutputType::P2WPKH)),
|
||||
p2wsh: create(Filter::Type(OutputType::P2WSH)),
|
||||
p2tr: create(Filter::Type(OutputType::P2TR)),
|
||||
p2a: create(Filter::Type(OutputType::P2A)),
|
||||
}
|
||||
Self::from_fn(|id| create(Filter::Type(id.output_type())))
|
||||
}
|
||||
|
||||
pub fn new_with_name<F>(f: F) -> Result<Self>
|
||||
where
|
||||
F: Fn(&'static str) -> Result<T>,
|
||||
{
|
||||
Ok(Self {
|
||||
p2pk65: f(P2PK65)?,
|
||||
p2pk33: f(P2PK33)?,
|
||||
p2pkh: f(P2PKH)?,
|
||||
p2sh: f(P2SH)?,
|
||||
p2wpkh: f(P2WPKH)?,
|
||||
p2wsh: f(P2WSH)?,
|
||||
p2tr: f(P2TR)?,
|
||||
p2a: f(P2A)?,
|
||||
})
|
||||
Self::try_from_fn(|id| f(id.name()))
|
||||
}
|
||||
|
||||
pub fn map_with_name<U>(&self, f: impl Fn(&'static str, &T) -> U) -> ByAddrType<U> {
|
||||
@@ -78,16 +197,7 @@ impl<T> ByAddrType<T> {
|
||||
where
|
||||
F: Fn(usize) -> Result<T>,
|
||||
{
|
||||
Ok(Self {
|
||||
p2pk65: f(0)?,
|
||||
p2pk33: f(1)?,
|
||||
p2pkh: f(2)?,
|
||||
p2sh: f(3)?,
|
||||
p2wpkh: f(4)?,
|
||||
p2wsh: f(5)?,
|
||||
p2tr: f(6)?,
|
||||
p2a: f(7)?,
|
||||
})
|
||||
Self::try_from_fn(|id| f(id as usize))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -286,40 +396,26 @@ impl<T> ByAddrType<Option<T>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Zip one ByAddrType with a function, producing a new ByAddrType.
|
||||
pub fn zip_by_addr_type<S, R, F>(source: &ByAddrType<S>, f: F) -> Result<ByAddrType<R>>
|
||||
where
|
||||
F: Fn(&'static str, &S) -> Result<R>,
|
||||
{
|
||||
Ok(ByAddrType {
|
||||
p2pk65: f(P2PK65, &source.p2pk65)?,
|
||||
p2pk33: f(P2PK33, &source.p2pk33)?,
|
||||
p2pkh: f(P2PKH, &source.p2pkh)?,
|
||||
p2sh: f(P2SH, &source.p2sh)?,
|
||||
p2wpkh: f(P2WPKH, &source.p2wpkh)?,
|
||||
p2wsh: f(P2WSH, &source.p2wsh)?,
|
||||
p2tr: f(P2TR, &source.p2tr)?,
|
||||
p2a: f(P2A, &source.p2a)?,
|
||||
})
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use vecdb::ColumnId;
|
||||
|
||||
/// Zip two ByAddrTypes with a function, producing a new ByAddrType.
|
||||
pub fn zip2_by_addr_type<S1, S2, R, F>(
|
||||
a: &ByAddrType<S1>,
|
||||
b: &ByAddrType<S2>,
|
||||
f: F,
|
||||
) -> Result<ByAddrType<R>>
|
||||
where
|
||||
F: Fn(&'static str, &S1, &S2) -> Result<R>,
|
||||
{
|
||||
Ok(ByAddrType {
|
||||
p2pk65: f(P2PK65, &a.p2pk65, &b.p2pk65)?,
|
||||
p2pk33: f(P2PK33, &a.p2pk33, &b.p2pk33)?,
|
||||
p2pkh: f(P2PKH, &a.p2pkh, &b.p2pkh)?,
|
||||
p2sh: f(P2SH, &a.p2sh, &b.p2sh)?,
|
||||
p2wpkh: f(P2WPKH, &a.p2wpkh, &b.p2wpkh)?,
|
||||
p2wsh: f(P2WSH, &a.p2wsh, &b.p2wsh)?,
|
||||
p2tr: f(P2TR, &a.p2tr, &b.p2tr)?,
|
||||
p2a: f(P2A, &a.p2a, &b.p2a)?,
|
||||
})
|
||||
use super::{ADDR_TYPE_IDS, AddrTypeId};
|
||||
|
||||
#[test]
|
||||
fn column_order_matches_named_series() {
|
||||
assert_eq!(AddrTypeId::ALL, ADDR_TYPE_IDS);
|
||||
|
||||
let series = AddrTypeId::series(|column, _| column);
|
||||
assert!(series.values().copied().eq(ADDR_TYPE_IDS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_order_matches_column_indexes() {
|
||||
let row = AddrTypeId::from_fn(|column| column.index());
|
||||
|
||||
for column in ADDR_TYPE_IDS {
|
||||
assert_eq!(*column.get(&row), column.index());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,124 @@ use std::ops::{Add, AddAssign};
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::OutputType;
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{ColumnId, VecValue, Version};
|
||||
|
||||
use super::{Filter, SpendableType, UnspendableType};
|
||||
|
||||
pub const OP_RETURN: &str = "op_return";
|
||||
pub const OUTPUT_TYPE_COUNT: usize = OutputType::COUNT;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[repr(u8)]
|
||||
pub enum OutputTypeId {
|
||||
P2PK65,
|
||||
P2PK33,
|
||||
P2PKH,
|
||||
P2MS,
|
||||
P2SH,
|
||||
P2WPKH,
|
||||
P2WSH,
|
||||
P2TR,
|
||||
P2A,
|
||||
Unknown,
|
||||
Empty,
|
||||
OpReturn,
|
||||
}
|
||||
|
||||
pub const OUTPUT_TYPE_IDS: [OutputTypeId; OUTPUT_TYPE_COUNT] = [
|
||||
OutputTypeId::P2PK65,
|
||||
OutputTypeId::P2PK33,
|
||||
OutputTypeId::P2PKH,
|
||||
OutputTypeId::P2MS,
|
||||
OutputTypeId::P2SH,
|
||||
OutputTypeId::P2WPKH,
|
||||
OutputTypeId::P2WSH,
|
||||
OutputTypeId::P2TR,
|
||||
OutputTypeId::P2A,
|
||||
OutputTypeId::Unknown,
|
||||
OutputTypeId::Empty,
|
||||
OutputTypeId::OpReturn,
|
||||
];
|
||||
|
||||
impl OutputTypeId {
|
||||
pub const fn from_output_type(value: OutputType) -> Self {
|
||||
match value {
|
||||
OutputType::P2PK65 => Self::P2PK65,
|
||||
OutputType::P2PK33 => Self::P2PK33,
|
||||
OutputType::P2PKH => Self::P2PKH,
|
||||
OutputType::P2MS => Self::P2MS,
|
||||
OutputType::P2SH => Self::P2SH,
|
||||
OutputType::P2WPKH => Self::P2WPKH,
|
||||
OutputType::P2WSH => Self::P2WSH,
|
||||
OutputType::P2TR => Self::P2TR,
|
||||
OutputType::P2A => Self::P2A,
|
||||
OutputType::Unknown => Self::Unknown,
|
||||
OutputType::Empty => Self::Empty,
|
||||
OutputType::OpReturn => Self::OpReturn,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn output_type(self) -> OutputType {
|
||||
match self {
|
||||
Self::P2PK65 => OutputType::P2PK65,
|
||||
Self::P2PK33 => OutputType::P2PK33,
|
||||
Self::P2PKH => OutputType::P2PKH,
|
||||
Self::P2MS => OutputType::P2MS,
|
||||
Self::P2SH => OutputType::P2SH,
|
||||
Self::P2WPKH => OutputType::P2WPKH,
|
||||
Self::P2WSH => OutputType::P2WSH,
|
||||
Self::P2TR => OutputType::P2TR,
|
||||
Self::P2A => OutputType::P2A,
|
||||
Self::Unknown => OutputType::Unknown,
|
||||
Self::Empty => OutputType::Empty,
|
||||
Self::OpReturn => OutputType::OpReturn,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColumnId for OutputTypeId {
|
||||
type Row<T>
|
||||
= [T; OUTPUT_TYPE_COUNT]
|
||||
where
|
||||
T: VecValue;
|
||||
|
||||
const VERSION: Version = Version::ONE;
|
||||
const ALL: &'static [Self] = &OUTPUT_TYPE_IDS;
|
||||
|
||||
#[inline]
|
||||
fn index(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
|
||||
&row[self as usize]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
|
||||
&mut row[self as usize]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn from_fn<T, F>(mut f: F) -> Self::Row<T>
|
||||
where
|
||||
T: VecValue,
|
||||
F: FnMut(Self) -> T,
|
||||
{
|
||||
std::array::from_fn(|index| f(OUTPUT_TYPE_IDS[index]))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn map<T, U, F>(row: Self::Row<T>, f: F) -> Self::Row<U>
|
||||
where
|
||||
T: VecValue,
|
||||
U: VecValue,
|
||||
F: FnMut(T) -> U,
|
||||
{
|
||||
row.map(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug, Traversable)]
|
||||
pub struct ByType<T> {
|
||||
@@ -137,3 +251,30 @@ where
|
||||
self.unspendable += rhs.unspendable;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn column_ids_match_by_type_order() {
|
||||
let by_type = ByType::new(|filter, _| {
|
||||
let Filter::Type(output_type) = filter else {
|
||||
unreachable!()
|
||||
};
|
||||
output_type
|
||||
});
|
||||
let output_types: Vec<_> = by_type.iter().copied().collect();
|
||||
let column_output_types: Vec<_> = OutputTypeId::ALL
|
||||
.iter()
|
||||
.map(|column| column.output_type())
|
||||
.collect();
|
||||
|
||||
assert_eq!(column_output_types, output_types);
|
||||
|
||||
let row = OutputTypeId::from_fn(|column| column.index());
|
||||
for column in OutputTypeId::ALL {
|
||||
assert_eq!(*column.get(&row), column.index());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ mod loss;
|
||||
mod over_age;
|
||||
mod over_amount;
|
||||
mod profit;
|
||||
mod profitability;
|
||||
mod profitability_range;
|
||||
mod spendable_type;
|
||||
mod state_level;
|
||||
@@ -49,6 +50,7 @@ pub use loss::*;
|
||||
pub use over_age::*;
|
||||
pub use over_amount::*;
|
||||
pub use profit::*;
|
||||
pub use profitability::*;
|
||||
pub use profitability_range::*;
|
||||
pub use spendable_type::*;
|
||||
pub use state_level::*;
|
||||
|
||||
@@ -0,0 +1,479 @@
|
||||
use std::{fmt, ops::AddAssign};
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
use vecdb::{ColumnId, Formattable, VecValue, Version};
|
||||
|
||||
use crate::{
|
||||
LOSS_COUNT, Loss, PROFIT_COUNT, PROFITABILITY_RANGE_COUNT, Profit, ProfitabilityRange,
|
||||
};
|
||||
|
||||
pub const PROFITABILITY_COUNT: usize = PROFITABILITY_RANGE_COUNT + PROFIT_COUNT + LOSS_COUNT;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
||||
pub struct ProfitabilityRow<T> {
|
||||
pub range: [T; PROFITABILITY_RANGE_COUNT],
|
||||
pub profit: [T; PROFIT_COUNT],
|
||||
pub loss: [T; LOSS_COUNT],
|
||||
}
|
||||
|
||||
impl<T> ProfitabilityRow<T>
|
||||
where
|
||||
T: AddAssign + Copy + Default,
|
||||
{
|
||||
pub fn from_ranges(range: [T; PROFITABILITY_RANGE_COUNT]) -> Self {
|
||||
let (profit_ranges, loss_ranges) = range.split_at(PROFIT_COUNT + 1);
|
||||
|
||||
let mut profit = [T::default(); PROFIT_COUNT];
|
||||
let mut total = profit_ranges[0];
|
||||
for (threshold, &value) in profit.iter_mut().rev().zip(&profit_ranges[1..]) {
|
||||
total += value;
|
||||
*threshold = total;
|
||||
}
|
||||
|
||||
let mut loss = [T::default(); LOSS_COUNT];
|
||||
let mut total = loss_ranges[loss_ranges.len() - 1];
|
||||
for (threshold, &value) in loss
|
||||
.iter_mut()
|
||||
.rev()
|
||||
.zip(loss_ranges[..loss_ranges.len() - 1].iter().rev())
|
||||
{
|
||||
total += value;
|
||||
*threshold = total;
|
||||
}
|
||||
|
||||
Self {
|
||||
range,
|
||||
profit,
|
||||
loss,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Formattable> Formattable for ProfitabilityRow<T> {
|
||||
fn write_to(&self, buf: &mut Vec<u8>) {
|
||||
buf.extend_from_slice(b"{\"range\":");
|
||||
self.range.write_to(buf);
|
||||
buf.extend_from_slice(b",\"profit\":");
|
||||
self.profit.write_to(buf);
|
||||
buf.extend_from_slice(b",\"loss\":");
|
||||
self.loss.write_to(buf);
|
||||
buf.push(b'}');
|
||||
}
|
||||
|
||||
fn fmt_csv(&self, output: &mut String) -> fmt::Result {
|
||||
let mut json = Vec::new();
|
||||
self.write_to(&mut json);
|
||||
let json = std::str::from_utf8(&json).map_err(|_| fmt::Error)?;
|
||||
|
||||
output.push('"');
|
||||
for character in json.chars() {
|
||||
if character == '"' {
|
||||
output.push('"');
|
||||
}
|
||||
output.push(character);
|
||||
}
|
||||
output.push('"');
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Every profitability range and aggregate threshold in column storage order.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[repr(u8)]
|
||||
pub enum ProfitabilityId {
|
||||
RangeOver1000PctInProfit,
|
||||
Range500To1000PctInProfit,
|
||||
Range300To500PctInProfit,
|
||||
Range200To300PctInProfit,
|
||||
Range100To200PctInProfit,
|
||||
Range90To100PctInProfit,
|
||||
Range80To90PctInProfit,
|
||||
Range70To80PctInProfit,
|
||||
Range60To70PctInProfit,
|
||||
Range50To60PctInProfit,
|
||||
Range40To50PctInProfit,
|
||||
Range30To40PctInProfit,
|
||||
Range20To30PctInProfit,
|
||||
Range10To20PctInProfit,
|
||||
Range0To10PctInProfit,
|
||||
Range0To10PctInLoss,
|
||||
Range10To20PctInLoss,
|
||||
Range20To30PctInLoss,
|
||||
Range30To40PctInLoss,
|
||||
Range40To50PctInLoss,
|
||||
Range50To60PctInLoss,
|
||||
Range60To70PctInLoss,
|
||||
Range70To80PctInLoss,
|
||||
Range80To90PctInLoss,
|
||||
Range90To100PctInLoss,
|
||||
Profit,
|
||||
ProfitOver10Pct,
|
||||
ProfitOver20Pct,
|
||||
ProfitOver30Pct,
|
||||
ProfitOver40Pct,
|
||||
ProfitOver50Pct,
|
||||
ProfitOver60Pct,
|
||||
ProfitOver70Pct,
|
||||
ProfitOver80Pct,
|
||||
ProfitOver90Pct,
|
||||
ProfitOver100Pct,
|
||||
ProfitOver200Pct,
|
||||
ProfitOver300Pct,
|
||||
ProfitOver500Pct,
|
||||
Loss,
|
||||
LossOver10Pct,
|
||||
LossOver20Pct,
|
||||
LossOver30Pct,
|
||||
LossOver40Pct,
|
||||
LossOver50Pct,
|
||||
LossOver60Pct,
|
||||
LossOver70Pct,
|
||||
LossOver80Pct,
|
||||
}
|
||||
|
||||
pub const PROFITABILITY_IDS: [ProfitabilityId; PROFITABILITY_COUNT] = [
|
||||
ProfitabilityId::RangeOver1000PctInProfit,
|
||||
ProfitabilityId::Range500To1000PctInProfit,
|
||||
ProfitabilityId::Range300To500PctInProfit,
|
||||
ProfitabilityId::Range200To300PctInProfit,
|
||||
ProfitabilityId::Range100To200PctInProfit,
|
||||
ProfitabilityId::Range90To100PctInProfit,
|
||||
ProfitabilityId::Range80To90PctInProfit,
|
||||
ProfitabilityId::Range70To80PctInProfit,
|
||||
ProfitabilityId::Range60To70PctInProfit,
|
||||
ProfitabilityId::Range50To60PctInProfit,
|
||||
ProfitabilityId::Range40To50PctInProfit,
|
||||
ProfitabilityId::Range30To40PctInProfit,
|
||||
ProfitabilityId::Range20To30PctInProfit,
|
||||
ProfitabilityId::Range10To20PctInProfit,
|
||||
ProfitabilityId::Range0To10PctInProfit,
|
||||
ProfitabilityId::Range0To10PctInLoss,
|
||||
ProfitabilityId::Range10To20PctInLoss,
|
||||
ProfitabilityId::Range20To30PctInLoss,
|
||||
ProfitabilityId::Range30To40PctInLoss,
|
||||
ProfitabilityId::Range40To50PctInLoss,
|
||||
ProfitabilityId::Range50To60PctInLoss,
|
||||
ProfitabilityId::Range60To70PctInLoss,
|
||||
ProfitabilityId::Range70To80PctInLoss,
|
||||
ProfitabilityId::Range80To90PctInLoss,
|
||||
ProfitabilityId::Range90To100PctInLoss,
|
||||
ProfitabilityId::Profit,
|
||||
ProfitabilityId::ProfitOver10Pct,
|
||||
ProfitabilityId::ProfitOver20Pct,
|
||||
ProfitabilityId::ProfitOver30Pct,
|
||||
ProfitabilityId::ProfitOver40Pct,
|
||||
ProfitabilityId::ProfitOver50Pct,
|
||||
ProfitabilityId::ProfitOver60Pct,
|
||||
ProfitabilityId::ProfitOver70Pct,
|
||||
ProfitabilityId::ProfitOver80Pct,
|
||||
ProfitabilityId::ProfitOver90Pct,
|
||||
ProfitabilityId::ProfitOver100Pct,
|
||||
ProfitabilityId::ProfitOver200Pct,
|
||||
ProfitabilityId::ProfitOver300Pct,
|
||||
ProfitabilityId::ProfitOver500Pct,
|
||||
ProfitabilityId::Loss,
|
||||
ProfitabilityId::LossOver10Pct,
|
||||
ProfitabilityId::LossOver20Pct,
|
||||
ProfitabilityId::LossOver30Pct,
|
||||
ProfitabilityId::LossOver40Pct,
|
||||
ProfitabilityId::LossOver50Pct,
|
||||
ProfitabilityId::LossOver60Pct,
|
||||
ProfitabilityId::LossOver70Pct,
|
||||
ProfitabilityId::LossOver80Pct,
|
||||
];
|
||||
|
||||
impl ProfitabilityId {
|
||||
pub fn range_ids() -> &'static [Self] {
|
||||
&PROFITABILITY_IDS[..PROFITABILITY_RANGE_COUNT]
|
||||
}
|
||||
|
||||
pub fn profit_ids() -> &'static [Self] {
|
||||
&PROFITABILITY_IDS[PROFITABILITY_RANGE_COUNT..PROFITABILITY_RANGE_COUNT + PROFIT_COUNT]
|
||||
}
|
||||
|
||||
pub fn loss_ids() -> &'static [Self] {
|
||||
&PROFITABILITY_IDS[PROFITABILITY_RANGE_COUNT + PROFIT_COUNT..]
|
||||
}
|
||||
|
||||
pub fn range_series<T>(
|
||||
mut create: impl FnMut(Self, &'static str) -> T,
|
||||
) -> ProfitabilityRange<T> {
|
||||
let names = ProfitabilityRange::names();
|
||||
ProfitabilityRange {
|
||||
over_1000pct_in_profit: create(
|
||||
Self::RangeOver1000PctInProfit,
|
||||
names.over_1000pct_in_profit.id,
|
||||
),
|
||||
_500pct_to_1000pct_in_profit: create(
|
||||
Self::Range500To1000PctInProfit,
|
||||
names._500pct_to_1000pct_in_profit.id,
|
||||
),
|
||||
_300pct_to_500pct_in_profit: create(
|
||||
Self::Range300To500PctInProfit,
|
||||
names._300pct_to_500pct_in_profit.id,
|
||||
),
|
||||
_200pct_to_300pct_in_profit: create(
|
||||
Self::Range200To300PctInProfit,
|
||||
names._200pct_to_300pct_in_profit.id,
|
||||
),
|
||||
_100pct_to_200pct_in_profit: create(
|
||||
Self::Range100To200PctInProfit,
|
||||
names._100pct_to_200pct_in_profit.id,
|
||||
),
|
||||
_90pct_to_100pct_in_profit: create(
|
||||
Self::Range90To100PctInProfit,
|
||||
names._90pct_to_100pct_in_profit.id,
|
||||
),
|
||||
_80pct_to_90pct_in_profit: create(
|
||||
Self::Range80To90PctInProfit,
|
||||
names._80pct_to_90pct_in_profit.id,
|
||||
),
|
||||
_70pct_to_80pct_in_profit: create(
|
||||
Self::Range70To80PctInProfit,
|
||||
names._70pct_to_80pct_in_profit.id,
|
||||
),
|
||||
_60pct_to_70pct_in_profit: create(
|
||||
Self::Range60To70PctInProfit,
|
||||
names._60pct_to_70pct_in_profit.id,
|
||||
),
|
||||
_50pct_to_60pct_in_profit: create(
|
||||
Self::Range50To60PctInProfit,
|
||||
names._50pct_to_60pct_in_profit.id,
|
||||
),
|
||||
_40pct_to_50pct_in_profit: create(
|
||||
Self::Range40To50PctInProfit,
|
||||
names._40pct_to_50pct_in_profit.id,
|
||||
),
|
||||
_30pct_to_40pct_in_profit: create(
|
||||
Self::Range30To40PctInProfit,
|
||||
names._30pct_to_40pct_in_profit.id,
|
||||
),
|
||||
_20pct_to_30pct_in_profit: create(
|
||||
Self::Range20To30PctInProfit,
|
||||
names._20pct_to_30pct_in_profit.id,
|
||||
),
|
||||
_10pct_to_20pct_in_profit: create(
|
||||
Self::Range10To20PctInProfit,
|
||||
names._10pct_to_20pct_in_profit.id,
|
||||
),
|
||||
_0pct_to_10pct_in_profit: create(
|
||||
Self::Range0To10PctInProfit,
|
||||
names._0pct_to_10pct_in_profit.id,
|
||||
),
|
||||
_0pct_to_10pct_in_loss: create(
|
||||
Self::Range0To10PctInLoss,
|
||||
names._0pct_to_10pct_in_loss.id,
|
||||
),
|
||||
_10pct_to_20pct_in_loss: create(
|
||||
Self::Range10To20PctInLoss,
|
||||
names._10pct_to_20pct_in_loss.id,
|
||||
),
|
||||
_20pct_to_30pct_in_loss: create(
|
||||
Self::Range20To30PctInLoss,
|
||||
names._20pct_to_30pct_in_loss.id,
|
||||
),
|
||||
_30pct_to_40pct_in_loss: create(
|
||||
Self::Range30To40PctInLoss,
|
||||
names._30pct_to_40pct_in_loss.id,
|
||||
),
|
||||
_40pct_to_50pct_in_loss: create(
|
||||
Self::Range40To50PctInLoss,
|
||||
names._40pct_to_50pct_in_loss.id,
|
||||
),
|
||||
_50pct_to_60pct_in_loss: create(
|
||||
Self::Range50To60PctInLoss,
|
||||
names._50pct_to_60pct_in_loss.id,
|
||||
),
|
||||
_60pct_to_70pct_in_loss: create(
|
||||
Self::Range60To70PctInLoss,
|
||||
names._60pct_to_70pct_in_loss.id,
|
||||
),
|
||||
_70pct_to_80pct_in_loss: create(
|
||||
Self::Range70To80PctInLoss,
|
||||
names._70pct_to_80pct_in_loss.id,
|
||||
),
|
||||
_80pct_to_90pct_in_loss: create(
|
||||
Self::Range80To90PctInLoss,
|
||||
names._80pct_to_90pct_in_loss.id,
|
||||
),
|
||||
_90pct_to_100pct_in_loss: create(
|
||||
Self::Range90To100PctInLoss,
|
||||
names._90pct_to_100pct_in_loss.id,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn profit_series<T>(mut create: impl FnMut(Self, &'static str) -> T) -> Profit<T> {
|
||||
let names = Profit::names();
|
||||
Profit {
|
||||
all: create(Self::Profit, names.all.id),
|
||||
_10pct: create(Self::ProfitOver10Pct, names._10pct.id),
|
||||
_20pct: create(Self::ProfitOver20Pct, names._20pct.id),
|
||||
_30pct: create(Self::ProfitOver30Pct, names._30pct.id),
|
||||
_40pct: create(Self::ProfitOver40Pct, names._40pct.id),
|
||||
_50pct: create(Self::ProfitOver50Pct, names._50pct.id),
|
||||
_60pct: create(Self::ProfitOver60Pct, names._60pct.id),
|
||||
_70pct: create(Self::ProfitOver70Pct, names._70pct.id),
|
||||
_80pct: create(Self::ProfitOver80Pct, names._80pct.id),
|
||||
_90pct: create(Self::ProfitOver90Pct, names._90pct.id),
|
||||
_100pct: create(Self::ProfitOver100Pct, names._100pct.id),
|
||||
_200pct: create(Self::ProfitOver200Pct, names._200pct.id),
|
||||
_300pct: create(Self::ProfitOver300Pct, names._300pct.id),
|
||||
_500pct: create(Self::ProfitOver500Pct, names._500pct.id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn loss_series<T>(mut create: impl FnMut(Self, &'static str) -> T) -> Loss<T> {
|
||||
let names = Loss::names();
|
||||
Loss {
|
||||
all: create(Self::Loss, names.all.id),
|
||||
_10pct: create(Self::LossOver10Pct, names._10pct.id),
|
||||
_20pct: create(Self::LossOver20Pct, names._20pct.id),
|
||||
_30pct: create(Self::LossOver30Pct, names._30pct.id),
|
||||
_40pct: create(Self::LossOver40Pct, names._40pct.id),
|
||||
_50pct: create(Self::LossOver50Pct, names._50pct.id),
|
||||
_60pct: create(Self::LossOver60Pct, names._60pct.id),
|
||||
_70pct: create(Self::LossOver70Pct, names._70pct.id),
|
||||
_80pct: create(Self::LossOver80Pct, names._80pct.id),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn is_profit(self) -> bool {
|
||||
let index = self as usize;
|
||||
index < PROFIT_COUNT + 1
|
||||
|| (index >= PROFITABILITY_RANGE_COUNT
|
||||
&& index < PROFITABILITY_RANGE_COUNT + PROFIT_COUNT)
|
||||
}
|
||||
}
|
||||
|
||||
impl ColumnId for ProfitabilityId {
|
||||
type Row<T>
|
||||
= ProfitabilityRow<T>
|
||||
where
|
||||
T: VecValue;
|
||||
|
||||
const VERSION: Version = Version::ONE;
|
||||
const ALL: &'static [Self] = &PROFITABILITY_IDS;
|
||||
|
||||
#[inline]
|
||||
fn index(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
|
||||
let index = self as usize;
|
||||
if index < PROFITABILITY_RANGE_COUNT {
|
||||
&row.range[index]
|
||||
} else if index < PROFITABILITY_RANGE_COUNT + PROFIT_COUNT {
|
||||
&row.profit[index - PROFITABILITY_RANGE_COUNT]
|
||||
} else {
|
||||
&row.loss[index - PROFITABILITY_RANGE_COUNT - PROFIT_COUNT]
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
|
||||
let index = self as usize;
|
||||
if index < PROFITABILITY_RANGE_COUNT {
|
||||
&mut row.range[index]
|
||||
} else if index < PROFITABILITY_RANGE_COUNT + PROFIT_COUNT {
|
||||
&mut row.profit[index - PROFITABILITY_RANGE_COUNT]
|
||||
} else {
|
||||
&mut row.loss[index - PROFITABILITY_RANGE_COUNT - PROFIT_COUNT]
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn from_fn<T, F>(mut f: F) -> Self::Row<T>
|
||||
where
|
||||
T: VecValue,
|
||||
F: FnMut(Self) -> T,
|
||||
{
|
||||
ProfitabilityRow {
|
||||
range: std::array::from_fn(|index| f(PROFITABILITY_IDS[index])),
|
||||
profit: std::array::from_fn(|index| {
|
||||
f(PROFITABILITY_IDS[PROFITABILITY_RANGE_COUNT + index])
|
||||
}),
|
||||
loss: std::array::from_fn(|index| {
|
||||
f(PROFITABILITY_IDS[PROFITABILITY_RANGE_COUNT + PROFIT_COUNT + index])
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn map<T, U, F>(row: Self::Row<T>, mut f: F) -> Self::Row<U>
|
||||
where
|
||||
T: VecValue,
|
||||
U: VecValue,
|
||||
F: FnMut(T) -> U,
|
||||
{
|
||||
ProfitabilityRow {
|
||||
range: row.range.map(&mut f),
|
||||
profit: row.profit.map(&mut f),
|
||||
loss: row.loss.map(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn profitability_ids_cover_the_schema_in_storage_order() {
|
||||
assert_eq!(
|
||||
ProfitabilityId::range_ids().len(),
|
||||
PROFITABILITY_RANGE_COUNT
|
||||
);
|
||||
assert_eq!(ProfitabilityId::profit_ids().len(), PROFIT_COUNT);
|
||||
assert_eq!(ProfitabilityId::loss_ids().len(), LOSS_COUNT);
|
||||
for (index, id) in PROFITABILITY_IDS.into_iter().enumerate() {
|
||||
assert_eq!(id.index(), index);
|
||||
}
|
||||
|
||||
assert!(
|
||||
ProfitabilityId::range_series(|id, _| id)
|
||||
.iter()
|
||||
.copied()
|
||||
.eq(ProfitabilityId::range_ids().iter().copied())
|
||||
);
|
||||
assert!(
|
||||
ProfitabilityId::profit_series(|id, _| id)
|
||||
.iter()
|
||||
.copied()
|
||||
.eq(ProfitabilityId::profit_ids().iter().copied())
|
||||
);
|
||||
assert!(
|
||||
ProfitabilityId::loss_series(|id, _| id)
|
||||
.iter()
|
||||
.copied()
|
||||
.eq(ProfitabilityId::loss_ids().iter().copied())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_expand_ranges_into_profit_prefixes_and_loss_suffixes() {
|
||||
let ranges = std::array::from_fn(|index| index + 1);
|
||||
let row = ProfitabilityRow::from_ranges(ranges);
|
||||
|
||||
assert_eq!(
|
||||
row.profit[0],
|
||||
ranges[..PROFIT_COUNT + 1].iter().copied().sum::<usize>()
|
||||
);
|
||||
assert_eq!(row.profit[PROFIT_COUNT - 1], ranges[0] + ranges[1]);
|
||||
assert_eq!(
|
||||
row.loss[0],
|
||||
ranges[PROFIT_COUNT + 1..].iter().copied().sum::<usize>()
|
||||
);
|
||||
assert_eq!(
|
||||
row.loss[LOSS_COUNT - 1],
|
||||
ranges[PROFITABILITY_RANGE_COUNT - 2..]
|
||||
.iter()
|
||||
.copied()
|
||||
.sum::<usize>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,121 @@ use brk_traversable::Traversable;
|
||||
use brk_types::OutputType;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use serde::Serialize;
|
||||
use vecdb::{ColumnId, VecValue, Version};
|
||||
|
||||
use super::{CohortName, Filter};
|
||||
|
||||
pub const SPENDABLE_TYPE_COUNT: usize = 11;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[repr(u8)]
|
||||
pub enum SpendableTypeId {
|
||||
P2PK65,
|
||||
P2PK33,
|
||||
P2PKH,
|
||||
P2MS,
|
||||
P2SH,
|
||||
P2WPKH,
|
||||
P2WSH,
|
||||
P2TR,
|
||||
P2A,
|
||||
Unknown,
|
||||
Empty,
|
||||
}
|
||||
|
||||
pub const SPENDABLE_TYPE_IDS: [SpendableTypeId; SPENDABLE_TYPE_COUNT] = [
|
||||
SpendableTypeId::P2PK65,
|
||||
SpendableTypeId::P2PK33,
|
||||
SpendableTypeId::P2PKH,
|
||||
SpendableTypeId::P2MS,
|
||||
SpendableTypeId::P2SH,
|
||||
SpendableTypeId::P2WPKH,
|
||||
SpendableTypeId::P2WSH,
|
||||
SpendableTypeId::P2TR,
|
||||
SpendableTypeId::P2A,
|
||||
SpendableTypeId::Unknown,
|
||||
SpendableTypeId::Empty,
|
||||
];
|
||||
|
||||
impl SpendableTypeId {
|
||||
pub const fn from_output_type(value: OutputType) -> Option<Self> {
|
||||
match value {
|
||||
OutputType::P2PK65 => Some(Self::P2PK65),
|
||||
OutputType::P2PK33 => Some(Self::P2PK33),
|
||||
OutputType::P2PKH => Some(Self::P2PKH),
|
||||
OutputType::P2MS => Some(Self::P2MS),
|
||||
OutputType::P2SH => Some(Self::P2SH),
|
||||
OutputType::P2WPKH => Some(Self::P2WPKH),
|
||||
OutputType::P2WSH => Some(Self::P2WSH),
|
||||
OutputType::P2TR => Some(Self::P2TR),
|
||||
OutputType::P2A => Some(Self::P2A),
|
||||
OutputType::Unknown => Some(Self::Unknown),
|
||||
OutputType::Empty => Some(Self::Empty),
|
||||
OutputType::OpReturn => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn output_type(self) -> OutputType {
|
||||
match self {
|
||||
Self::P2PK65 => OutputType::P2PK65,
|
||||
Self::P2PK33 => OutputType::P2PK33,
|
||||
Self::P2PKH => OutputType::P2PKH,
|
||||
Self::P2MS => OutputType::P2MS,
|
||||
Self::P2SH => OutputType::P2SH,
|
||||
Self::P2WPKH => OutputType::P2WPKH,
|
||||
Self::P2WSH => OutputType::P2WSH,
|
||||
Self::P2TR => OutputType::P2TR,
|
||||
Self::P2A => OutputType::P2A,
|
||||
Self::Unknown => OutputType::Unknown,
|
||||
Self::Empty => OutputType::Empty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColumnId for SpendableTypeId {
|
||||
type Row<T>
|
||||
= [T; SPENDABLE_TYPE_COUNT]
|
||||
where
|
||||
T: VecValue;
|
||||
|
||||
const VERSION: Version = Version::ONE;
|
||||
const ALL: &'static [Self] = &SPENDABLE_TYPE_IDS;
|
||||
|
||||
#[inline]
|
||||
fn index(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
|
||||
&row[self as usize]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
|
||||
&mut row[self as usize]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn from_fn<T, F>(mut f: F) -> Self::Row<T>
|
||||
where
|
||||
T: VecValue,
|
||||
F: FnMut(Self) -> T,
|
||||
{
|
||||
std::array::from_fn(|index| f(SPENDABLE_TYPE_IDS[index]))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn map<T, U, F>(row: Self::Row<T>, f: F) -> Self::Row<U>
|
||||
where
|
||||
T: VecValue,
|
||||
U: VecValue,
|
||||
F: FnMut(T) -> U,
|
||||
{
|
||||
row.map(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Spendable type values
|
||||
pub const SPENDABLE_TYPE_VALUES: SpendableType<OutputType> = SpendableType {
|
||||
p2pk65: OutputType::P2PK65,
|
||||
@@ -279,3 +391,28 @@ where
|
||||
self.empty += rhs.empty;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn column_ids_match_spendable_type_order() {
|
||||
let output_types: Vec<_> = SPENDABLE_TYPE_VALUES.iter().copied().collect();
|
||||
let column_output_types: Vec<_> = SpendableTypeId::ALL
|
||||
.iter()
|
||||
.map(|column| column.output_type())
|
||||
.collect();
|
||||
|
||||
assert_eq!(column_output_types, output_types);
|
||||
assert_eq!(
|
||||
SpendableTypeId::from_output_type(OutputType::OpReturn),
|
||||
None
|
||||
);
|
||||
|
||||
let row = SpendableTypeId::from_fn(|column| column.index());
|
||||
for column in SpendableTypeId::ALL {
|
||||
assert_eq!(*column.get(&row), column.index());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,22 @@
|
||||
//! Address activity tracking - per-block counts of address behaviors.
|
||||
//!
|
||||
//! Tracks global and per-address-type activity metrics:
|
||||
//!
|
||||
//! | Metric | Description |
|
||||
//! |--------|-------------|
|
||||
//! | `receiving` | Unique addresses that received this block |
|
||||
//! | `sending` | Unique addresses that sent this block |
|
||||
//! | `reactivated` | Addresses that were empty and now have funds |
|
||||
//! | `bidirectional` | Addresses that both sent AND received in same block |
|
||||
//! | `active` | Distinct addresses involved (sent ∪ received) |
|
||||
//! Per-block address activity, split by address type.
|
||||
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_cohort::{AddrTypeId, ByAddrType};
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::Version;
|
||||
use brk_types::{StoredU32, StoredU64, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, Database, Rw, StorageMode};
|
||||
use vecdb::{AnyStoredVec, AnyVec, ColumnId, Database, ReadOnlyClone, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{CachedWindowStartVec, CountPerBlockRollingAverage, Windows},
|
||||
internal::{
|
||||
CachedWindowStartVec, ColumnarPerBlockCumulativeRolling, LazyPerBlockCumulativeAverage,
|
||||
StoredU64ToStoredU32, Windows, WithAddrTypes,
|
||||
},
|
||||
};
|
||||
|
||||
/// Per-block activity counts - reset each block.
|
||||
/// Per-block activity counts, reset after every block.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct BlockActivityCounts {
|
||||
pub reactivated: u32,
|
||||
@@ -33,217 +26,69 @@ pub struct BlockActivityCounts {
|
||||
}
|
||||
|
||||
impl BlockActivityCounts {
|
||||
/// Reset all counts to zero.
|
||||
#[inline]
|
||||
pub(crate) fn reset(&mut self) {
|
||||
*self = Self::default();
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn active(&self) -> u32 {
|
||||
debug_assert!(self.bidirectional <= self.sending.min(self.receiving));
|
||||
self.sending + self.receiving - self.bidirectional
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-address-type activity counts - aggregated during block processing.
|
||||
/// Activity counts accumulated during block processing for each address type.
|
||||
#[derive(Debug, Default, Deref, DerefMut)]
|
||||
pub struct AddrTypeToActivityCounts(pub ByAddrType<BlockActivityCounts>);
|
||||
|
||||
impl AddrTypeToActivityCounts {
|
||||
/// Reset all per-type counts.
|
||||
pub(crate) fn reset(&mut self) {
|
||||
self.0.values_mut().for_each(|v| v.reset());
|
||||
self.0.values_mut().for_each(BlockActivityCounts::reset);
|
||||
}
|
||||
|
||||
/// Sum all types to get totals.
|
||||
pub(crate) fn totals(&self) -> BlockActivityCounts {
|
||||
let mut total = BlockActivityCounts::default();
|
||||
for counts in self.0.values() {
|
||||
total.reactivated += counts.reactivated;
|
||||
total.sending += counts.sending;
|
||||
total.receiving += counts.receiving;
|
||||
total.bidirectional += counts.bidirectional;
|
||||
}
|
||||
total
|
||||
pub(crate) fn active(&self) -> u32 {
|
||||
self.0.values().map(BlockActivityCounts::active).sum()
|
||||
}
|
||||
}
|
||||
|
||||
/// Activity count vectors for a single category (e.g., one address type or "all").
|
||||
#[derive(Traversable)]
|
||||
pub struct ActivityCountVecs<M: StorageMode = Rw> {
|
||||
pub reactivated: CountPerBlockRollingAverage<M>,
|
||||
pub sending: CountPerBlockRollingAverage<M>,
|
||||
pub receiving: CountPerBlockRollingAverage<M>,
|
||||
pub bidirectional: CountPerBlockRollingAverage<M>,
|
||||
/// Distinct addresses involved in this block (sent ∪ received),
|
||||
/// computed at push time as `sending + receiving - bidirectional`
|
||||
/// via inclusion-exclusion. For per-type instances this is
|
||||
/// per-type. For the `all` aggregate it's the cross-type total.
|
||||
pub active: CountPerBlockRollingAverage<M>,
|
||||
}
|
||||
|
||||
impl ActivityCountVecs {
|
||||
/// `prefix` is prepended to each field's disk name. Use `""` for the
|
||||
/// "all" aggregate and `"{type}_"` for per-address-type instances.
|
||||
/// Field names are suffixed with `_addrs` so the final disk series
|
||||
/// are e.g. `active_addrs`, `p2tr_bidirectional_addrs`.
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
prefix: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
reactivated: CountPerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
&format!("{prefix}reactivated_addrs"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?,
|
||||
sending: CountPerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
&format!("{prefix}sending_addrs"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?,
|
||||
receiving: CountPerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
&format!("{prefix}receiving_addrs"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?,
|
||||
bidirectional: CountPerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
&format!("{prefix}bidirectional_addrs"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?,
|
||||
active: CountPerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
&format!("{prefix}active_addrs"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?,
|
||||
#[inline(always)]
|
||||
fn row(
|
||||
&self,
|
||||
value: impl Fn(&BlockActivityCounts) -> u32,
|
||||
) -> <AddrTypeId as ColumnId>::Row<StoredU64> {
|
||||
AddrTypeId::from_fn(|column| {
|
||||
StoredU64::from(StoredU32::from(value(column.select(&self.0))))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.reactivated
|
||||
.min_stateful_len()
|
||||
.min(self.sending.min_stateful_len())
|
||||
.min(self.receiving.min_stateful_len())
|
||||
.min(self.bidirectional.min_stateful_len())
|
||||
.min(self.active.min_stateful_len())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
[
|
||||
self.reactivated.stored_mut(),
|
||||
self.sending.stored_mut(),
|
||||
self.receiving.stored_mut(),
|
||||
self.bidirectional.stored_mut(),
|
||||
self.active.stored_mut(),
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.reactivated.reset()?;
|
||||
self.sending.reset()?;
|
||||
self.receiving.reset()?;
|
||||
self.bidirectional.reset()?;
|
||||
self.active.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_height(&mut self, counts: &BlockActivityCounts) {
|
||||
self.reactivated.push_block(counts.reactivated.into());
|
||||
self.sending.push_block(counts.sending.into());
|
||||
self.receiving.push_block(counts.receiving.into());
|
||||
self.bidirectional.push_block(counts.bidirectional.into());
|
||||
let active = counts.sending + counts.receiving - counts.bidirectional;
|
||||
self.active.push_block(active.into());
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-address-type activity count vecs.
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct ActivityCountVecs {
|
||||
pub reactivated: LazyPerBlockCumulativeAverage<StoredU32, StoredU64, StoredU64ToStoredU32>,
|
||||
pub sending: LazyPerBlockCumulativeAverage<StoredU32, StoredU64, StoredU64ToStoredU32>,
|
||||
pub receiving: LazyPerBlockCumulativeAverage<StoredU32, StoredU64, StoredU64ToStoredU32>,
|
||||
pub bidirectional: LazyPerBlockCumulativeAverage<StoredU32, StoredU64, StoredU64ToStoredU32>,
|
||||
pub active: LazyPerBlockCumulativeAverage<StoredU32, StoredU64, StoredU64ToStoredU32>,
|
||||
}
|
||||
|
||||
/// Five metric-first cumulative matrices with cohort-first public views.
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct AddrTypeToActivityCountVecs<M: StorageMode = Rw>(ByAddrType<ActivityCountVecs<M>>);
|
||||
|
||||
impl From<ByAddrType<ActivityCountVecs>> for AddrTypeToActivityCountVecs {
|
||||
#[inline]
|
||||
fn from(value: ByAddrType<ActivityCountVecs>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl AddrTypeToActivityCountVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self::from(ByAddrType::<ActivityCountVecs>::new_with_name(
|
||||
|type_name| {
|
||||
ActivityCountVecs::forced_import(
|
||||
db,
|
||||
&format!("{type_name}_"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)
|
||||
},
|
||||
)?))
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.0
|
||||
.values()
|
||||
.map(|v| v.min_stateful_len())
|
||||
.min()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
|
||||
for type_vecs in self.0.values_mut() {
|
||||
vecs.push(type_vecs.reactivated.stored_mut());
|
||||
vecs.push(type_vecs.sending.stored_mut());
|
||||
vecs.push(type_vecs.receiving.stored_mut());
|
||||
vecs.push(type_vecs.bidirectional.stored_mut());
|
||||
vecs.push(type_vecs.active.stored_mut());
|
||||
}
|
||||
vecs.into_par_iter()
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
for v in self.0.values_mut() {
|
||||
v.reset_height()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_height(&mut self, counts: &AddrTypeToActivityCounts) {
|
||||
for (vecs, c) in self.0.values_mut().zip(counts.0.values()) {
|
||||
vecs.push_height(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Storage for activity metrics (global + per type).
|
||||
#[derive(Traversable)]
|
||||
pub struct AddrActivityVecs<M: StorageMode = Rw> {
|
||||
pub all: ActivityCountVecs<M>,
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
#[traversable(flatten)]
|
||||
pub by_addr_type: AddrTypeToActivityCountVecs<M>,
|
||||
pub series: WithAddrTypes<ActivityCountVecs>,
|
||||
|
||||
#[traversable(hidden)]
|
||||
cumulative_reactivated: ColumnarPerBlockCumulativeRolling<StoredU64, AddrTypeId, (), M>,
|
||||
#[traversable(hidden)]
|
||||
cumulative_sending: ColumnarPerBlockCumulativeRolling<StoredU64, AddrTypeId, (), M>,
|
||||
#[traversable(hidden)]
|
||||
cumulative_receiving: ColumnarPerBlockCumulativeRolling<StoredU64, AddrTypeId, (), M>,
|
||||
#[traversable(hidden)]
|
||||
cumulative_bidirectional: ColumnarPerBlockCumulativeRolling<StoredU64, AddrTypeId, (), M>,
|
||||
#[traversable(hidden)]
|
||||
cumulative_active: ColumnarPerBlockCumulativeRolling<StoredU64, AddrTypeId, (), M>,
|
||||
}
|
||||
|
||||
impl AddrActivityVecs {
|
||||
@@ -253,41 +98,174 @@ impl AddrActivityVecs {
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
let cumulative_version = version + Version::TWO;
|
||||
let cumulative_reactivated = ColumnarPerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
"reactivated_addrs_by_type_cumulative",
|
||||
cumulative_version,
|
||||
|_| (),
|
||||
)?;
|
||||
let cumulative_sending = ColumnarPerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
"sending_addrs_by_type_cumulative",
|
||||
cumulative_version,
|
||||
|_| (),
|
||||
)?;
|
||||
let cumulative_receiving = ColumnarPerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
"receiving_addrs_by_type_cumulative",
|
||||
cumulative_version,
|
||||
|_| (),
|
||||
)?;
|
||||
let cumulative_bidirectional = ColumnarPerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
"bidirectional_addrs_by_type_cumulative",
|
||||
cumulative_version,
|
||||
|_| (),
|
||||
)?;
|
||||
let cumulative_active = ColumnarPerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
"active_addrs_by_type_cumulative",
|
||||
cumulative_version,
|
||||
|_| (),
|
||||
)?;
|
||||
|
||||
let reactivated = WithAddrTypes::from_columnar_cumulative_average_source(
|
||||
"reactivated_addrs",
|
||||
version,
|
||||
&cumulative_reactivated.cumulative.read_only_clone(),
|
||||
indexes,
|
||||
cached_starts,
|
||||
);
|
||||
let sending = WithAddrTypes::from_columnar_cumulative_average_source(
|
||||
"sending_addrs",
|
||||
version,
|
||||
&cumulative_sending.cumulative.read_only_clone(),
|
||||
indexes,
|
||||
cached_starts,
|
||||
);
|
||||
let receiving = WithAddrTypes::from_columnar_cumulative_average_source(
|
||||
"receiving_addrs",
|
||||
version,
|
||||
&cumulative_receiving.cumulative.read_only_clone(),
|
||||
indexes,
|
||||
cached_starts,
|
||||
);
|
||||
let bidirectional = WithAddrTypes::from_columnar_cumulative_average_source(
|
||||
"bidirectional_addrs",
|
||||
version,
|
||||
&cumulative_bidirectional.cumulative.read_only_clone(),
|
||||
indexes,
|
||||
cached_starts,
|
||||
);
|
||||
let active = WithAddrTypes::from_columnar_cumulative_average_source(
|
||||
"active_addrs",
|
||||
version,
|
||||
&cumulative_active.cumulative.read_only_clone(),
|
||||
indexes,
|
||||
cached_starts,
|
||||
);
|
||||
|
||||
let by_addr_type = AddrTypeId::series(|column, _| ActivityCountVecs {
|
||||
reactivated: column.select(&reactivated.by_addr_type).clone(),
|
||||
sending: column.select(&sending.by_addr_type).clone(),
|
||||
receiving: column.select(&receiving.by_addr_type).clone(),
|
||||
bidirectional: column.select(&bidirectional.by_addr_type).clone(),
|
||||
active: column.select(&active.by_addr_type).clone(),
|
||||
});
|
||||
let series = WithAddrTypes {
|
||||
all: ActivityCountVecs {
|
||||
reactivated: reactivated.all,
|
||||
sending: sending.all,
|
||||
receiving: receiving.all,
|
||||
bidirectional: bidirectional.all,
|
||||
active: active.all,
|
||||
},
|
||||
by_addr_type,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
all: ActivityCountVecs::forced_import(db, "", version, indexes, cached_starts)?,
|
||||
by_addr_type: AddrTypeToActivityCountVecs::forced_import(
|
||||
db,
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?,
|
||||
series,
|
||||
cumulative_reactivated,
|
||||
cumulative_sending,
|
||||
cumulative_receiving,
|
||||
cumulative_bidirectional,
|
||||
cumulative_active,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.all
|
||||
.min_stateful_len()
|
||||
.min(self.by_addr_type.min_stateful_len())
|
||||
[
|
||||
self.cumulative_reactivated.cumulative.len(),
|
||||
self.cumulative_sending.cumulative.len(),
|
||||
self.cumulative_receiving.cumulative.len(),
|
||||
self.cumulative_bidirectional.cumulative.len(),
|
||||
self.cumulative_active.cumulative.len(),
|
||||
]
|
||||
.into_iter()
|
||||
.min()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
self.all
|
||||
.par_iter_height_mut()
|
||||
.chain(self.by_addr_type.par_iter_height_mut())
|
||||
[
|
||||
self.cumulative_reactivated.stored_mut(),
|
||||
self.cumulative_sending.stored_mut(),
|
||||
self.cumulative_receiving.stored_mut(),
|
||||
self.cumulative_bidirectional.stored_mut(),
|
||||
self.cumulative_active.stored_mut(),
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.all.reset_height()?;
|
||||
self.by_addr_type.reset_height()?;
|
||||
self.cumulative_reactivated.reset()?;
|
||||
self.cumulative_sending.reset()?;
|
||||
self.cumulative_receiving.reset()?;
|
||||
self.cumulative_bidirectional.reset()?;
|
||||
self.cumulative_active.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_height(&mut self, counts: &AddrTypeToActivityCounts) {
|
||||
let totals = counts.totals();
|
||||
self.all.push_height(&totals);
|
||||
self.by_addr_type.push_height(counts);
|
||||
self.cumulative_reactivated
|
||||
.push_block(counts.row(|counts| counts.reactivated));
|
||||
self.cumulative_sending
|
||||
.push_block(counts.row(|counts| counts.sending));
|
||||
self.cumulative_receiving
|
||||
.push_block(counts.row(|counts| counts.receiving));
|
||||
self.cumulative_bidirectional
|
||||
.push_block(counts.row(|counts| counts.bidirectional));
|
||||
self.cumulative_active
|
||||
.push_block(counts.row(BlockActivityCounts::active));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn active_count_sums_distinct_addresses_across_types() {
|
||||
let counts = AddrTypeToActivityCounts(ByAddrType {
|
||||
p2pkh: BlockActivityCounts {
|
||||
sending: 5,
|
||||
receiving: 4,
|
||||
bidirectional: 2,
|
||||
..BlockActivityCounts::default()
|
||||
},
|
||||
p2tr: BlockActivityCounts {
|
||||
sending: 3,
|
||||
receiving: 2,
|
||||
bidirectional: 1,
|
||||
..BlockActivityCounts::default()
|
||||
},
|
||||
..ByAddrType::default()
|
||||
});
|
||||
|
||||
assert_eq!(counts.active(), 11);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use brk_cohort::AddrTypeId;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{StoredU64, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{Database, Rw, StorageMode};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, AnyVec, Database, Rw, StorageMode, WritableVec};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{PerBlock, WithAddrTypes},
|
||||
internal::{ColumnarPerBlock, LazyColumnPerBlock, LazyPerBlock, WithAddrTypes},
|
||||
};
|
||||
|
||||
use super::AddrTypeToAddrCount;
|
||||
@@ -16,7 +18,13 @@ use super::AddrTypeToAddrCount;
|
||||
/// funded/total pairs used by exposed, reused, and respent.
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct AddrCountsVecs<M: StorageMode = Rw>(
|
||||
#[traversable(flatten)] pub WithAddrTypes<PerBlock<StoredU64, M>>,
|
||||
#[traversable(flatten)]
|
||||
pub ColumnarPerBlock<
|
||||
StoredU64,
|
||||
AddrTypeId,
|
||||
WithAddrTypes<LazyColumnPerBlock<StoredU64, AddrTypeId>, LazyPerBlock<StoredU64>>,
|
||||
M,
|
||||
>,
|
||||
);
|
||||
|
||||
impl AddrCountsVecs {
|
||||
@@ -26,13 +34,31 @@ impl AddrCountsVecs {
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(WithAddrTypes::<PerBlock<StoredU64>>::forced_import(
|
||||
db, name, version, indexes,
|
||||
Ok(Self(ColumnarPerBlock::forced_import(
|
||||
db,
|
||||
&format!("{name}_by_type"),
|
||||
version,
|
||||
|source| WithAddrTypes::from_columnar_source(name, version, source, indexes),
|
||||
)?))
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.height.len()
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
rayon::iter::once(&mut self.height as &mut dyn AnyStoredVec)
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.height.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_counts(&mut self, counts: &AddrTypeToAddrCount) {
|
||||
self.push_height(counts.sum(), counts.values().copied());
|
||||
self.push(counts.row());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Lengths;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::Version;
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, Database, Exit, Rw, StorageMode};
|
||||
use vecdb::{AnyStoredVec, Database, Rw, StorageMode};
|
||||
|
||||
use crate::indexes;
|
||||
|
||||
@@ -70,10 +69,4 @@ impl AddrCountFundedTotalVecs {
|
||||
self.funded.push_counts(funded);
|
||||
self.total.push_counts(total);
|
||||
}
|
||||
|
||||
pub(crate) fn compute_rest(&mut self, starting_lengths: &Lengths, exit: &Exit) -> Result<()> {
|
||||
self.funded.compute_rest(starting_lengths, exit)?;
|
||||
self.total.compute_rest(starting_lengths, exit)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_types::Height;
|
||||
use brk_cohort::{AddrTypeId, ByAddrType};
|
||||
use brk_types::{Height, StoredU64};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::ReadableVec;
|
||||
use vecdb::{ColumnId, ReadableVec};
|
||||
|
||||
use super::AddrCountsVecs;
|
||||
|
||||
@@ -12,9 +12,8 @@ use super::AddrCountsVecs;
|
||||
pub struct AddrTypeToAddrCount(ByAddrType<u64>);
|
||||
|
||||
impl AddrTypeToAddrCount {
|
||||
#[inline]
|
||||
pub(crate) fn sum(&self) -> u64 {
|
||||
self.0.values().sum()
|
||||
pub(crate) fn row(&self) -> <AddrTypeId as ColumnId>::Row<StoredU64> {
|
||||
AddrTypeId::from_fn(|id| StoredU64::from(*id.select(&self.0)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,12 +88,21 @@ impl ExposedAddrVecs {
|
||||
.min(self.supply.min_stateful_len())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_stateful_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
self.count
|
||||
.par_iter_height_mut()
|
||||
.chain(self.supply.par_iter_height_mut())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
self.count
|
||||
.par_iter_height_mut()
|
||||
.chain(self.supply.par_iter_height_mut())
|
||||
.chain(rayon::iter::once(self.supply_share.stored_mut()))
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
@@ -115,7 +124,6 @@ impl ExposedAddrVecs {
|
||||
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.count.compute_rest(starting_lengths, exit)?;
|
||||
self.supply_share.compute_rest(
|
||||
starting_lengths.height,
|
||||
&self.supply,
|
||||
|
||||
@@ -23,7 +23,7 @@ impl NewAddrCountVecs {
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
) -> Self {
|
||||
Self(WithAddrTypes {
|
||||
all: LazyPerBlockCumulativeRolling::from_source(
|
||||
all: LazyPerBlockCumulativeRolling::from_lazy_source(
|
||||
"new_addr_count",
|
||||
version,
|
||||
&total.all,
|
||||
@@ -31,7 +31,7 @@ impl NewAddrCountVecs {
|
||||
indexes,
|
||||
),
|
||||
by_addr_type: total.by_addr_type.map_with_name(|name, total| {
|
||||
LazyPerBlockCumulativeRolling::from_source(
|
||||
LazyPerBlockCumulativeRolling::from_column_source(
|
||||
&format!("{name}_new_addr_count"),
|
||||
version,
|
||||
total,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_cohort::{AddrTypeId, ByAddrType};
|
||||
use brk_types::StoredU64;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::ColumnId;
|
||||
|
||||
/// Per-block running counter of address-reuse events, per address type. Shared
|
||||
/// across reused (receive-based) and respent (spend-based) flavors, and
|
||||
@@ -18,6 +20,11 @@ impl AddrTypeToAddrEventCount {
|
||||
self.0.values().sum()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn row(&self) -> <AddrTypeId as ColumnId>::Row<StoredU64> {
|
||||
AddrTypeId::from_fn(|column| StoredU64::from(*column.select(&self.0)))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn reset(&mut self) {
|
||||
for v in self.0.values_mut() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use brk_cohort::{ByAddrType, zip2_by_addr_type};
|
||||
use brk_cohort::{AddrTypeId, ByAddrType};
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Lengths;
|
||||
use brk_traversable::Traversable;
|
||||
@@ -9,9 +9,10 @@ use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec};
|
||||
use crate::{
|
||||
indexes, inputs,
|
||||
internal::{
|
||||
CachedBlockCountReader, CachedWindowStartVec, CountPerBlockRollingAverage,
|
||||
LazyPercentCumulativeRolling, PerBlockCumulativeRolling, PerBlockRollingAverage, RatioU64,
|
||||
Windows, WithAddrTypes,
|
||||
CachedBlockCountReader, CachedWindowStartVec, ColumnarPerBlockCumulativeRolling,
|
||||
CountPerBlockRollingAverage, LazyColumnPerBlockCumulativeRolling,
|
||||
LazyPerBlockCumulativeRolling, LazyPercentCumulativeRolling, PerBlockRollingAverage,
|
||||
RatioU64, Windows, WithAddrTypes,
|
||||
},
|
||||
outputs,
|
||||
};
|
||||
@@ -78,33 +79,45 @@ impl AddrEventShares {
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
all: LazyPercentCumulativeRolling<PartsPerMillion32>,
|
||||
numerators: &ByAddrType<PerBlockCumulativeRolling<StoredU64>>,
|
||||
numerators: &ByAddrType<LazyColumnPerBlockCumulativeRolling<StoredU64, AddrTypeId>>,
|
||||
denominators: &ByAddrType<CachedBlockCountReader>,
|
||||
) -> Result<Self> {
|
||||
let by_addr_type = zip2_by_addr_type(
|
||||
numerators,
|
||||
denominators,
|
||||
|type_name, numerator, denominator| {
|
||||
Ok(LazyPercentCumulativeRolling::from_cached_block_count(
|
||||
&format!("{type_name}_{name}"),
|
||||
version,
|
||||
&numerator.cumulative.height,
|
||||
denominator.clone(),
|
||||
cached_starts,
|
||||
indexes,
|
||||
))
|
||||
},
|
||||
)?;
|
||||
Ok(Self { all, by_addr_type })
|
||||
) -> Self {
|
||||
let by_addr_type = AddrTypeId::series(|column, type_name| {
|
||||
LazyPercentCumulativeRolling::from_cached_block_count(
|
||||
&format!("{type_name}_{name}"),
|
||||
version,
|
||||
&column.select(numerators).cumulative.height,
|
||||
column.select(denominators).clone(),
|
||||
cached_starts,
|
||||
indexes,
|
||||
)
|
||||
});
|
||||
Self { all, by_addr_type }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct AddrEventsVecs<M: StorageMode = Rw> {
|
||||
pub output_to_reused_addr_count: WithAddrTypes<PerBlockCumulativeRolling<StoredU64, M>>,
|
||||
pub output_to_reused_addr_count: ColumnarPerBlockCumulativeRolling<
|
||||
StoredU64,
|
||||
AddrTypeId,
|
||||
WithAddrTypes<
|
||||
LazyColumnPerBlockCumulativeRolling<StoredU64, AddrTypeId>,
|
||||
LazyPerBlockCumulativeRolling<StoredU64>,
|
||||
>,
|
||||
M,
|
||||
>,
|
||||
pub output_to_reused_addr_share: AddrEventShares,
|
||||
pub spendable_output_to_reused_addr_share: LazyPercentCumulativeRolling<PartsPerMillion32>,
|
||||
pub input_from_reused_addr_count: WithAddrTypes<PerBlockCumulativeRolling<StoredU64, M>>,
|
||||
pub input_from_reused_addr_count: ColumnarPerBlockCumulativeRolling<
|
||||
StoredU64,
|
||||
AddrTypeId,
|
||||
WithAddrTypes<
|
||||
LazyColumnPerBlockCumulativeRolling<StoredU64, AddrTypeId>,
|
||||
LazyPerBlockCumulativeRolling<StoredU64>,
|
||||
>,
|
||||
M,
|
||||
>,
|
||||
pub input_from_reused_addr_share: AddrEventShares,
|
||||
pub active_reused_addr_count: CountPerBlockRollingAverage<M>,
|
||||
pub active_reused_addr_share: PerBlockRollingAverage<StoredF32, StoredF32, M>,
|
||||
@@ -121,12 +134,19 @@ impl AddrEventsVecs {
|
||||
inputs_by_type: &inputs::ByTypeVecs,
|
||||
) -> Result<Self> {
|
||||
let import_count = |name: &str| {
|
||||
WithAddrTypes::<PerBlockCumulativeRolling<StoredU64>>::forced_import(
|
||||
ColumnarPerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
name,
|
||||
&format!("{name}_by_type_cumulative"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
|source| {
|
||||
WithAddrTypes::from_columnar_cumulative_source(
|
||||
name,
|
||||
version,
|
||||
source,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
@@ -147,7 +167,7 @@ impl AddrEventsVecs {
|
||||
),
|
||||
&output_to_reused_addr_count.by_addr_type,
|
||||
&output_denominators,
|
||||
)?;
|
||||
);
|
||||
let spendable_share_name = format!("spendable_output_to_{name}_addr_share");
|
||||
let spendable_output_to_reused_addr_share =
|
||||
LazyPercentCumulativeRolling::from_cumulative_ratio::<
|
||||
@@ -179,7 +199,7 @@ impl AddrEventsVecs {
|
||||
),
|
||||
&input_from_reused_addr_count.by_addr_type,
|
||||
&input_denominators,
|
||||
)?;
|
||||
);
|
||||
|
||||
let active_reused_addr_count = CountPerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
@@ -209,8 +229,9 @@ impl AddrEventsVecs {
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.output_to_reused_addr_count
|
||||
.min_stateful_len()
|
||||
.min(self.input_from_reused_addr_count.min_stateful_len())
|
||||
.cumulative
|
||||
.len()
|
||||
.min(self.input_from_reused_addr_count.cumulative.len())
|
||||
.min(self.active_reused_addr_count.min_stateful_len())
|
||||
.min(self.active_reused_addr_share.block.len())
|
||||
}
|
||||
@@ -218,9 +239,10 @@ impl AddrEventsVecs {
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
self.output_to_reused_addr_count
|
||||
.par_iter_height_mut()
|
||||
.chain(self.input_from_reused_addr_count.par_iter_height_mut())
|
||||
rayon::iter::once(self.output_to_reused_addr_count.stored_mut())
|
||||
.chain(rayon::iter::once(
|
||||
self.input_from_reused_addr_count.stored_mut(),
|
||||
))
|
||||
.chain([
|
||||
self.active_reused_addr_count.stored_mut(),
|
||||
&mut self.active_reused_addr_share.block as &mut dyn AnyStoredVec,
|
||||
@@ -228,8 +250,8 @@ impl AddrEventsVecs {
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.output_to_reused_addr_count.reset_height()?;
|
||||
self.input_from_reused_addr_count.reset_height()?;
|
||||
self.output_to_reused_addr_count.reset()?;
|
||||
self.input_from_reused_addr_count.reset()?;
|
||||
self.active_reused_addr_count.reset()?;
|
||||
self.active_reused_addr_share.block.reset()?;
|
||||
Ok(())
|
||||
@@ -243,10 +265,8 @@ impl AddrEventsVecs {
|
||||
active_addr_count: u32,
|
||||
active_reused_addr_count: u32,
|
||||
) {
|
||||
self.output_to_reused_addr_count
|
||||
.push_height(uses.sum(), uses.values().copied());
|
||||
self.input_from_reused_addr_count
|
||||
.push_height(spends.sum(), spends.values().copied());
|
||||
self.output_to_reused_addr_count.push_block(uses.row());
|
||||
self.input_from_reused_addr_count.push_block(spends.row());
|
||||
self.active_reused_addr_count
|
||||
.push_block(StoredU32::from(active_reused_addr_count));
|
||||
// Stored as a percentage in [0, 100] to match the rest of the
|
||||
|
||||
@@ -97,6 +97,15 @@ impl ReusedAddrVecs {
|
||||
.min(self.supply.min_stateful_len())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_stateful_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
self.count
|
||||
.par_iter_height_mut()
|
||||
.chain(self.events.par_iter_height_mut())
|
||||
.chain(self.supply.par_iter_height_mut())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
@@ -104,6 +113,7 @@ impl ReusedAddrVecs {
|
||||
.par_iter_height_mut()
|
||||
.chain(self.events.par_iter_height_mut())
|
||||
.chain(self.supply.par_iter_height_mut())
|
||||
.chain(rayon::iter::once(self.supply_share.stored_mut()))
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
@@ -136,7 +146,6 @@ impl ReusedAddrVecs {
|
||||
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.count.compute_rest(starting_lengths, exit)?;
|
||||
self.events.compute_rest(starting_lengths, exit)?;
|
||||
self.supply_share.compute_rest(
|
||||
starting_lengths.height,
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_cohort::{AddrTypeId, ByAddrType};
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, PartsPerMillion32, Sats, Version};
|
||||
use vecdb::{Database, Exit, ReadableVec, Rw, StorageMode, WritableVec};
|
||||
use vecdb::{
|
||||
AnyStoredVec, BinaryTransform, Database, Exit, ReadOnlyClone, ReadableVec, Rw, StorageMode,
|
||||
WritableVec,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
distribution::metrics::AllSupplyCache,
|
||||
indexes,
|
||||
internal::{LazyPercentPerBlock, PercentPerBlock, RatioSats},
|
||||
internal::{ColumnarPerBlock, LazyColumnPercentPerBlock, LazyPercentPerBlock, RatioSats},
|
||||
};
|
||||
|
||||
use super::vecs::AddrSupplyVecs;
|
||||
@@ -20,7 +23,9 @@ use super::vecs::AddrSupplyVecs;
|
||||
pub struct AddrSupplyShareVecs<M: StorageMode = Rw> {
|
||||
pub all: LazyPercentPerBlock<PartsPerMillion32>,
|
||||
#[traversable(flatten)]
|
||||
pub by_addr_type: ByAddrType<PercentPerBlock<PartsPerMillion32, M>>,
|
||||
pub by_addr_type: ByAddrType<LazyColumnPercentPerBlock<PartsPerMillion32, AddrTypeId>>,
|
||||
#[traversable(hidden)]
|
||||
ppm: ColumnarPerBlock<PartsPerMillion32, AddrTypeId, (), M>,
|
||||
}
|
||||
|
||||
impl AddrSupplyShareVecs {
|
||||
@@ -40,20 +45,35 @@ impl AddrSupplyShareVecs {
|
||||
all_supply.cached_boxed_clone(),
|
||||
indexes,
|
||||
);
|
||||
let by_addr_type = ByAddrType::new_with_name(|type_name| {
|
||||
PercentPerBlock::forced_import(db, &format!("{type_name}_{name}"), version, indexes)
|
||||
})?;
|
||||
let ppm =
|
||||
ColumnarPerBlock::forced_import(db, &format!("{name}_ppm_by_type"), version, |_| ())?;
|
||||
let source = ppm.height.read_only_clone();
|
||||
let by_addr_type = AddrTypeId::series(|column, type_name| {
|
||||
LazyColumnPercentPerBlock::new(
|
||||
&format!("{type_name}_{name}"),
|
||||
version,
|
||||
&source,
|
||||
column,
|
||||
indexes,
|
||||
)
|
||||
});
|
||||
|
||||
Ok(Self { all, by_addr_type })
|
||||
Ok(Self {
|
||||
all,
|
||||
by_addr_type,
|
||||
ppm,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
for share in self.by_addr_type.values_mut() {
|
||||
share.ppm.height.reset()?;
|
||||
}
|
||||
self.ppm.height.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn stored_mut(&mut self) -> &mut dyn AnyStoredVec {
|
||||
self.ppm.stored_mut()
|
||||
}
|
||||
|
||||
pub(crate) fn compute_rest(
|
||||
&mut self,
|
||||
max_from: Height,
|
||||
@@ -61,18 +81,12 @@ impl AddrSupplyShareVecs {
|
||||
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
for ((_, share), ((_, cat), (_, denom))) in self
|
||||
.by_addr_type
|
||||
.iter_mut()
|
||||
.zip(supply.by_addr_type.iter().zip(type_supply_sats.iter()))
|
||||
{
|
||||
share.compute_binary::<Sats, Sats, RatioSats<PartsPerMillion32>>(
|
||||
max_from,
|
||||
&cat.sats.height,
|
||||
*denom,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
self.ppm.compute_matrix_columns2(
|
||||
max_from,
|
||||
&supply.height,
|
||||
|column| *column.select(type_supply_sats),
|
||||
|_, category, total| RatioSats::<PartsPerMillion32>::apply(category, total),
|
||||
exit,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_cohort::{AddrTypeId, ByAddrType};
|
||||
use brk_types::{Height, OutputType, Sats};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::ReadableVec;
|
||||
use vecdb::{ColumnId, ReadableVec};
|
||||
|
||||
use super::vecs::AddrSupplyVecs;
|
||||
|
||||
@@ -12,8 +12,8 @@ pub struct AddrTypeToSupply(ByAddrType<Sats>);
|
||||
|
||||
impl AddrTypeToSupply {
|
||||
#[inline]
|
||||
pub(crate) fn sum(&self) -> Sats {
|
||||
self.0.values().copied().sum()
|
||||
pub(crate) fn row(&self) -> <AddrTypeId as ColumnId>::Row<Sats> {
|
||||
AddrTypeId::from_fn(|column| *column.select(&self.0))
|
||||
}
|
||||
|
||||
/// Apply a signed `after - before` delta to the slot for `output_type`.
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
use brk_cohort::AddrTypeId;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Cents, Height, Version};
|
||||
use brk_types::{Cents, Height, Sats, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{CachedBoxedVec, Database, Rw, StorageMode};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, AnyVec, CachedBoxedVec, Database, Rw, StorageMode, WritableVec};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{SpotValuePerBlock, WithAddrTypes},
|
||||
internal::{
|
||||
ColumnarPerBlock, LazyColumnSpotValuePerBlock, LazySpotValuePerBlock, WithAddrTypes,
|
||||
},
|
||||
};
|
||||
|
||||
use super::AddrTypeToSupply;
|
||||
@@ -17,7 +21,13 @@ use super::AddrTypeToSupply;
|
||||
/// sats × spot price.
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct AddrSupplyVecs<M: StorageMode = Rw>(
|
||||
#[traversable(flatten)] pub WithAddrTypes<SpotValuePerBlock<M>>,
|
||||
#[traversable(flatten)]
|
||||
pub ColumnarPerBlock<
|
||||
Sats,
|
||||
AddrTypeId,
|
||||
WithAddrTypes<LazyColumnSpotValuePerBlock<AddrTypeId>, LazySpotValuePerBlock>,
|
||||
M,
|
||||
>,
|
||||
);
|
||||
|
||||
impl AddrSupplyVecs {
|
||||
@@ -28,17 +38,36 @@ impl AddrSupplyVecs {
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(WithAddrTypes::<SpotValuePerBlock>::forced_import(
|
||||
let name = format!("{name}_addr_supply");
|
||||
Ok(Self(ColumnarPerBlock::forced_import(
|
||||
db,
|
||||
&format!("{name}_addr_supply"),
|
||||
&format!("{name}_sats_by_type"),
|
||||
version,
|
||||
indexes,
|
||||
spot_price,
|
||||
|source| {
|
||||
WithAddrTypes::from_columnar_spot_value_source(
|
||||
&name, version, source, indexes, spot_price,
|
||||
)
|
||||
},
|
||||
)?))
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.height.len()
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
rayon::iter::once(self.stored_mut())
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.height.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_supply(&mut self, supply: &AddrTypeToSupply) {
|
||||
self.push_height(supply.sum(), supply.values().copied());
|
||||
self.push(supply.row());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
use brk_cohort::AddrTypeId;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, StoredU64, Version};
|
||||
use brk_types::{Height, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{Database, Exit, Rw, StorageMode};
|
||||
use vecdb::{ColumnId, Database, Exit, ReadOnlyClone, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{PerBlock, WithAddrTypes},
|
||||
};
|
||||
use crate::indexes;
|
||||
|
||||
use super::AddrCountsVecs;
|
||||
|
||||
/// Total address count (global + per-type) with all derived indexes.
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct TotalAddrCountVecs<M: StorageMode = Rw>(
|
||||
#[traversable(flatten)] pub WithAddrTypes<PerBlock<StoredU64, M>>,
|
||||
);
|
||||
pub struct TotalAddrCountVecs<M: StorageMode = Rw>(#[traversable(flatten)] pub AddrCountsVecs<M>);
|
||||
|
||||
impl TotalAddrCountVecs {
|
||||
pub(crate) fn forced_import(
|
||||
@@ -23,7 +19,7 @@ impl TotalAddrCountVecs {
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(WithAddrTypes::<PerBlock<StoredU64>>::forced_import(
|
||||
Ok(Self(AddrCountsVecs::forced_import(
|
||||
db,
|
||||
"total_addr_count",
|
||||
version,
|
||||
@@ -39,24 +35,21 @@ impl TotalAddrCountVecs {
|
||||
empty_addr_count: &AddrCountsVecs,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.all.height.compute_add(
|
||||
let addr_count = addr_count.height.read_only_clone();
|
||||
let empty_addr_count = empty_addr_count.height.read_only_clone();
|
||||
self.height.compute_transform2(
|
||||
max_from,
|
||||
&addr_count.all.height,
|
||||
&empty_addr_count.all.height,
|
||||
&addr_count,
|
||||
&empty_addr_count,
|
||||
|(height, addr_count, empty_addr_count, ..)| {
|
||||
let total = AddrTypeId::from_fn(|column| {
|
||||
*column.get(&addr_count) + *column.get(&empty_addr_count)
|
||||
});
|
||||
(height, total)
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
|
||||
for ((_, total), ((_, addr), (_, empty))) in self.by_addr_type.iter_mut().zip(
|
||||
addr_count
|
||||
.by_addr_type
|
||||
.iter()
|
||||
.zip(empty_addr_count.by_addr_type.iter()),
|
||||
) {
|
||||
total
|
||||
.height
|
||||
.compute_add(max_from, &addr.height, &empty.height, exit)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use brk_cohort::{
|
||||
AGE_RANGE_COUNT, Filter, PROFITABILITY_RANGE_COUNT, compute_profitability_boundaries,
|
||||
AGE_RANGE_COUNT, AgeRangeId, Filter, PROFITABILITY_RANGE_COUNT,
|
||||
compute_profitability_boundaries,
|
||||
};
|
||||
use brk_types::{Cents, CentsCompact, PartsPerMillion32, Sats};
|
||||
use vecdb::ColumnId;
|
||||
|
||||
use crate::{
|
||||
distribution::state::PendingDelta,
|
||||
@@ -123,18 +127,14 @@ impl CostBasisFenwick {
|
||||
}
|
||||
|
||||
/// Pre-compute `is_sth` lookup from the STH filter and age-range filters.
|
||||
pub(super) fn compute_is_sth<'a>(
|
||||
&mut self,
|
||||
sth_filter: &Filter,
|
||||
age_range_filters: impl Iterator<Item = &'a Filter>,
|
||||
) {
|
||||
for (i, f) in age_range_filters.enumerate() {
|
||||
self.is_sth[i] = sth_filter.includes(f);
|
||||
pub(super) fn compute_is_sth(&mut self, sth_filter: &Filter) {
|
||||
for id in AgeRangeId::ALL {
|
||||
self.is_sth[id.index()] = sth_filter.includes(id.filter());
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_sth_at(&self, age_range_idx: usize) -> bool {
|
||||
self.is_sth[age_range_idx]
|
||||
pub(super) fn is_sth(&self, id: AgeRangeId) -> bool {
|
||||
self.is_sth[id.index()]
|
||||
}
|
||||
|
||||
/// Apply a net delta from a pending map entry.
|
||||
@@ -158,7 +158,7 @@ impl CostBasisFenwick {
|
||||
/// Bulk-initialize from age-range maps.
|
||||
pub(super) fn bulk_init<'a>(
|
||||
&mut self,
|
||||
maps: impl Iterator<Item = (&'a std::collections::BTreeMap<CentsCompact, Sats>, bool)>,
|
||||
maps: impl Iterator<Item = (&'a BTreeMap<CentsCompact, Sats>, bool)>,
|
||||
) {
|
||||
self.tree.reset();
|
||||
self.totals = CostBasisNode::default();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::path::Path;
|
||||
|
||||
use brk_cohort::{
|
||||
AGE_RANGE_COUNT, AgeRange, AmountRange, ByEntry, ByEpoch, Class, CohortContext, Filter,
|
||||
Filtered, OverAge, OverAmount, SpendableType, Term, UnderAge, UnderAmount,
|
||||
AGE_RANGE_COUNT, AgeRange, AgeRangeId, AmountRange, ByEntry, ByEpoch, Class, CohortContext,
|
||||
Filter, OverAge, OverAmount, SpendableType, Term, UnderAge, UnderAmount,
|
||||
};
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Lengths;
|
||||
@@ -10,7 +10,8 @@ use brk_traversable::Traversable;
|
||||
use brk_types::{Cents, CentsSquaredSats, Height, Sats, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, CachedBoxedVec, Database, Exit, ReadOnlyClone, Rw, StorageMode,
|
||||
AnyStoredVec, AnyVec, BinaryTransform, CachedBoxedVec, ColumnId, ColumnarVec, Database,
|
||||
EagerVec, Exit, ImportableVec, PcoVec, ReadOnlyClone, ReadableVec, Rw, StorageMode,
|
||||
WritableVec,
|
||||
};
|
||||
|
||||
@@ -18,15 +19,17 @@ use crate::{
|
||||
distribution::{
|
||||
AllChainCache, DynCohortVecs,
|
||||
metrics::{
|
||||
AllCohortMetrics, AllSupplyCache, BasicCohortMetrics, CohortMetricsBase,
|
||||
CoreCohortMetrics, ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig,
|
||||
MinimalCohortMetrics, ProfitabilityMetrics, RealizedFullAccum, SupplyCore,
|
||||
TypeCohortMetrics,
|
||||
AgeRangeSupplySources, AllCohortMetrics, AllSupplyCache, BasicCohortMetrics,
|
||||
CohortMetricsBase, CoreCohortMetrics, ExtendedAdjustedCohortMetrics,
|
||||
ExtendedCohortMetrics, ImportConfig, MinimalCohortMetrics, ProfitabilityMetrics,
|
||||
RealizedFullAccum, TypeCohortMetrics,
|
||||
},
|
||||
state::UTXOCohortState,
|
||||
},
|
||||
indexes,
|
||||
internal::{CachedWindowStartVec, ValuePerBlockCumulativeRolling, Windows},
|
||||
internal::{
|
||||
CachedWindowStartVec, LazyColumnValuePerBlockCumulativeRolling, SatsToCents, Windows,
|
||||
},
|
||||
price,
|
||||
};
|
||||
|
||||
@@ -52,7 +55,12 @@ pub struct UTXOCohorts<M: StorageMode = Rw> {
|
||||
#[traversable(rename = "type")]
|
||||
pub type_: SpendableType<UTXOCohortVecs<TypeCohortMetrics<M>>>,
|
||||
pub profitability: ProfitabilityMetrics<M>,
|
||||
pub matured: AgeRange<ValuePerBlockCumulativeRolling<M>>,
|
||||
#[traversable(skip)]
|
||||
age_range_supply: AgeRangeSupplySources<M>,
|
||||
pub matured: AgeRange<LazyColumnValuePerBlockCumulativeRolling<AgeRangeId>>,
|
||||
pub cumulative_matured_sats: M::Stored<EagerVec<ColumnarVec<PcoVec<Height, Sats>, AgeRangeId>>>,
|
||||
pub cumulative_matured_cents:
|
||||
M::Stored<EagerVec<ColumnarVec<PcoVec<Height, Cents>, AgeRangeId>>>,
|
||||
#[traversable(skip)]
|
||||
all_supply_cache: AllSupplyCache,
|
||||
#[traversable(skip)]
|
||||
@@ -69,6 +77,10 @@ pub(crate) struct UTXOCohortsTransientState {
|
||||
pub(super) tick_tock_cached_positions: [usize; AGE_RANGE_COUNT - 1],
|
||||
}
|
||||
|
||||
const MATURED_VERSION: Version = Version::new(4);
|
||||
const AGE_RANGE_SUPPLY_VERSION: Version = Version::ONE;
|
||||
const WRITE_INTERVAL: usize = 10_000;
|
||||
|
||||
impl UTXOCohorts<Rw> {
|
||||
/// Separate cohorts currently total 74:
|
||||
/// 23 age + 5 epoch + 18 class + 2 entry + 15 amount + 11 spendable type.
|
||||
@@ -85,8 +97,9 @@ impl UTXOCohorts<Rw> {
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let v = version + VERSION;
|
||||
let prefix = CohortContext::Utxo.prefix();
|
||||
|
||||
// Phase 1: Import "all" supply first so it can be referenced by all cohorts' relative metrics.
|
||||
// Phase 1: Import the age-range supply source and its cached all-column view.
|
||||
let all_full_name = CohortContext::Utxo.full_name(&Filter::All, "");
|
||||
let all_cfg = ImportConfig {
|
||||
db,
|
||||
@@ -97,33 +110,31 @@ impl UTXOCohorts<Rw> {
|
||||
cached_starts,
|
||||
spot_price,
|
||||
};
|
||||
let all_supply = SupplyCore::forced_import_all(&all_cfg)?;
|
||||
let all_supply_cache = AllSupplyCache::new(&all_supply.total.sats.height);
|
||||
let age_range_supply =
|
||||
AgeRangeSupplySources::forced_import(db, prefix, v + AGE_RANGE_SUPPLY_VERSION)?;
|
||||
let (all_supply, all_supply_cache) = age_range_supply.all(&all_cfg);
|
||||
let all_chain_cache = AllChainCache::new(&all_supply_cache, spot_price);
|
||||
|
||||
// Phase 2: Import separate (stateful) cohorts.
|
||||
|
||||
// Helper for separate cohorts with BasicCohortMetrics + full state
|
||||
let basic_separate =
|
||||
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<BasicCohortMetrics>> {
|
||||
let full_name = CohortContext::Utxo.full_name(&f, name);
|
||||
let cfg = ImportConfig {
|
||||
db,
|
||||
filter: &f,
|
||||
full_name: &full_name,
|
||||
version: v,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
};
|
||||
let state = Some(Box::new(UTXOCohortState::new(states_path, &full_name)));
|
||||
Ok(UTXOCohortVecs::new(
|
||||
state,
|
||||
BasicCohortMetrics::forced_import(&cfg, &all_supply_cache)?,
|
||||
))
|
||||
let age_range = AgeRange::try_from_fn(|id| {
|
||||
let filter = id.filter().clone();
|
||||
let full_name = CohortContext::Utxo.full_name(&filter, id.name().id);
|
||||
let cfg = ImportConfig {
|
||||
db,
|
||||
filter: &filter,
|
||||
full_name: &full_name,
|
||||
version: v,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
};
|
||||
|
||||
let age_range = AgeRange::try_new(&basic_separate)?;
|
||||
let state = Some(Box::new(UTXOCohortState::new(states_path, &full_name)));
|
||||
let supply = age_range_supply.column(&cfg, id, &all_supply_cache);
|
||||
Ok::<_, brk_error::Error>(UTXOCohortVecs::new(
|
||||
state,
|
||||
BasicCohortMetrics::forced_import(&cfg, supply)?,
|
||||
))
|
||||
})?;
|
||||
|
||||
let core_separate =
|
||||
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<CoreCohortMetrics>> {
|
||||
@@ -221,7 +232,7 @@ impl UTXOCohorts<Rw> {
|
||||
None,
|
||||
ExtendedAdjustedCohortMetrics::forced_import(
|
||||
&cfg,
|
||||
&all_supply_cache,
|
||||
age_range_supply.filtered(&cfg, &f, &all_supply_cache),
|
||||
&all_chain_cache,
|
||||
)?,
|
||||
)
|
||||
@@ -242,7 +253,11 @@ impl UTXOCohorts<Rw> {
|
||||
};
|
||||
UTXOCohortVecs::new(
|
||||
None,
|
||||
ExtendedCohortMetrics::forced_import(&cfg, &all_supply_cache, &all_chain_cache)?,
|
||||
ExtendedCohortMetrics::forced_import(
|
||||
&cfg,
|
||||
age_range_supply.filtered(&cfg, &f, &all_supply_cache),
|
||||
&all_chain_cache,
|
||||
)?,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -259,9 +274,10 @@ impl UTXOCohorts<Rw> {
|
||||
cached_starts,
|
||||
spot_price,
|
||||
};
|
||||
let supply = age_range_supply.filtered(&cfg, &f, &all_supply_cache);
|
||||
Ok(UTXOCohortVecs::new(
|
||||
None,
|
||||
CoreCohortMetrics::forced_import(&cfg, &all_supply_cache)?,
|
||||
CoreCohortMetrics::forced_import_with_supply(&cfg, supply)?,
|
||||
))
|
||||
};
|
||||
|
||||
@@ -292,18 +308,32 @@ impl UTXOCohorts<Rw> {
|
||||
let under_amount = UnderAmount::try_new(&minimal_no_state)?;
|
||||
let over_amount = OverAmount::try_new(&minimal_no_state)?;
|
||||
|
||||
let prefix = CohortContext::Utxo.prefix();
|
||||
let matured = AgeRange::try_new(&|_f: Filter,
|
||||
name: &'static str|
|
||||
-> Result<ValuePerBlockCumulativeRolling> {
|
||||
ValuePerBlockCumulativeRolling::forced_import(
|
||||
let matured_version = v + MATURED_VERSION;
|
||||
let matured_sats =
|
||||
EagerVec::<ColumnarVec<PcoVec<Height, Sats>, AgeRangeId>>::forced_import(
|
||||
db,
|
||||
&format!("{prefix}_{name}_matured_supply"),
|
||||
v,
|
||||
&format!("{prefix}_age_range_matured_supply_cumulative_sats"),
|
||||
matured_version,
|
||||
)?;
|
||||
let matured_cents =
|
||||
EagerVec::<ColumnarVec<PcoVec<Height, Cents>, AgeRangeId>>::forced_import(
|
||||
db,
|
||||
&format!("{prefix}_age_range_matured_supply_cumulative_cents"),
|
||||
matured_version,
|
||||
)?;
|
||||
let matured_sats_ref = matured_sats.read_only_clone();
|
||||
let matured_cents_ref = matured_cents.read_only_clone();
|
||||
let matured = AgeRangeId::series(CohortContext::Utxo, |column, name| {
|
||||
LazyColumnValuePerBlockCumulativeRolling::new(
|
||||
&format!("{name}_matured_supply"),
|
||||
matured_version,
|
||||
&matured_sats_ref,
|
||||
&matured_cents_ref,
|
||||
column,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)
|
||||
})?;
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
all,
|
||||
@@ -320,7 +350,10 @@ impl UTXOCohorts<Rw> {
|
||||
under_amount,
|
||||
over_amount,
|
||||
profitability,
|
||||
age_range_supply,
|
||||
matured,
|
||||
cumulative_matured_sats: matured_sats,
|
||||
cumulative_matured_cents: matured_cents,
|
||||
all_supply_cache,
|
||||
caches: UTXOCohortsTransientState::default(),
|
||||
})
|
||||
@@ -348,20 +381,18 @@ impl UTXOCohorts<Rw> {
|
||||
age_range,
|
||||
..
|
||||
} = self;
|
||||
caches
|
||||
.fenwick
|
||||
.compute_is_sth(&sth.metrics.filter, age_range.iter().map(|v| v.filter()));
|
||||
caches.fenwick.compute_is_sth(&sth.metrics.filter);
|
||||
|
||||
let maps: Vec<_> = age_range
|
||||
let maps: Vec<_> = AgeRangeId::ALL
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, sub)| {
|
||||
.filter_map(|&id| {
|
||||
let sub = id.select(age_range);
|
||||
let state = sub.state.as_ref()?;
|
||||
let map = state.cost_basis_map();
|
||||
if map.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some((map, caches.fenwick.is_sth_at(i)))
|
||||
Some((map, caches.fenwick.is_sth(id)))
|
||||
})
|
||||
.collect();
|
||||
caches.fenwick.bulk_init(maps.into_iter());
|
||||
@@ -377,9 +408,10 @@ impl UTXOCohorts<Rw> {
|
||||
let Self {
|
||||
caches, age_range, ..
|
||||
} = self;
|
||||
for (i, sub) in age_range.iter().enumerate() {
|
||||
for &id in AgeRangeId::ALL {
|
||||
let sub = id.select(age_range);
|
||||
if let Some(state) = sub.state.as_ref() {
|
||||
let is_sth = caches.fenwick.is_sth_at(i);
|
||||
let is_sth = caches.fenwick.is_sth(id);
|
||||
state.for_each_cost_basis_pending(|&price, delta| {
|
||||
caches.fenwick.apply_delta(price, delta, is_sth);
|
||||
});
|
||||
@@ -390,9 +422,49 @@ impl UTXOCohorts<Rw> {
|
||||
/// Push maturation sats to the matured vecs for the given height.
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_maturation(&mut self, matured: &AgeRange<Sats>) {
|
||||
for (v, &sats) in self.matured.iter_mut().zip(matured.iter()) {
|
||||
v.push_block_sats(sats);
|
||||
let mut cumulative = self
|
||||
.cumulative_matured_sats
|
||||
.collect_last()
|
||||
.unwrap_or_default();
|
||||
for column in AgeRangeId::ALL {
|
||||
*column.get_mut(&mut cumulative) += *column.select(matured);
|
||||
}
|
||||
self.cumulative_matured_sats.push(cumulative);
|
||||
}
|
||||
|
||||
fn compute_matured_cents(
|
||||
&mut self,
|
||||
starting_height: Height,
|
||||
prices: &price::Vecs,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let cumulative_sats = self.cumulative_matured_sats.read_only_clone();
|
||||
let mut previous_sats = None;
|
||||
|
||||
self.cumulative_matured_cents.compute_transform2_batched(
|
||||
starting_height,
|
||||
&cumulative_sats,
|
||||
&prices.spot.cents.height,
|
||||
WRITE_INTERVAL,
|
||||
|(height, current_sats, price, target)| {
|
||||
let prior_sats = previous_sats.replace(current_sats).unwrap_or_else(|| {
|
||||
height
|
||||
.decremented()
|
||||
.and_then(|height| cumulative_sats.collect_one(height))
|
||||
.unwrap_or_default()
|
||||
});
|
||||
let mut current_cents = target.collect_last().unwrap_or_default();
|
||||
|
||||
for column in AgeRangeId::ALL {
|
||||
let block_sats = *column.get(¤t_sats) - *column.get(&prior_sats);
|
||||
*column.get_mut(&mut current_cents) += SatsToCents::apply(block_sats, price);
|
||||
}
|
||||
|
||||
(height, current_cents)
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_separate_mut(
|
||||
@@ -574,10 +646,8 @@ impl UTXOCohorts<Rw> {
|
||||
.try_for_each(|v| v.compute_rest_part1(prices, starting_lengths, exit))?;
|
||||
}
|
||||
|
||||
// Compute matured cumulative + cents from sats × price
|
||||
self.matured
|
||||
.par_iter_mut()
|
||||
.try_for_each(|v| v.compute_rest(starting_lengths.height, prices, exit))?;
|
||||
// Compute matured cumulative cents from the cumulative sats matrix × price.
|
||||
self.compute_matured_cents(starting_lengths.height, prices, exit)?;
|
||||
|
||||
// Compute profitability supply cents and realized price
|
||||
self.profitability.compute(prices, starting_lengths, exit)?;
|
||||
@@ -751,11 +821,9 @@ impl UTXOCohorts<Rw> {
|
||||
vecs.extend(v.metrics.collect_all_vecs_mut());
|
||||
}
|
||||
vecs.extend(self.profitability.collect_all_vecs_mut());
|
||||
for v in self.matured.iter_mut() {
|
||||
let inner = &mut v.inner;
|
||||
vecs.push(&mut inner.cumulative.sats.height);
|
||||
vecs.push(&mut inner.cumulative.cents.height);
|
||||
}
|
||||
vecs.extend(self.age_range_supply.stored_vecs_mut());
|
||||
vecs.push(&mut self.cumulative_matured_sats);
|
||||
vecs.push(&mut self.cumulative_matured_cents);
|
||||
vecs.into_par_iter()
|
||||
}
|
||||
|
||||
@@ -768,13 +836,12 @@ impl UTXOCohorts<Rw> {
|
||||
pub(crate) fn min_stateful_len(&self) -> Height {
|
||||
self.iter_separate()
|
||||
.map(|v| Height::from(v.min_stateful_len()))
|
||||
.chain(
|
||||
self.matured
|
||||
.iter()
|
||||
.map(|v| Height::from(v.cumulative.sats.height.len())),
|
||||
)
|
||||
.chain(std::iter::once(Height::from(
|
||||
self.cumulative_matured_sats.len(),
|
||||
)))
|
||||
.min()
|
||||
.unwrap_or_default()
|
||||
.min(Height::from(self.age_range_supply.len()))
|
||||
.min(Height::from(self.profitability.min_stateful_len()))
|
||||
.min(Height::from(self.all.min_stateful_len()))
|
||||
.min(Height::from(self.sth.min_stateful_len()))
|
||||
@@ -828,6 +895,7 @@ impl UTXOCohorts<Rw> {
|
||||
sth,
|
||||
lth,
|
||||
age_range,
|
||||
age_range_supply,
|
||||
..
|
||||
} = self;
|
||||
|
||||
@@ -841,11 +909,19 @@ impl UTXOCohorts<Rw> {
|
||||
let mut sth_ccap = (0u128, 0u128);
|
||||
let mut lth_ccap = (0u128, 0u128);
|
||||
|
||||
for ar in age_range.iter_mut() {
|
||||
let mut total_supply = AgeRangeId::from_fn(|_| Sats::ZERO);
|
||||
let mut supply_in_profit = AgeRangeId::from_fn(|_| Sats::ZERO);
|
||||
let mut supply_in_loss = AgeRangeId::from_fn(|_| Sats::ZERO);
|
||||
|
||||
for &id in AgeRangeId::ALL {
|
||||
let ar = id.select_mut(age_range);
|
||||
if let Some(state) = ar.state.as_mut() {
|
||||
all_acc.add(&state.realized);
|
||||
|
||||
let u = state.compute_unrealized_state(height_price);
|
||||
*id.get_mut(&mut total_supply) = state.supply.value;
|
||||
*id.get_mut(&mut supply_in_profit) = u.supply_in_profit;
|
||||
*id.get_mut(&mut supply_in_loss) = u.supply_in_loss;
|
||||
all_ccap.0 += u.capitalized_cap_in_profit_raw;
|
||||
all_ccap.1 += u.capitalized_cap_in_loss_raw;
|
||||
|
||||
@@ -861,6 +937,8 @@ impl UTXOCohorts<Rw> {
|
||||
}
|
||||
}
|
||||
|
||||
age_range_supply.push(total_supply, supply_in_profit, supply_in_loss);
|
||||
|
||||
let all_capitalized_price = all.metrics.realized.push_accum(&all_acc);
|
||||
sth.metrics.realized.push_accum(&sth_acc);
|
||||
lth.metrics.realized.push_accum(<h_acc);
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
use std::{cmp::Reverse, collections::BinaryHeap, path::Path};
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
collections::{BTreeMap, BinaryHeap},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use brk_cohort::{
|
||||
AGE_RANGE_NAMES, CohortContext, Filtered, PROFITABILITY_RANGE_COUNT, TERM_NAMES, UTXO_ALL_NAME,
|
||||
AgeRangeId, CohortContext, Filtered, PROFITABILITY_RANGE_COUNT, TERM_NAMES, UTXO_ALL_NAME,
|
||||
};
|
||||
use brk_error::Result;
|
||||
use brk_types::{Cents, CentsCompact, Date, Dollars, PartsPerMillion32, Sats, UrpdRaw};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::ColumnId;
|
||||
|
||||
use crate::distribution::metrics::{CostBasis, ProfitabilityMetrics};
|
||||
|
||||
@@ -45,20 +50,16 @@ impl UTXOCohorts {
|
||||
/// consumers can avoid writing and immediately rereading the current day.
|
||||
pub(crate) fn age_range_urpd_entries(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (usize, bool, CentsCompact, Sats)> + '_ {
|
||||
let sth_filter = &self.sth.metrics.filter;
|
||||
self.age_range
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(move |(age, cohort)| {
|
||||
let is_sth = sth_filter.includes(cohort.filter());
|
||||
cohort.state.iter().flat_map(move |state| {
|
||||
state
|
||||
.cost_basis_map()
|
||||
.iter()
|
||||
.map(move |(&price, &sats)| (age, is_sth, rounded_urpd_price(price), sats))
|
||||
})
|
||||
) -> impl Iterator<Item = (AgeRangeId, CentsCompact, Sats)> + '_ {
|
||||
AgeRangeId::ALL.iter().copied().flat_map(move |id| {
|
||||
let cohort = id.select(&self.age_range);
|
||||
cohort.state.iter().flat_map(move |state| {
|
||||
state
|
||||
.cost_basis_map()
|
||||
.iter()
|
||||
.map(move |(&price, &sats)| (id, rounded_urpd_price(price), sats))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Push all Fenwick-derived per-block results: percentiles, density, profitability.
|
||||
@@ -82,12 +83,12 @@ impl UTXOCohorts {
|
||||
fn write_disk_distributions(&mut self, date: Date, states_path: &Path) -> Result<()> {
|
||||
let sth_filter = self.sth.metrics.filter.clone();
|
||||
|
||||
self.age_range
|
||||
AgeRangeId::ALL
|
||||
.iter()
|
||||
.zip(AGE_RANGE_NAMES.iter())
|
||||
.map(|&id| (id, id.select(&self.age_range)))
|
||||
.collect::<Vec<_>>()
|
||||
.into_par_iter()
|
||||
.try_for_each(|(sub, name)| -> Result<()> {
|
||||
.try_for_each(|(id, sub)| -> Result<()> {
|
||||
let Some(state) = sub.state.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
@@ -102,7 +103,7 @@ impl UTXOCohorts {
|
||||
merged.push((rounded, sats));
|
||||
}
|
||||
}
|
||||
let full = CohortContext::Utxo.prefixed(name.id);
|
||||
let full = CohortContext::Utxo.prefixed(id.name().id);
|
||||
UrpdRaw::write(states_path, &full, date, merged.into_iter())
|
||||
})?;
|
||||
|
||||
@@ -173,15 +174,12 @@ fn push_profitability(
|
||||
buckets: &[ProfitabilityRangeResult; PROFITABILITY_RANGE_COUNT],
|
||||
metrics: &mut ProfitabilityMetrics,
|
||||
) {
|
||||
for (i, bucket) in metrics.range.as_array_mut().into_iter().enumerate() {
|
||||
let r = &buckets[i];
|
||||
bucket.push(
|
||||
Sats::from(r.all_sats),
|
||||
Sats::from(r.sth_sats),
|
||||
raw_usd_to_dollars(r.all_usd),
|
||||
raw_usd_to_dollars(r.sth_usd),
|
||||
);
|
||||
}
|
||||
metrics.push_ranges(
|
||||
(*buckets).map(|range| Sats::from(range.all_sats)),
|
||||
(*buckets).map(|range| Sats::from(range.sth_sats)),
|
||||
(*buckets).map(|range| raw_usd_to_dollars(range.all_usd)),
|
||||
(*buckets).map(|range| raw_usd_to_dollars(range.sth_usd)),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -243,7 +241,7 @@ impl MergeTarget {
|
||||
/// K-way merge via BinaryHeap over BTreeMap iterators.
|
||||
/// Only builds merged distribution for disk writes.
|
||||
fn merge_k_way(
|
||||
maps: &[(&std::collections::BTreeMap<CentsCompact, Sats>, bool)],
|
||||
maps: &[(&BTreeMap<CentsCompact, Sats>, bool)],
|
||||
targets: &mut AllSthLth<MergeTarget>,
|
||||
) {
|
||||
let mut iters: Vec<_> = maps
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use brk_cohort::{AGE_BOUNDARIES, AGE_RANGE_COUNT, AgeRange};
|
||||
use brk_cohort::{AGE_BOUNDARIES, AGE_RANGE_IDS, AgeRange};
|
||||
use brk_types::{CostBasisSnapshot, ONE_HOUR_IN_SEC, Sats, Timestamp};
|
||||
use vecdb::{Rw, unlikely};
|
||||
|
||||
@@ -35,17 +35,18 @@ impl UTXOCohorts<Rw> {
|
||||
return AgeRange::default();
|
||||
}
|
||||
|
||||
let mut matured = [Sats::ZERO; AGE_RANGE_COUNT];
|
||||
|
||||
// Get age_range cohort states (indexed 0..AGE_RANGE_COUNT)
|
||||
// Cohort i covers hours [BOUNDARIES[i-1], BOUNDARIES[i])
|
||||
// Cohort 0 covers [0, 1) hours
|
||||
// The final cohort covers [15*365*24, infinity) hours
|
||||
let mut age_cohorts: Vec<_> = self.age_range.iter_mut().map(|v| &mut v.state).collect();
|
||||
let cached = &mut self.caches.tick_tock_cached_positions;
|
||||
let mut matured = AgeRange::default();
|
||||
let Self {
|
||||
age_range, caches, ..
|
||||
} = self;
|
||||
let cached = &mut caches.tick_tock_cached_positions;
|
||||
|
||||
// For each boundary (in hours), find blocks that just crossed it
|
||||
for (boundary_idx, &boundary_hours) in AGE_BOUNDARIES.iter().enumerate() {
|
||||
for (boundary_idx, (&boundary_hours, adjacent)) in AGE_BOUNDARIES
|
||||
.iter()
|
||||
.zip(AGE_RANGE_IDS.windows(2))
|
||||
.enumerate()
|
||||
{
|
||||
let boundary_seconds = (boundary_hours as u32) * ONE_HOUR_IN_SEC;
|
||||
|
||||
// Blocks crossing boundary B have timestamps in (prev - B*HOUR, curr - B*HOUR]
|
||||
@@ -81,18 +82,21 @@ impl UTXOCohorts<Rw> {
|
||||
.map_or(chain_state.len(), |pos| start_idx + pos);
|
||||
|
||||
// Move supply from younger cohort to older cohort
|
||||
let younger = adjacent[0];
|
||||
let older = adjacent[1];
|
||||
let matured_sats = younger.select_mut(&mut matured);
|
||||
for block_state in &chain_state[start_idx..end_idx] {
|
||||
let snapshot = CostBasisSnapshot::from_utxo(block_state.price, &block_state.supply);
|
||||
if let Some(state) = age_cohorts[boundary_idx].as_mut() {
|
||||
if let Some(state) = younger.select_mut(age_range).state.as_mut() {
|
||||
state.decrement_snapshot(&snapshot);
|
||||
}
|
||||
if let Some(state) = age_cohorts[boundary_idx + 1].as_mut() {
|
||||
if let Some(state) = older.select_mut(age_range).state.as_mut() {
|
||||
state.increment_snapshot(&snapshot);
|
||||
}
|
||||
matured[boundary_idx] += block_state.supply.value;
|
||||
*matured_sats += block_state.supply.value;
|
||||
}
|
||||
}
|
||||
|
||||
AgeRange::from_array(matured)
|
||||
matured
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,9 +434,7 @@ pub(crate) fn process_blocks(
|
||||
// Update Fenwick tree from pending deltas (must happen before push_cohort_states drains pending)
|
||||
vecs.utxo_cohorts.update_fenwick_from_pending();
|
||||
|
||||
let activity_totals = state.activity.totals();
|
||||
let active_addr_count =
|
||||
activity_totals.sending + activity_totals.receiving - activity_totals.bidirectional;
|
||||
let active_addr_count = state.activity.active();
|
||||
vecs.addrs.push_height(&state, active_addr_count);
|
||||
|
||||
let is_last_of_day = is_last_of_day[offset];
|
||||
|
||||
@@ -5,7 +5,7 @@ use brk_types::{StoredF32, StoredF64, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{AnyStoredVec, Exit, Rw, StorageMode};
|
||||
|
||||
use crate::internal::{Identity, LazyPerBlock, PerBlock, Windows};
|
||||
use crate::internal::{ColumnarRollingWindows, Identity, LazyPerBlock};
|
||||
|
||||
use crate::{
|
||||
distribution::{
|
||||
@@ -26,7 +26,7 @@ pub struct ActivityFull<M: StorageMode = Rw> {
|
||||
|
||||
pub coinyears_destroyed: LazyPerBlock<StoredF64, StoredF64>,
|
||||
|
||||
pub dormancy: Windows<PerBlock<StoredF32, M>>,
|
||||
pub dormancy: ColumnarRollingWindows<StoredF32, M>,
|
||||
}
|
||||
|
||||
impl ActivityFull {
|
||||
@@ -41,14 +41,12 @@ impl ActivityFull {
|
||||
cfg.indexes,
|
||||
);
|
||||
|
||||
let dormancy = Windows::try_from_fn(|suffix| {
|
||||
PerBlock::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name(&format!("dormancy_{suffix}")),
|
||||
cfg.version + v1,
|
||||
cfg.indexes,
|
||||
)
|
||||
})?;
|
||||
let dormancy = ColumnarRollingWindows::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("dormancy"),
|
||||
cfg.version + v1,
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
@@ -71,9 +69,7 @@ impl ActivityFull {
|
||||
|
||||
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
let mut vecs = self.inner.collect_vecs_mut();
|
||||
for d in self.dormancy.as_mut_array() {
|
||||
vecs.push(&mut d.height);
|
||||
}
|
||||
vecs.push(self.dormancy.stored_mut());
|
||||
vecs
|
||||
}
|
||||
|
||||
@@ -96,28 +92,25 @@ impl ActivityFull {
|
||||
self.inner
|
||||
.compute_rest_part1(prices, starting_lengths, exit)?;
|
||||
|
||||
for ((dormancy, cdd_sum), tv_sum) in self
|
||||
.dormancy
|
||||
.as_mut_array()
|
||||
.into_iter()
|
||||
.zip(self.inner.coindays_destroyed.sum.as_array())
|
||||
.zip(self.inner.minimal.transfer_volume.sum.0.as_array())
|
||||
{
|
||||
dormancy.height.compute_transform2(
|
||||
starting_lengths.height,
|
||||
&cdd_sum.height,
|
||||
&tv_sum.btc.height,
|
||||
|(i, rolling_cdd, rolling_btc, ..)| {
|
||||
let btc = f64::from(rolling_btc);
|
||||
if btc == 0.0 {
|
||||
(i, StoredF32::from(0.0f32))
|
||||
} else {
|
||||
(i, StoredF32::from((f64::from(rolling_cdd) / btc) as f32))
|
||||
}
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
let Self {
|
||||
inner, dormancy, ..
|
||||
} = self;
|
||||
let cdd_sums = &inner.coindays_destroyed.sum;
|
||||
let transfer_volume_sums = &inner.minimal.transfer_volume.sum.0;
|
||||
dormancy.compute_columns2(
|
||||
starting_lengths.height,
|
||||
|window| &window.select(cdd_sums).height,
|
||||
|window| &window.select(transfer_volume_sums).btc.height,
|
||||
|_, rolling_cdd, rolling_btc| {
|
||||
let btc = f64::from(rolling_btc);
|
||||
if btc == 0.0 {
|
||||
StoredF32::from(0.0f32)
|
||||
} else {
|
||||
StoredF32::from((f64::from(rolling_cdd) / btc) as f32)
|
||||
}
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ use vecdb::{AnyStoredVec, Exit, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
distribution::metrics::{
|
||||
ActivityCore, AllSupplyCache, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore,
|
||||
SupplyCore, UnrealizedCore,
|
||||
ActivityCore, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore, SupplyCore,
|
||||
UnrealizedCore,
|
||||
},
|
||||
price,
|
||||
};
|
||||
@@ -43,8 +43,7 @@ impl CohortMetricsBase for BasicCohortMetrics {
|
||||
}
|
||||
|
||||
impl BasicCohortMetrics {
|
||||
pub(crate) fn forced_import(cfg: &ImportConfig, all_supply: &AllSupplyCache) -> Result<Self> {
|
||||
let supply = SupplyCore::forced_import(cfg, all_supply)?;
|
||||
pub(crate) fn forced_import(cfg: &ImportConfig, supply: SupplyCore) -> Result<Self> {
|
||||
let realized = RealizedCore::forced_import(cfg)?;
|
||||
let unrealized = UnrealizedCore::forced_import(cfg, &realized.price.ppm)?;
|
||||
|
||||
|
||||
@@ -26,12 +26,20 @@ pub struct CoreCohortMetrics<M: StorageMode = Rw> {
|
||||
|
||||
impl CoreCohortMetrics {
|
||||
pub(crate) fn forced_import(cfg: &ImportConfig, all_supply: &AllSupplyCache) -> Result<Self> {
|
||||
let supply = SupplyCore::forced_import(cfg, all_supply)?;
|
||||
Self::forced_import_with_supply(cfg, supply)
|
||||
}
|
||||
|
||||
pub(crate) fn forced_import_with_supply(
|
||||
cfg: &ImportConfig,
|
||||
supply: SupplyCore,
|
||||
) -> Result<Self> {
|
||||
let realized = RealizedCore::forced_import(cfg)?;
|
||||
let unrealized = UnrealizedCore::forced_import(cfg, &realized.price.ppm)?;
|
||||
|
||||
Ok(Self {
|
||||
filter: cfg.filter.clone(),
|
||||
supply: Box::new(SupplyCore::forced_import(cfg, all_supply)?),
|
||||
supply: Box::new(supply),
|
||||
outputs: Box::new(OutputsBase::forced_import(cfg)?),
|
||||
activity: Box::new(ActivityCore::forced_import(cfg)?),
|
||||
realized: Box::new(realized),
|
||||
|
||||
@@ -9,8 +9,8 @@ use vecdb::{Exit, Rw, StorageMode};
|
||||
use crate::{
|
||||
distribution::AllChainCache,
|
||||
distribution::metrics::{
|
||||
ActivityFull, AllSupplyCache, CohortMetricsBase, CostBasis, ImportConfig, OutputsBase,
|
||||
RealizedFull, RelativeWithExtended, SupplyCore, UnrealizedFull,
|
||||
ActivityFull, CohortMetricsBase, CostBasis, ImportConfig, OutputsBase, RealizedFull,
|
||||
RelativeWithExtended, SupplyCore, UnrealizedFull,
|
||||
},
|
||||
price,
|
||||
};
|
||||
@@ -69,10 +69,9 @@ impl CohortMetricsBase for ExtendedCohortMetrics {
|
||||
impl ExtendedCohortMetrics {
|
||||
pub(crate) fn forced_import(
|
||||
cfg: &ImportConfig,
|
||||
all_supply: &AllSupplyCache,
|
||||
supply: SupplyCore,
|
||||
all_chain: &AllChainCache,
|
||||
) -> Result<Self> {
|
||||
let supply = SupplyCore::forced_import(cfg, all_supply)?;
|
||||
let realized = RealizedFull::forced_import(cfg, all_chain)?;
|
||||
let unrealized = UnrealizedFull::forced_import(cfg, &realized.price.ppm)?;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
|
||||
use crate::{
|
||||
distribution::AllChainCache,
|
||||
distribution::metrics::{
|
||||
ActivityFull, AdjustedSopr, AllSupplyCache, CohortMetricsBase, ImportConfig, RealizedFull,
|
||||
ActivityFull, AdjustedSopr, CohortMetricsBase, ImportConfig, RealizedFull, SupplyCore,
|
||||
UnrealizedFull,
|
||||
},
|
||||
price,
|
||||
@@ -52,10 +52,10 @@ impl CohortMetricsBase for ExtendedAdjustedCohortMetrics {
|
||||
impl ExtendedAdjustedCohortMetrics {
|
||||
pub(crate) fn forced_import(
|
||||
cfg: &ImportConfig,
|
||||
all_supply: &AllSupplyCache,
|
||||
supply: SupplyCore,
|
||||
all_chain: &AllChainCache,
|
||||
) -> Result<Self> {
|
||||
let inner = ExtendedCohortMetrics::forced_import(cfg, all_supply, all_chain)?;
|
||||
let inner = ExtendedCohortMetrics::forced_import(cfg, supply, all_chain)?;
|
||||
let asopr = AdjustedSopr::forced_import(cfg)?;
|
||||
Ok(Self {
|
||||
inner,
|
||||
|
||||
@@ -9,11 +9,12 @@ use vecdb::{BytesVec, BytesVecValue, CachedBoxedVec, Database, ImportableVec};
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
CachedWindowStartVec, FiatPerBlock, FiatPerBlockCumulativeWithSums, FiatType, NumericValue,
|
||||
PerBlock, PerBlockCumulativeRolling, PercentPerBlock, PercentRollingWindows, Price,
|
||||
CachedWindowStartVec, ColumnarPercentRollingWindows, ColumnarRollingWindows,
|
||||
ColumnarRollingWindowsFrom1w, FiatPerBlock, FiatPerBlockCumulativeWithSums, FiatType,
|
||||
NumericValue, PerBlock, PerBlockCumulativeRolling, PercentPerBlock, Price,
|
||||
PriceWithRatioPerBlock, RatioPerBlock, RollingWindow24hPerBlock, RollingWindows,
|
||||
RollingWindowsFrom1w, SpotValuePerBlock, ValuePerBlock, ValuePerBlockCumulative,
|
||||
ValuePerBlockCumulativeRolling, Windows,
|
||||
SpotValuePerBlock, ValuePerBlock, ValuePerBlockCumulative, ValuePerBlockCumulativeRolling,
|
||||
Windows,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -43,7 +44,7 @@ impl_config_import!(
|
||||
PercentPerBlock<PartsPerMillion32>,
|
||||
PercentPerBlock<PartsPerMillionSigned32>,
|
||||
PercentPerBlock<PartsPerMillionSigned64>,
|
||||
PercentRollingWindows<PartsPerMillion32>,
|
||||
ColumnarPercentRollingWindows<PartsPerMillion32>,
|
||||
Price<PerBlock<Cents>>,
|
||||
);
|
||||
|
||||
@@ -96,6 +97,11 @@ impl<T: NumericValue + JsonSchema> ConfigImport for RollingWindows<T> {
|
||||
Self::forced_import(cfg.db, &cfg.name(suffix), cfg.version + offset, cfg.indexes)
|
||||
}
|
||||
}
|
||||
impl<T: NumericValue + JsonSchema> ConfigImport for ColumnarRollingWindows<T> {
|
||||
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
|
||||
Self::forced_import(cfg.db, &cfg.name(suffix), cfg.version + offset, cfg.indexes)
|
||||
}
|
||||
}
|
||||
impl<T: NumericValue + JsonSchema> ConfigImport for RollingWindow24hPerBlock<T> {
|
||||
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
|
||||
Self::forced_import(cfg.db, &cfg.name(suffix), cfg.version + offset, cfg.indexes)
|
||||
@@ -123,7 +129,7 @@ impl<C: FiatType> ConfigImport for FiatPerBlockCumulativeWithSums<C> {
|
||||
)
|
||||
}
|
||||
}
|
||||
impl<T: NumericValue + JsonSchema> ConfigImport for RollingWindowsFrom1w<T> {
|
||||
impl<T: NumericValue + JsonSchema> ConfigImport for ColumnarRollingWindowsFrom1w<T> {
|
||||
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
|
||||
Self::forced_import(cfg.db, &cfg.name(suffix), cfg.version + offset, cfg.indexes)
|
||||
}
|
||||
|
||||
@@ -89,6 +89,8 @@ impl CostBasis {
|
||||
.len()
|
||||
.min(self.max.cents.height.len())
|
||||
.min(self.supply_density.ppm.height.len())
|
||||
.min(self.per_coin.prices.height.len())
|
||||
.min(self.per_dollar.prices.height.len())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
@@ -130,18 +132,8 @@ impl CostBasis {
|
||||
&mut self.max.cents.height,
|
||||
&mut self.supply_density.ppm.height,
|
||||
];
|
||||
vecs.extend(
|
||||
self.per_coin
|
||||
.vecs
|
||||
.iter_mut()
|
||||
.map(|v| &mut v.cents.height as &mut dyn AnyStoredVec),
|
||||
);
|
||||
vecs.extend(
|
||||
self.per_dollar
|
||||
.vecs
|
||||
.iter_mut()
|
||||
.map(|v| &mut v.cents.height as &mut dyn AnyStoredVec),
|
||||
);
|
||||
vecs.push(self.per_coin.prices.stored_mut());
|
||||
vecs.push(self.per_dollar.prices.stored_mut());
|
||||
vecs
|
||||
}
|
||||
|
||||
|
||||
@@ -1,256 +1,159 @@
|
||||
use brk_cohort::{Loss, Profit, ProfitabilityRange};
|
||||
use brk_cohort::{
|
||||
Loss, PROFITABILITY_RANGE_COUNT, Profit, ProfitabilityId, ProfitabilityRange, ProfitabilityRow,
|
||||
};
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Lengths;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Bitcoin, Cents, Dollars, Height, PartsPerMillionSigned32, Sats, Version};
|
||||
use vecdb::{AnyStoredVec, AnyVec, CachedBoxedVec, Database, Exit, Rw, StorageMode, WritableVec};
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, CachedBoxedVec, ColumnId, ColumnarVec, Database, EagerVec, Exit,
|
||||
ImportableVec, PcoVec, ReadOnlyClone, ReadOnlyColumnarVec, Rw, StorageMode, WritableVec,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
CachedWindowStartVec, PerBlock, RatioPerBlock, SpotValuePerBlock,
|
||||
SpotValuePerBlockWithDeltas, Windows,
|
||||
CachedWindowStartVec, LazyColumnPerBlock, LazyColumnRatioPerBlock,
|
||||
LazyColumnSpotValuePerBlock, LazyColumnSpotValuePerBlockWithDeltas, Windows,
|
||||
},
|
||||
price,
|
||||
};
|
||||
|
||||
#[derive(Traversable)]
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct WithSth<All, Sth = All> {
|
||||
pub all: All,
|
||||
pub sth: Sth,
|
||||
}
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct ProfitabilityBucket<M: StorageMode = Rw> {
|
||||
pub supply: WithSth<SpotValuePerBlockWithDeltas<M>, SpotValuePerBlock<M>>,
|
||||
pub realized_cap: WithSth<PerBlock<Dollars, M>>,
|
||||
pub unrealized_pnl: WithSth<PerBlock<Dollars, M>>,
|
||||
pub nupl: RatioPerBlock<PartsPerMillionSigned32, M>,
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct ProfitabilityBucket {
|
||||
pub supply: WithSth<
|
||||
LazyColumnSpotValuePerBlockWithDeltas<ProfitabilityId>,
|
||||
LazyColumnSpotValuePerBlock<ProfitabilityId>,
|
||||
>,
|
||||
pub realized_cap: WithSth<
|
||||
LazyColumnPerBlock<Dollars, ProfitabilityId>,
|
||||
LazyColumnPerBlock<Dollars, ProfitabilityId>,
|
||||
>,
|
||||
pub unrealized_pnl: WithSth<
|
||||
LazyColumnPerBlock<Dollars, ProfitabilityId>,
|
||||
LazyColumnPerBlock<Dollars, ProfitabilityId>,
|
||||
>,
|
||||
pub nupl: LazyColumnRatioPerBlock<PartsPerMillionSigned32, ProfitabilityId>,
|
||||
}
|
||||
|
||||
impl<M: StorageMode> ProfitabilityBucket<M> {
|
||||
fn min_len(&self) -> usize {
|
||||
self.supply
|
||||
.all
|
||||
.sats
|
||||
.height
|
||||
.len()
|
||||
.min(self.realized_cap.all.height.len())
|
||||
}
|
||||
struct ProfitabilitySources {
|
||||
all_supply_sats: ReadOnlyColumnarVec<PcoVec<Height, Sats>, ProfitabilityId>,
|
||||
sth_supply_sats: ReadOnlyColumnarVec<PcoVec<Height, Sats>, ProfitabilityId>,
|
||||
all_realized_cap: ReadOnlyColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>,
|
||||
sth_realized_cap: ReadOnlyColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>,
|
||||
all_unrealized_pnl: ReadOnlyColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>,
|
||||
sth_unrealized_pnl: ReadOnlyColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>,
|
||||
nupl: ReadOnlyColumnarVec<PcoVec<Height, PartsPerMillionSigned32>, ProfitabilityId>,
|
||||
}
|
||||
|
||||
impl ProfitabilityBucket {
|
||||
fn forced_import(
|
||||
db: &Database,
|
||||
fn new(
|
||||
name: &str,
|
||||
version: Version,
|
||||
column: ProfitabilityId,
|
||||
sources: &ProfitabilitySources,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
) -> Self {
|
||||
Self {
|
||||
supply: WithSth {
|
||||
all: SpotValuePerBlockWithDeltas::forced_import(
|
||||
db,
|
||||
all: LazyColumnSpotValuePerBlockWithDeltas::new(
|
||||
&format!("{name}_supply"),
|
||||
version,
|
||||
&sources.all_supply_sats,
|
||||
column,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
)?,
|
||||
sth: SpotValuePerBlock::forced_import(
|
||||
db,
|
||||
),
|
||||
sth: LazyColumnSpotValuePerBlock::new(
|
||||
&format!("{name}_sth_supply"),
|
||||
version,
|
||||
&sources.sth_supply_sats,
|
||||
column,
|
||||
indexes,
|
||||
spot_price,
|
||||
)?,
|
||||
),
|
||||
},
|
||||
realized_cap: WithSth {
|
||||
all: PerBlock::forced_import(
|
||||
db,
|
||||
all: LazyColumnPerBlock::new(
|
||||
&format!("{name}_realized_cap"),
|
||||
version,
|
||||
&sources.all_realized_cap,
|
||||
column,
|
||||
indexes,
|
||||
)?,
|
||||
sth: PerBlock::forced_import(
|
||||
db,
|
||||
),
|
||||
sth: LazyColumnPerBlock::new(
|
||||
&format!("{name}_sth_realized_cap"),
|
||||
version,
|
||||
&sources.sth_realized_cap,
|
||||
column,
|
||||
indexes,
|
||||
)?,
|
||||
),
|
||||
},
|
||||
unrealized_pnl: WithSth {
|
||||
all: PerBlock::forced_import(
|
||||
db,
|
||||
all: LazyColumnPerBlock::new(
|
||||
&format!("{name}_unrealized_pnl"),
|
||||
version,
|
||||
&sources.all_unrealized_pnl,
|
||||
column,
|
||||
indexes,
|
||||
)?,
|
||||
sth: PerBlock::forced_import(
|
||||
db,
|
||||
),
|
||||
sth: LazyColumnPerBlock::new(
|
||||
&format!("{name}_sth_unrealized_pnl"),
|
||||
version,
|
||||
&sources.sth_unrealized_pnl,
|
||||
column,
|
||||
indexes,
|
||||
)?,
|
||||
),
|
||||
},
|
||||
nupl: RatioPerBlock::forced_import_ppm(
|
||||
db,
|
||||
nupl: LazyColumnRatioPerBlock::new(
|
||||
&format!("{name}_nupl"),
|
||||
version + Version::ONE,
|
||||
version + Version::new(4),
|
||||
&sources.nupl,
|
||||
column,
|
||||
indexes,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push(
|
||||
&mut self,
|
||||
supply: Sats,
|
||||
sth_supply: Sats,
|
||||
realized_cap: Dollars,
|
||||
sth_realized_cap: Dollars,
|
||||
) {
|
||||
self.supply.all.sats.height.push(supply);
|
||||
self.supply.sth.sats.height.push(sth_supply);
|
||||
self.realized_cap.all.height.push(realized_cap);
|
||||
self.realized_cap.sth.height.push(sth_realized_cap);
|
||||
}
|
||||
|
||||
pub(crate) fn compute(
|
||||
&mut self,
|
||||
prices: &price::Vecs,
|
||||
starting_lengths: &Lengths,
|
||||
is_profit: bool,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let max_from = starting_lengths.height;
|
||||
|
||||
self.unrealized_pnl.all.height.compute_transform3(
|
||||
max_from,
|
||||
&prices.spot.cents.height,
|
||||
&self.realized_cap.all.height,
|
||||
&self.supply.all.sats.height,
|
||||
|(i, spot, cap, supply, ..)| {
|
||||
let mv = f64::from(Dollars::from(spot)) * f64::from(Bitcoin::from(supply));
|
||||
let rc = f64::from(cap);
|
||||
let pnl = if is_profit { mv - rc } else { rc - mv }.max(0.0);
|
||||
(i, Dollars::from(pnl))
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
self.unrealized_pnl.sth.height.compute_transform3(
|
||||
max_from,
|
||||
&prices.spot.cents.height,
|
||||
&self.realized_cap.sth.height,
|
||||
&self.supply.sth.sats.height,
|
||||
|(i, spot, cap, supply, ..)| {
|
||||
let mv = f64::from(Dollars::from(spot)) * f64::from(Bitcoin::from(supply));
|
||||
let rc = f64::from(cap);
|
||||
let pnl = if is_profit { mv - rc } else { rc - mv }.max(0.0);
|
||||
(i, Dollars::from(pnl))
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.nupl.ppm.height.compute_transform3(
|
||||
max_from,
|
||||
&prices.spot.cents.height,
|
||||
&self.realized_cap.all.height,
|
||||
&self.supply.all.sats.height,
|
||||
|(i, spot, cap_dollars, supply_sats, ..)| {
|
||||
let p = spot.as_u128();
|
||||
let supply = supply_sats.as_u128();
|
||||
if p == 0 || supply == 0 {
|
||||
(i, PartsPerMillionSigned32::ZERO)
|
||||
} else {
|
||||
let rp = Cents::from(cap_dollars).as_u128() * Sats::ONE_BTC_U128 / supply;
|
||||
let ratio = (p as f64 - rp as f64) / p as f64;
|
||||
(i, PartsPerMillionSigned32::from(ratio))
|
||||
}
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn compute_from_ranges(
|
||||
&mut self,
|
||||
prices: &price::Vecs,
|
||||
starting_lengths: &Lengths,
|
||||
is_profit: bool,
|
||||
sources: &[&ProfitabilityBucket],
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let max_from = starting_lengths.height;
|
||||
|
||||
self.supply.all.sats.height.compute_sum_of_others(
|
||||
max_from,
|
||||
&sources
|
||||
.iter()
|
||||
.map(|s| &s.supply.all.sats.height)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
self.supply.sth.sats.height.compute_sum_of_others(
|
||||
max_from,
|
||||
&sources
|
||||
.iter()
|
||||
.map(|s| &s.supply.sth.sats.height)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
self.realized_cap.all.height.compute_sum_of_others(
|
||||
max_from,
|
||||
&sources
|
||||
.iter()
|
||||
.map(|s| &s.realized_cap.all.height)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
self.realized_cap.sth.height.compute_sum_of_others(
|
||||
max_from,
|
||||
&sources
|
||||
.iter()
|
||||
.map(|s| &s.realized_cap.sth.height)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.compute(prices, starting_lengths, is_profit, exit)
|
||||
}
|
||||
|
||||
pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
vec![
|
||||
&mut self.supply.all.inner.sats.height as &mut dyn AnyStoredVec,
|
||||
&mut self.supply.sth.sats.height,
|
||||
&mut self.realized_cap.all.height,
|
||||
&mut self.realized_cap.sth.height,
|
||||
&mut self.unrealized_pnl.all.height,
|
||||
&mut self.unrealized_pnl.sth.height,
|
||||
&mut self.nupl.ppm.height,
|
||||
]
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// All profitability metrics: 25 ranges + 14 profit thresholds + 9 loss thresholds.
|
||||
#[derive(Traversable)]
|
||||
pub struct ProfitabilityMetrics<M: StorageMode = Rw> {
|
||||
pub range: ProfitabilityRange<ProfitabilityBucket<M>>,
|
||||
pub profit: Profit<ProfitabilityBucket<M>>,
|
||||
pub loss: Loss<ProfitabilityBucket<M>>,
|
||||
pub range: ProfitabilityRange<ProfitabilityBucket>,
|
||||
pub profit: Profit<ProfitabilityBucket>,
|
||||
pub loss: Loss<ProfitabilityBucket>,
|
||||
pub all_supply_sats: M::Stored<EagerVec<ColumnarVec<PcoVec<Height, Sats>, ProfitabilityId>>>,
|
||||
pub sth_supply_sats: M::Stored<EagerVec<ColumnarVec<PcoVec<Height, Sats>, ProfitabilityId>>>,
|
||||
pub all_realized_cap:
|
||||
M::Stored<EagerVec<ColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>>>,
|
||||
pub sth_realized_cap:
|
||||
M::Stored<EagerVec<ColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>>>,
|
||||
pub all_unrealized_pnl:
|
||||
M::Stored<EagerVec<ColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>>>,
|
||||
pub sth_unrealized_pnl:
|
||||
M::Stored<EagerVec<ColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>>>,
|
||||
pub nupl:
|
||||
M::Stored<EagerVec<ColumnarVec<PcoVec<Height, PartsPerMillionSigned32>, ProfitabilityId>>>,
|
||||
}
|
||||
|
||||
impl<M: StorageMode> ProfitabilityMetrics<M> {
|
||||
pub fn iter(&self) -> impl Iterator<Item = &ProfitabilityBucket<M>> {
|
||||
pub fn iter(&self) -> impl Iterator<Item = &ProfitabilityBucket> {
|
||||
self.range
|
||||
.iter()
|
||||
.chain(self.profit.iter())
|
||||
.chain(self.loss.iter())
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut ProfitabilityBucket<M>> {
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut ProfitabilityBucket> {
|
||||
self.range
|
||||
.iter_mut()
|
||||
.chain(self.profit.iter_mut())
|
||||
@@ -258,7 +161,7 @@ impl<M: StorageMode> ProfitabilityMetrics<M> {
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.range.iter().map(|b| b.min_len()).min().unwrap_or(0)
|
||||
self.all_supply_sats.len().min(self.all_realized_cap.len())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,75 +173,274 @@ impl ProfitabilityMetrics {
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let range = ProfitabilityRange::try_new(|name| {
|
||||
ProfitabilityBucket::forced_import(
|
||||
let source_version = version + Version::TWO;
|
||||
let all_supply_sats =
|
||||
EagerVec::<ColumnarVec<PcoVec<Height, Sats>, ProfitabilityId>>::forced_import(
|
||||
db,
|
||||
"profitability_all_supply_sats",
|
||||
source_version,
|
||||
)?;
|
||||
let sth_supply_sats =
|
||||
EagerVec::<ColumnarVec<PcoVec<Height, Sats>, ProfitabilityId>>::forced_import(
|
||||
db,
|
||||
"profitability_sth_supply_sats",
|
||||
source_version,
|
||||
)?;
|
||||
let all_realized_cap =
|
||||
EagerVec::<ColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>>::forced_import(
|
||||
db,
|
||||
"profitability_all_realized_cap",
|
||||
source_version,
|
||||
)?;
|
||||
let sth_realized_cap =
|
||||
EagerVec::<ColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>>::forced_import(
|
||||
db,
|
||||
"profitability_sth_realized_cap",
|
||||
source_version,
|
||||
)?;
|
||||
let all_unrealized_pnl =
|
||||
EagerVec::<ColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>>::forced_import(
|
||||
db,
|
||||
"profitability_all_unrealized_pnl",
|
||||
source_version,
|
||||
)?;
|
||||
let sth_unrealized_pnl =
|
||||
EagerVec::<ColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>>::forced_import(
|
||||
db,
|
||||
"profitability_sth_unrealized_pnl",
|
||||
source_version,
|
||||
)?;
|
||||
let nupl = EagerVec::<ColumnarVec<
|
||||
PcoVec<Height, PartsPerMillionSigned32>,
|
||||
ProfitabilityId,
|
||||
>>::forced_import(
|
||||
db,
|
||||
"profitability_nupl_ppm",
|
||||
source_version + Version::new(4),
|
||||
)?;
|
||||
|
||||
let sources = ProfitabilitySources {
|
||||
all_supply_sats: all_supply_sats.read_only_clone(),
|
||||
sth_supply_sats: sth_supply_sats.read_only_clone(),
|
||||
all_realized_cap: all_realized_cap.read_only_clone(),
|
||||
sth_realized_cap: sth_realized_cap.read_only_clone(),
|
||||
all_unrealized_pnl: all_unrealized_pnl.read_only_clone(),
|
||||
sth_unrealized_pnl: sth_unrealized_pnl.read_only_clone(),
|
||||
nupl: nupl.read_only_clone(),
|
||||
};
|
||||
|
||||
let range = ProfitabilityId::range_series(|column, name| {
|
||||
ProfitabilityBucket::new(
|
||||
name,
|
||||
version,
|
||||
column,
|
||||
&sources,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
)
|
||||
})?;
|
||||
});
|
||||
|
||||
let aggregate_version = version + Version::TWO;
|
||||
|
||||
let profit = Profit::try_new(|name| {
|
||||
ProfitabilityBucket::forced_import(
|
||||
db,
|
||||
let profit = ProfitabilityId::profit_series(|column, name| {
|
||||
ProfitabilityBucket::new(
|
||||
name,
|
||||
aggregate_version,
|
||||
column,
|
||||
&sources,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
)
|
||||
})?;
|
||||
});
|
||||
|
||||
let loss = Loss::try_new(|name| {
|
||||
ProfitabilityBucket::forced_import(
|
||||
db,
|
||||
let loss = ProfitabilityId::loss_series(|column, name| {
|
||||
ProfitabilityBucket::new(
|
||||
name,
|
||||
aggregate_version,
|
||||
column,
|
||||
&sources,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
)
|
||||
})?;
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
range,
|
||||
profit,
|
||||
loss,
|
||||
all_supply_sats,
|
||||
sth_supply_sats,
|
||||
all_realized_cap,
|
||||
sth_realized_cap,
|
||||
all_unrealized_pnl,
|
||||
sth_unrealized_pnl,
|
||||
nupl,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_ranges(
|
||||
&mut self,
|
||||
all_supply_sats: [Sats; PROFITABILITY_RANGE_COUNT],
|
||||
sth_supply_sats: [Sats; PROFITABILITY_RANGE_COUNT],
|
||||
all_realized_cap: [Dollars; PROFITABILITY_RANGE_COUNT],
|
||||
sth_realized_cap: [Dollars; PROFITABILITY_RANGE_COUNT],
|
||||
) {
|
||||
self.all_supply_sats
|
||||
.push(ProfitabilityRow::from_ranges(all_supply_sats));
|
||||
self.sth_supply_sats
|
||||
.push(ProfitabilityRow::from_ranges(sth_supply_sats));
|
||||
self.all_realized_cap
|
||||
.push(ProfitabilityRow::from_ranges(all_realized_cap));
|
||||
self.sth_realized_cap
|
||||
.push(ProfitabilityRow::from_ranges(sth_realized_cap));
|
||||
}
|
||||
|
||||
pub(crate) fn compute(
|
||||
&mut self,
|
||||
prices: &price::Vecs,
|
||||
starting_lengths: &Lengths,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
for (is_profit, bucket) in self.range.iter_mut_with_is_profit() {
|
||||
bucket.compute(prices, starting_lengths, is_profit, exit)?;
|
||||
}
|
||||
let Self {
|
||||
all_supply_sats,
|
||||
sth_supply_sats,
|
||||
all_realized_cap,
|
||||
sth_realized_cap,
|
||||
all_unrealized_pnl,
|
||||
sth_unrealized_pnl,
|
||||
nupl,
|
||||
..
|
||||
} = self;
|
||||
let max_from = starting_lengths.height;
|
||||
|
||||
let range_arr = self.range.as_array();
|
||||
|
||||
for (threshold, sources) in self.profit.iter_mut_with_growing_prefix(&range_arr) {
|
||||
threshold.compute_from_ranges(prices, starting_lengths, true, sources, exit)?;
|
||||
}
|
||||
for (threshold, sources) in self.loss.iter_mut_with_growing_suffix(&range_arr) {
|
||||
threshold.compute_from_ranges(prices, starting_lengths, false, sources, exit)?;
|
||||
}
|
||||
all_unrealized_pnl.compute_transform3(
|
||||
max_from,
|
||||
&prices.spot.cents.height,
|
||||
all_realized_cap,
|
||||
all_supply_sats,
|
||||
|(height, spot, cap, supply, ..)| (height, unrealized_pnl_row(spot, cap, supply)),
|
||||
exit,
|
||||
)?;
|
||||
sth_unrealized_pnl.compute_transform3(
|
||||
max_from,
|
||||
&prices.spot.cents.height,
|
||||
sth_realized_cap,
|
||||
sth_supply_sats,
|
||||
|(height, spot, cap, supply, ..)| (height, unrealized_pnl_row(spot, cap, supply)),
|
||||
exit,
|
||||
)?;
|
||||
nupl.compute_transform3(
|
||||
max_from,
|
||||
&prices.spot.cents.height,
|
||||
all_realized_cap,
|
||||
all_supply_sats,
|
||||
|(height, spot, cap, supply, ..)| (height, nupl_row(spot, cap, supply)),
|
||||
exit,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
let mut vecs = Vec::new();
|
||||
for bucket in self.iter_mut() {
|
||||
vecs.extend(bucket.collect_all_vecs_mut());
|
||||
}
|
||||
vecs
|
||||
pub(crate) fn collect_all_vecs_mut(&mut self) -> [&mut dyn AnyStoredVec; 7] {
|
||||
[
|
||||
&mut self.all_supply_sats as &mut dyn AnyStoredVec,
|
||||
&mut self.sth_supply_sats,
|
||||
&mut self.all_realized_cap,
|
||||
&mut self.sth_realized_cap,
|
||||
&mut self.all_unrealized_pnl,
|
||||
&mut self.sth_unrealized_pnl,
|
||||
&mut self.nupl,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn unrealized_pnl_row(
|
||||
spot: Cents,
|
||||
cap: ProfitabilityRow<Dollars>,
|
||||
supply: ProfitabilityRow<Sats>,
|
||||
) -> ProfitabilityRow<Dollars> {
|
||||
ProfitabilityId::from_fn(|column| {
|
||||
let market_value =
|
||||
f64::from(Dollars::from(spot)) * f64::from(Bitcoin::from(*column.get(&supply)));
|
||||
let realized_cap = f64::from(*column.get(&cap));
|
||||
let pnl = if column.is_profit() {
|
||||
market_value - realized_cap
|
||||
} else {
|
||||
realized_cap - market_value
|
||||
}
|
||||
.max(0.0);
|
||||
Dollars::from(pnl)
|
||||
})
|
||||
}
|
||||
|
||||
fn nupl_row(
|
||||
spot: Cents,
|
||||
cap: ProfitabilityRow<Dollars>,
|
||||
supply: ProfitabilityRow<Sats>,
|
||||
) -> ProfitabilityRow<PartsPerMillionSigned32> {
|
||||
ProfitabilityId::from_fn(|column| {
|
||||
let spot = spot.as_u128();
|
||||
let supply = column.get(&supply).as_u128();
|
||||
if spot == 0 || supply == 0 {
|
||||
PartsPerMillionSigned32::ZERO
|
||||
} else {
|
||||
let realized_price =
|
||||
Cents::from(*column.get(&cap)).as_u128() * Sats::ONE_BTC_U128 / supply;
|
||||
PartsPerMillionSigned32::from((spot as f64 - realized_price as f64) / spot as f64)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use brk_cohort::PROFIT_COUNT;
|
||||
|
||||
#[test]
|
||||
fn expanded_profitability_thresholds_match_prefix_and_suffix_sums() {
|
||||
let ranges = std::array::from_fn(|index| Sats::from(index as u64 + 1));
|
||||
let row = ProfitabilityRow::from_ranges(ranges);
|
||||
let sum = |values: &[Sats]| {
|
||||
values
|
||||
.iter()
|
||||
.copied()
|
||||
.fold(Sats::default(), |total, value| total + value)
|
||||
};
|
||||
|
||||
for (threshold, &column) in ProfitabilityId::profit_ids().iter().enumerate() {
|
||||
assert_eq!(
|
||||
*column.get(&row),
|
||||
sum(&ranges[..PROFIT_COUNT + 1 - threshold])
|
||||
);
|
||||
}
|
||||
for (threshold, &column) in ProfitabilityId::loss_ids().iter().enumerate() {
|
||||
assert_eq!(
|
||||
*column.get(&row),
|
||||
sum(&ranges[PROFIT_COUNT + 1 + threshold..])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derived_rows_preserve_profit_and_loss_polarity() {
|
||||
let supply = ProfitabilityId::from_fn(|_| Sats::ONE_BTC);
|
||||
let cap = ProfitabilityId::from_fn(|column| {
|
||||
Dollars::from(if column.is_profit() { 1.0 } else { 3.0 })
|
||||
});
|
||||
let spot = Cents::from(200_u64);
|
||||
|
||||
let pnl = unrealized_pnl_row(spot, cap.clone(), supply.clone());
|
||||
let nupl = nupl_row(spot, cap, supply);
|
||||
|
||||
for column in ProfitabilityId::ALL {
|
||||
assert_eq!(*column.get(&pnl), Dollars::from(1.0));
|
||||
assert_eq!(
|
||||
*column.get(&nupl),
|
||||
PartsPerMillionSigned32::from(if column.is_profit() { 0.5 } else { -0.5 })
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,16 @@ use brk_error::Result;
|
||||
use brk_indexer::Lengths;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Cents, Height, StoredF64, Version};
|
||||
use vecdb::{Exit, ReadableVec, Rw, StorageMode};
|
||||
use vecdb::{BinaryTransform, Exit, ReadableVec, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
distribution::metrics::ImportConfig,
|
||||
internal::{PerBlockCumulativeRolling, RatioCents64, RollingWindows},
|
||||
internal::{ColumnarRollingWindows, PerBlockCumulativeRolling, RatioCents64},
|
||||
};
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct AdjustedSopr<M: StorageMode = Rw> {
|
||||
pub ratio: RollingWindows<StoredF64, M>,
|
||||
pub ratio: ColumnarRollingWindows<StoredF64, M>,
|
||||
pub transfer_volume: PerBlockCumulativeRolling<Cents, M>,
|
||||
pub value_destroyed: PerBlockCumulativeRolling<Cents, M>,
|
||||
}
|
||||
@@ -48,20 +48,15 @@ impl AdjustedSopr {
|
||||
exit,
|
||||
)?;
|
||||
|
||||
for ((sopr, tv), vd) in self
|
||||
.ratio
|
||||
.as_mut_array()
|
||||
.into_iter()
|
||||
.zip(self.transfer_volume.sum.as_array())
|
||||
.zip(self.value_destroyed.sum.as_array())
|
||||
{
|
||||
sopr.compute_binary::<Cents, Cents, RatioCents64>(
|
||||
starting_lengths.height,
|
||||
&tv.height,
|
||||
&vd.height,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
self.ratio.compute_columns2(
|
||||
starting_lengths.height,
|
||||
|window| &window.select(&self.transfer_volume.sum).height,
|
||||
|window| &window.select(&self.value_destroyed.sum).height,
|
||||
|_, transfer_volume, value_destroyed| {
|
||||
RatioCents64::apply(transfer_volume, value_destroyed)
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,15 +6,18 @@ use brk_types::{
|
||||
PartsPerMillion64, PartsPerMillionSigned64, StoredF64, Version,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{AnyStoredVec, AnyVec, BytesVec, Exit, ReadableVec, Rw, StorageMode, WritableVec};
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, BinaryTransform, BytesVec, Exit, ReadableVec, Rw, StorageMode,
|
||||
WritableVec,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
distribution::AllChainCache,
|
||||
distribution::state::{CohortState, CostBasisData, RealizedState, WithCapital},
|
||||
internal::{
|
||||
ColumnarPercentRollingWindows, ColumnarRollingWindows, ColumnarRollingWindowsFrom1w,
|
||||
FiatPerBlockCumulativeWithSums, LazyPercentPerBlock, PercentPerBlock,
|
||||
PercentRollingWindows, PriceWithRatioPerBlock, RatioCents, RatioCents64,
|
||||
RatioCentsSignedCents, RollingWindows, RollingWindowsFrom1w,
|
||||
PriceWithRatioPerBlock, RatioCents, RatioCents64, RatioCentsSignedCents,
|
||||
ValuePerBlockCumulativeRolling,
|
||||
},
|
||||
price,
|
||||
@@ -35,7 +38,7 @@ pub struct RealizedNetPnl<M: StorageMode = Rw> {
|
||||
#[derive(Traversable)]
|
||||
pub struct RealizedSopr<M: StorageMode = Rw> {
|
||||
#[traversable(rename = "ratio")]
|
||||
pub ratio_extended: RollingWindowsFrom1w<StoredF64, M>,
|
||||
pub ratio_extended: ColumnarRollingWindowsFrom1w<StoredF64, M>,
|
||||
}
|
||||
|
||||
#[derive(Traversable)]
|
||||
@@ -61,13 +64,13 @@ pub struct RealizedFull<M: StorageMode = Rw> {
|
||||
#[traversable(wrap = "cap", rename = "to_own_mcap")]
|
||||
pub cap_to_own_mcap: LazyPercentPerBlock<PartsPerMillion32>,
|
||||
pub gross_pnl: FiatPerBlockCumulativeWithSums<Cents, M>,
|
||||
pub sell_side_risk_ratio: PercentRollingWindows<PartsPerMillion32, M>,
|
||||
pub sell_side_risk_ratio: ColumnarPercentRollingWindows<PartsPerMillion32, M>,
|
||||
pub net_pnl: RealizedNetPnl<M>,
|
||||
pub sopr: RealizedSopr<M>,
|
||||
pub peak_regret: RealizedPeakRegret<M>,
|
||||
pub capitalized: RealizedCapitalized<M>,
|
||||
|
||||
pub profit_to_loss_ratio: RollingWindows<StoredF64, M>,
|
||||
pub profit_to_loss_ratio: ColumnarRollingWindows<StoredF64, M>,
|
||||
|
||||
#[traversable(hidden)]
|
||||
cap_raw: M::Stored<BytesVec<Height, CentsSats>>,
|
||||
@@ -246,21 +249,22 @@ impl RealizedFull {
|
||||
)?;
|
||||
|
||||
// SOPR ratios from lazy rolling sums (1w, 1m, 1y)
|
||||
for ((sopr, vc), vd) in self
|
||||
.sopr
|
||||
.ratio_extended
|
||||
.as_mut_array()
|
||||
.into_iter()
|
||||
.zip(activity_transfer_volume.sum.0.as_array()[1..].iter())
|
||||
.zip(self.core.sopr.value_destroyed.sum.as_array()[1..].iter())
|
||||
{
|
||||
sopr.compute_binary::<Cents, Cents, RatioCents64>(
|
||||
starting_lengths.height,
|
||||
&vc.cents.height,
|
||||
&vd.height,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
self.sopr.ratio_extended.compute_columns2(
|
||||
starting_lengths.height,
|
||||
|window| {
|
||||
&window
|
||||
.select_full(&activity_transfer_volume.sum.0)
|
||||
.cents
|
||||
.height
|
||||
},
|
||||
|window| {
|
||||
&window
|
||||
.select_full(&self.core.sopr.value_destroyed.sum)
|
||||
.height
|
||||
},
|
||||
|_, value_created, value_destroyed| RatioCents64::apply(value_created, value_destroyed),
|
||||
exit,
|
||||
)?;
|
||||
|
||||
// Gross PnL
|
||||
self.gross_pnl.compute_from_cumulative_pair(
|
||||
@@ -281,35 +285,24 @@ impl RealizedFull {
|
||||
exit,
|
||||
)?;
|
||||
// Sell-side risk ratios
|
||||
for (ssrr, rv) in self
|
||||
.sell_side_risk_ratio
|
||||
.as_mut_array()
|
||||
.into_iter()
|
||||
.zip(self.gross_pnl.sum.as_array())
|
||||
{
|
||||
ssrr.compute_binary::<Cents, Cents, RatioCents<PartsPerMillion32>>(
|
||||
starting_lengths.height,
|
||||
&rv.cents.height,
|
||||
&self.core.minimal.cap.cents.height,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
self.sell_side_risk_ratio.compute_columns2(
|
||||
starting_lengths.height,
|
||||
|window| &window.select(&self.gross_pnl.sum).cents.height,
|
||||
|_| &self.core.minimal.cap.cents.height,
|
||||
|_, realized_value, realized_cap| {
|
||||
RatioCents::<PartsPerMillion32>::apply(realized_value, realized_cap)
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
|
||||
// Realized profit to loss ratios
|
||||
for ((ratio, profit), loss) in self
|
||||
.profit_to_loss_ratio
|
||||
.as_mut_array()
|
||||
.into_iter()
|
||||
.zip(self.core.minimal.profit.sum.as_array())
|
||||
.zip(self.core.minimal.loss.sum.as_array())
|
||||
{
|
||||
ratio.compute_binary::<Cents, Cents, RatioCents64>(
|
||||
starting_lengths.height,
|
||||
&profit.cents.height,
|
||||
&loss.cents.height,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
self.profit_to_loss_ratio.compute_columns2(
|
||||
starting_lengths.height,
|
||||
|window| &window.select(&self.core.minimal.profit.sum).cents.height,
|
||||
|window| &window.select(&self.core.minimal.loss.sum).cents.height,
|
||||
|_, profit, loss| RatioCents64::apply(profit, loss),
|
||||
exit,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
use brk_cohort::{AGE_RANGE_IDS, AgeRangeId, Filter};
|
||||
use brk_error::Result;
|
||||
use brk_types::{Height, Sats, Version};
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, ColumnId, ColumnarVec, Database, EagerVec, ImportableVec, PcoVec,
|
||||
ReadOnlyClone, ReadableColumnarVec, Rw, StorageMode, WritableVec,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
distribution::metrics::{AllSupplyCache, ImportConfig, SupplyCore},
|
||||
internal::LazySpotValuePerBlock,
|
||||
};
|
||||
|
||||
type Source<M> = M::Stored<EagerVec<ColumnarVec<PcoVec<Height, Sats>, AgeRangeId>>>;
|
||||
|
||||
pub(crate) struct AgeRangeSupplySources<M: StorageMode = Rw> {
|
||||
total: Source<M>,
|
||||
in_profit: Source<M>,
|
||||
in_loss: Source<M>,
|
||||
}
|
||||
|
||||
impl AgeRangeSupplySources {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
prefix: &str,
|
||||
version: Version,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
total: EagerVec::forced_import(
|
||||
db,
|
||||
&format!("{prefix}_age_range_supply_sats"),
|
||||
version,
|
||||
)?,
|
||||
in_profit: EagerVec::forced_import(
|
||||
db,
|
||||
&format!("{prefix}_age_range_supply_in_profit_sats"),
|
||||
version,
|
||||
)?,
|
||||
in_loss: EagerVec::forced_import(
|
||||
db,
|
||||
&format!("{prefix}_age_range_supply_in_loss_sats"),
|
||||
version,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn all(&self, cfg: &ImportConfig) -> (SupplyCore, AllSupplyCache) {
|
||||
let total_name = cfg.name("supply");
|
||||
let total_source = self.total.read_only_clone().sum_columns(
|
||||
&format!("{total_name}_sats"),
|
||||
cfg.version,
|
||||
AGE_RANGE_IDS,
|
||||
);
|
||||
let all_supply = AllSupplyCache::new(total_source);
|
||||
let total = LazySpotValuePerBlock::from_sats_source(
|
||||
&total_name,
|
||||
cfg.version,
|
||||
all_supply.readable_boxed_clone(),
|
||||
cfg.indexes,
|
||||
cfg.spot_price,
|
||||
);
|
||||
let in_profit =
|
||||
Self::summed_spot(&self.in_profit, cfg, "supply_in_profit", AGE_RANGE_IDS);
|
||||
let in_loss = Self::summed_spot(&self.in_loss, cfg, "supply_in_loss", AGE_RANGE_IDS);
|
||||
|
||||
(
|
||||
SupplyCore::from_lazy_all(cfg, total, in_profit, in_loss),
|
||||
all_supply,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn column(
|
||||
&self,
|
||||
cfg: &ImportConfig,
|
||||
column: AgeRangeId,
|
||||
all_supply: &AllSupplyCache,
|
||||
) -> SupplyCore {
|
||||
SupplyCore::from_lazy(
|
||||
cfg,
|
||||
Self::column_spot(&self.total, cfg, "supply", column),
|
||||
Self::column_spot(&self.in_profit, cfg, "supply_in_profit", column),
|
||||
Self::column_spot(&self.in_loss, cfg, "supply_in_loss", column),
|
||||
all_supply,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn filtered(
|
||||
&self,
|
||||
cfg: &ImportConfig,
|
||||
filter: &Filter,
|
||||
all_supply: &AllSupplyCache,
|
||||
) -> SupplyCore {
|
||||
let columns: Vec<_> = AGE_RANGE_IDS
|
||||
.into_iter()
|
||||
.filter(|column| filter.includes(column.filter()))
|
||||
.collect();
|
||||
|
||||
SupplyCore::from_lazy(
|
||||
cfg,
|
||||
Self::summed_spot(&self.total, cfg, "supply", columns.iter().copied()),
|
||||
Self::summed_spot(
|
||||
&self.in_profit,
|
||||
cfg,
|
||||
"supply_in_profit",
|
||||
columns.iter().copied(),
|
||||
),
|
||||
Self::summed_spot(&self.in_loss, cfg, "supply_in_loss", columns),
|
||||
all_supply,
|
||||
)
|
||||
}
|
||||
|
||||
fn column_spot(
|
||||
source: &EagerVec<ColumnarVec<PcoVec<Height, Sats>, AgeRangeId>>,
|
||||
cfg: &ImportConfig,
|
||||
suffix: &str,
|
||||
column: AgeRangeId,
|
||||
) -> LazySpotValuePerBlock {
|
||||
let name = cfg.name(suffix);
|
||||
let source = source.read_only_clone().column(
|
||||
&format!("{name}_sats"),
|
||||
cfg.version,
|
||||
column,
|
||||
);
|
||||
LazySpotValuePerBlock::from_sats_source(
|
||||
&name,
|
||||
cfg.version,
|
||||
source,
|
||||
cfg.indexes,
|
||||
cfg.spot_price,
|
||||
)
|
||||
}
|
||||
|
||||
fn summed_spot(
|
||||
source: &EagerVec<ColumnarVec<PcoVec<Height, Sats>, AgeRangeId>>,
|
||||
cfg: &ImportConfig,
|
||||
suffix: &str,
|
||||
columns: impl IntoIterator<Item = AgeRangeId>,
|
||||
) -> LazySpotValuePerBlock {
|
||||
let name = cfg.name(suffix);
|
||||
let source = source.read_only_clone().sum_columns(
|
||||
&format!("{name}_sats"),
|
||||
cfg.version,
|
||||
columns,
|
||||
);
|
||||
LazySpotValuePerBlock::from_sats_source(
|
||||
&name,
|
||||
cfg.version,
|
||||
source,
|
||||
cfg.indexes,
|
||||
cfg.spot_price,
|
||||
)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push(
|
||||
&mut self,
|
||||
total: <AgeRangeId as ColumnId>::Row<Sats>,
|
||||
in_profit: <AgeRangeId as ColumnId>::Row<Sats>,
|
||||
in_loss: <AgeRangeId as ColumnId>::Row<Sats>,
|
||||
) {
|
||||
self.total.push(total);
|
||||
self.in_profit.push(in_profit);
|
||||
self.in_loss.push(in_loss);
|
||||
}
|
||||
|
||||
pub(crate) fn stored_vecs_mut(&mut self) -> [&mut dyn AnyStoredVec; 3] {
|
||||
[&mut self.total, &mut self.in_profit, &mut self.in_loss]
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.total
|
||||
.len()
|
||||
.min(self.in_profit.len())
|
||||
.min(self.in_loss.len())
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,44 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_cohort::{AddrTypeId, ByAddrType};
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Cents, Height, Sats, StoredU64, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{
|
||||
AnyStoredVec, CachedBoxedVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec,
|
||||
AnyStoredVec, CachedBoxedVec, Database, Exit, ReadOnlyClone, ReadableVec, Rw, StorageMode,
|
||||
WritableVec,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
distribution::AllChainCache,
|
||||
indexes,
|
||||
internal::{LazySpotValuePerBlock, SpotValuePerBlock},
|
||||
internal::{
|
||||
ColumnarPerBlock, LazyColumnSpotValuePerBlock, LazySpotValuePerBlock, WithAddrTypes,
|
||||
},
|
||||
};
|
||||
|
||||
/// Average amount held per UTXO and per funded address.
|
||||
///
|
||||
/// `utxo = supply / utxo_count`, `addr = supply / funded_addr_count`.
|
||||
#[derive(Traversable)]
|
||||
pub struct AvgAmountMetrics<M: StorageMode = Rw> {
|
||||
pub utxo: SpotValuePerBlock<M>,
|
||||
pub addr: SpotValuePerBlock<M>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct LazyAvgAmountMetrics {
|
||||
pub utxo: LazySpotValuePerBlock,
|
||||
pub addr: LazySpotValuePerBlock,
|
||||
pub struct AvgAmountMetrics<V> {
|
||||
pub utxo: V,
|
||||
pub addr: V,
|
||||
}
|
||||
|
||||
#[derive(Traversable)]
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct AvgAmountVecs<M: StorageMode = Rw> {
|
||||
pub all: LazyAvgAmountMetrics,
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
#[traversable(flatten)]
|
||||
pub by_addr_type: ByAddrType<AvgAmountMetrics<M>>,
|
||||
pub series: WithAddrTypes<
|
||||
AvgAmountMetrics<LazyColumnSpotValuePerBlock<AddrTypeId>>,
|
||||
AvgAmountMetrics<LazySpotValuePerBlock>,
|
||||
>,
|
||||
#[traversable(hidden)]
|
||||
utxo_source: ColumnarPerBlock<Sats, AddrTypeId, (), M>,
|
||||
#[traversable(hidden)]
|
||||
addr_source: ColumnarPerBlock<Sats, AddrTypeId, (), M>,
|
||||
}
|
||||
|
||||
impl AvgAmountVecs {
|
||||
@@ -57,7 +63,7 @@ impl AvgAmountVecs {
|
||||
funded_addr_count,
|
||||
|_, count, supply| supply / count,
|
||||
);
|
||||
let all = LazyAvgAmountMetrics {
|
||||
let all = AvgAmountMetrics {
|
||||
utxo: LazySpotValuePerBlock::from_sats_source(
|
||||
"avg_utxo_amount",
|
||||
version,
|
||||
@@ -73,92 +79,74 @@ impl AvgAmountVecs {
|
||||
spot_price,
|
||||
),
|
||||
};
|
||||
let by_addr_type = ByAddrType::new_with_name(|type_name| {
|
||||
AvgAmountMetrics::forced_import(db, type_name, version, indexes, spot_price)
|
||||
})?;
|
||||
|
||||
Ok(Self { all, by_addr_type })
|
||||
let utxo_source =
|
||||
ColumnarPerBlock::forced_import(db, "avg_utxo_amount_sats_by_type", version, |_| ())?;
|
||||
let addr_source =
|
||||
ColumnarPerBlock::forced_import(db, "avg_addr_amount_sats_by_type", version, |_| ())?;
|
||||
let utxo = utxo_source.height.read_only_clone();
|
||||
let addr = addr_source.height.read_only_clone();
|
||||
let by_addr_type = AddrTypeId::series(|column, type_name| AvgAmountMetrics {
|
||||
utxo: LazyColumnSpotValuePerBlock::new(
|
||||
&format!("{type_name}_avg_utxo_amount"),
|
||||
version,
|
||||
&utxo,
|
||||
column,
|
||||
indexes,
|
||||
spot_price,
|
||||
),
|
||||
addr: LazyColumnSpotValuePerBlock::new(
|
||||
&format!("{type_name}_avg_addr_amount"),
|
||||
version,
|
||||
&addr,
|
||||
column,
|
||||
indexes,
|
||||
spot_price,
|
||||
),
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
series: WithAddrTypes { all, by_addr_type },
|
||||
utxo_source,
|
||||
addr_source,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
self.by_addr_type
|
||||
.par_values_mut()
|
||||
.flat_map_iter(AvgAmountMetrics::collect_vecs_mut)
|
||||
rayon::iter::once(self.utxo_source.stored_mut())
|
||||
.chain(rayon::iter::once(self.addr_source.stored_mut()))
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
for metrics in self.by_addr_type.values_mut() {
|
||||
metrics.reset_height()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl AvgAmountMetrics {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
prefix: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let name = |suffix: &str| {
|
||||
if prefix.is_empty() {
|
||||
suffix.to_string()
|
||||
} else {
|
||||
format!("{prefix}_{suffix}")
|
||||
}
|
||||
};
|
||||
Ok(Self {
|
||||
utxo: SpotValuePerBlock::forced_import(
|
||||
db,
|
||||
&name("avg_utxo_amount"),
|
||||
version,
|
||||
indexes,
|
||||
spot_price,
|
||||
)?,
|
||||
addr: SpotValuePerBlock::forced_import(
|
||||
db,
|
||||
&name("avg_addr_amount"),
|
||||
version,
|
||||
indexes,
|
||||
spot_price,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
vec![
|
||||
&mut self.utxo.sats.height as &mut dyn AnyStoredVec,
|
||||
&mut self.addr.sats.height,
|
||||
]
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.utxo.sats.height.reset()?;
|
||||
self.addr.sats.height.reset()?;
|
||||
self.utxo_source.height.reset()?;
|
||||
self.addr_source.height.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn compute(
|
||||
&mut self,
|
||||
supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
utxo_count: &impl ReadableVec<Height, StoredU64>,
|
||||
funded_addr_count: &impl ReadableVec<Height, StoredU64>,
|
||||
supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
|
||||
utxo_count: &ByAddrType<&impl ReadableVec<Height, StoredU64>>,
|
||||
funded_addr_count: &ByAddrType<&impl ReadableVec<Height, StoredU64>>,
|
||||
max_from: Height,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.utxo
|
||||
.sats
|
||||
.height
|
||||
.compute_divide(max_from, supply_sats, utxo_count, exit)?;
|
||||
|
||||
self.addr
|
||||
.sats
|
||||
.height
|
||||
.compute_divide(max_from, supply_sats, funded_addr_count, exit)?;
|
||||
self.utxo_source.compute_columns2(
|
||||
max_from,
|
||||
|column| *column.select(supply_sats),
|
||||
|column| *column.select(utxo_count),
|
||||
|_, supply, count| supply / count,
|
||||
exit,
|
||||
)?;
|
||||
self.addr_source.compute_columns2(
|
||||
max_from,
|
||||
|column| *column.select(supply_sats),
|
||||
|column| *column.select(funded_addr_count),
|
||||
|_, supply, count| supply / count,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,14 +3,15 @@ use brk_indexer::Lengths;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, PartsPerMillion32, PartsPerMillionSigned64, Sats, SatsSigned, Version};
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, BinaryTransform, Exit, ReadableCloneableVec, Rw, StorageMode, WritableVec,
|
||||
AnyStoredVec, AnyVec, BinaryTransform, EagerVec, Exit, ImportableVec, PcoVec, ReadOnlyClone,
|
||||
ReadableCloneableVec, Rw, StorageMode, WritableVec,
|
||||
};
|
||||
|
||||
use crate::distribution::state::{CohortState, CostBasisOps, RealizedOps};
|
||||
|
||||
use crate::internal::{
|
||||
LazyIndexedVec, LazyPercentPerBlock, LazyRollingDeltasAmountFromHeight, RatioSats,
|
||||
SpotValuePerBlock,
|
||||
LazyIndexedVec, LazyPercentPerBlock, LazyRollingDeltasAmountFromHeight, LazySpotValuePerBlock,
|
||||
RatioSats,
|
||||
};
|
||||
|
||||
use crate::distribution::metrics::{AllSupplyCache, ImportConfig};
|
||||
@@ -18,15 +19,49 @@ use crate::distribution::metrics::{AllSupplyCache, ImportConfig};
|
||||
/// Base supply metrics: total supply + dominance (share of circulating).
|
||||
#[derive(Traversable)]
|
||||
pub struct SupplyBase<M: StorageMode = Rw> {
|
||||
pub total: SpotValuePerBlock<M>,
|
||||
pub total: LazySpotValuePerBlock,
|
||||
pub delta: LazyRollingDeltasAmountFromHeight<Sats, SatsSigned, PartsPerMillionSigned64>,
|
||||
#[traversable(rename = "dominance")]
|
||||
pub dominance: LazyPercentPerBlock<PartsPerMillion32>,
|
||||
#[traversable(hidden)]
|
||||
source: Option<M::Stored<EagerVec<PcoVec<Height, Sats>>>>,
|
||||
}
|
||||
|
||||
impl SupplyBase {
|
||||
pub(crate) fn forced_import(cfg: &ImportConfig, all_supply: &AllSupplyCache) -> Result<Self> {
|
||||
let supply: SpotValuePerBlock = cfg.import("supply", Version::ZERO)?;
|
||||
let name = cfg.name("supply");
|
||||
let sats_source =
|
||||
EagerVec::forced_import(cfg.db, &format!("{name}_sats"), cfg.version)?;
|
||||
let supply = LazySpotValuePerBlock::from_sats_source(
|
||||
&name,
|
||||
cfg.version,
|
||||
sats_source.read_only_clone(),
|
||||
cfg.indexes,
|
||||
cfg.spot_price,
|
||||
);
|
||||
let name = cfg.name("supply_dominance");
|
||||
let dominance_source = LazyIndexedVec::new(
|
||||
&format!("{name}_ppm_source"),
|
||||
cfg.version,
|
||||
supply.sats.height.read_only_boxed_clone(),
|
||||
all_supply.cached_boxed_clone(),
|
||||
|_, supply, all_supply| RatioSats::<PartsPerMillion32>::apply(supply, all_supply),
|
||||
);
|
||||
let dominance = LazyPercentPerBlock::from_height_source(
|
||||
&name,
|
||||
cfg.version,
|
||||
dominance_source,
|
||||
cfg.indexes,
|
||||
);
|
||||
|
||||
Ok(Self::new(cfg, supply, dominance, Some(sats_source)))
|
||||
}
|
||||
|
||||
pub(crate) fn from_lazy(
|
||||
cfg: &ImportConfig,
|
||||
supply: LazySpotValuePerBlock,
|
||||
all_supply: &AllSupplyCache,
|
||||
) -> Self {
|
||||
let name = cfg.name("supply_dominance");
|
||||
let source = LazyIndexedVec::new(
|
||||
&format!("{name}_ppm_source"),
|
||||
@@ -38,11 +73,10 @@ impl SupplyBase {
|
||||
let dominance =
|
||||
LazyPercentPerBlock::from_height_source(&name, cfg.version, source, cfg.indexes);
|
||||
|
||||
Ok(Self::new(cfg, supply, dominance))
|
||||
Self::new(cfg, supply, dominance, None)
|
||||
}
|
||||
|
||||
pub(crate) fn forced_import_all(cfg: &ImportConfig) -> Result<Self> {
|
||||
let supply: SpotValuePerBlock = cfg.import("supply", Version::ZERO)?;
|
||||
pub(crate) fn from_lazy_all(cfg: &ImportConfig, supply: LazySpotValuePerBlock) -> Self {
|
||||
let dominance = LazyPercentPerBlock::from_indexed_source(
|
||||
&cfg.name("supply_dominance"),
|
||||
cfg.version,
|
||||
@@ -51,7 +85,7 @@ impl SupplyBase {
|
||||
cfg.indexes,
|
||||
);
|
||||
|
||||
Ok(Self::new(cfg, supply, dominance))
|
||||
Self::new(cfg, supply, dominance, None)
|
||||
}
|
||||
|
||||
fn all_dominance(_height: Height, supply: Sats) -> PartsPerMillion32 {
|
||||
@@ -60,8 +94,9 @@ impl SupplyBase {
|
||||
|
||||
fn new(
|
||||
cfg: &ImportConfig,
|
||||
supply: SpotValuePerBlock,
|
||||
supply: LazySpotValuePerBlock,
|
||||
dominance: LazyPercentPerBlock<PartsPerMillion32>,
|
||||
source: Option<EagerVec<PcoVec<Height, Sats>>>,
|
||||
) -> Self {
|
||||
let delta = LazyRollingDeltasAmountFromHeight::new(
|
||||
&cfg.name("supply_delta"),
|
||||
@@ -75,6 +110,7 @@ impl SupplyBase {
|
||||
total: supply,
|
||||
delta,
|
||||
dominance,
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,11 +120,16 @@ impl SupplyBase {
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_state(&mut self, state: &CohortState<impl RealizedOps, impl CostBasisOps>) {
|
||||
self.total.sats.height.push(state.supply.value);
|
||||
if let Some(source) = self.source.as_mut() {
|
||||
source.push(state.supply.value);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
vec![&mut self.total.sats.height as &mut dyn AnyStoredVec]
|
||||
self.source
|
||||
.iter_mut()
|
||||
.map(|source| source as &mut dyn AnyStoredVec)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn compute_from_stateful(
|
||||
@@ -97,14 +138,16 @@ impl SupplyBase {
|
||||
others: &[&Self],
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.total.sats.height.compute_sum_of_others(
|
||||
starting_lengths.height,
|
||||
&others
|
||||
.iter()
|
||||
.map(|v| &v.total.sats.height)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
if let Some(source) = self.source.as_mut() {
|
||||
source.compute_sum_of_others(
|
||||
starting_lengths.height,
|
||||
&others
|
||||
.iter()
|
||||
.map(|v| &v.total.sats.height)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use brk_types::{Height, Sats};
|
||||
use vecdb::{
|
||||
CachedBoxedVec, CachedReadableVec, CachedVec, EagerVec, PcoVec, PcodecStrategy, ReadOnlyClone,
|
||||
ReadOnlyCompressedVec,
|
||||
CachedBoxedVec, CachedReadableVec, CachedVec, ReadableBoxedVec, ReadableCloneableVec,
|
||||
ReadableVec, TypedVec,
|
||||
};
|
||||
|
||||
/// Pinned in-memory snapshot of the all-cohort supply.
|
||||
@@ -10,20 +10,35 @@ use vecdb::{
|
||||
/// global cache budget because evicting it would make each lazy read hit disk.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AllSupplyCache {
|
||||
cache: CachedVec<ReadOnlyCompressedVec<Height, Sats, PcodecStrategy<Sats>>>,
|
||||
cache: CachedBoxedVec<Height, Sats>,
|
||||
source: ReadableBoxedVec<Height, Sats>,
|
||||
}
|
||||
|
||||
impl AllSupplyCache {
|
||||
pub(crate) fn new(source: &EagerVec<PcoVec<Height, Sats>>) -> Self {
|
||||
Self {
|
||||
cache: CachedVec::wrap(source.read_only_clone()),
|
||||
}
|
||||
pub(crate) fn new<V>(source: V) -> Self
|
||||
where
|
||||
V: TypedVec<I = Height, T = Sats>
|
||||
+ ReadableVec<Height, Sats>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
let cache = CachedVec::wrap(source);
|
||||
let source = ReadableCloneableVec::read_only_boxed_clone(&cache);
|
||||
let cache = cache.cached_boxed_clone();
|
||||
|
||||
Self { cache, source }
|
||||
}
|
||||
|
||||
pub(crate) fn cached_boxed_clone(&self) -> CachedBoxedVec<Height, Sats> {
|
||||
self.cache.cached_boxed_clone()
|
||||
}
|
||||
|
||||
pub(crate) fn readable_boxed_clone(&self) -> ReadableBoxedVec<Height, Sats> {
|
||||
self.source.read_only_boxed_clone()
|
||||
}
|
||||
|
||||
pub(crate) fn clear(&self) {
|
||||
self.cache.clear();
|
||||
}
|
||||
@@ -32,7 +47,7 @@ impl AllSupplyCache {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use brk_types::Version;
|
||||
use vecdb::{AnyStoredVec, Database, ImportableVec, WritableVec};
|
||||
use vecdb::{AnyStoredVec, Database, EagerVec, ImportableVec, PcoVec, ReadOnlyClone, WritableVec};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -54,7 +69,7 @@ mod tests {
|
||||
source.push(Sats::new(20));
|
||||
source.write().unwrap();
|
||||
|
||||
let cache = AllSupplyCache::new(&source);
|
||||
let cache = AllSupplyCache::new(source.read_only_clone());
|
||||
let reader = cache.cached_boxed_clone();
|
||||
assert_eq!(&*reader.cached(), &[Sats::new(10), Sats::new(20)]);
|
||||
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Lengths;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::Version;
|
||||
use brk_types::{Height, Sats, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, EagerVec, Exit, ImportableVec, PcoVec, ReadOnlyClone, Rw, StorageMode,
|
||||
WritableVec,
|
||||
};
|
||||
|
||||
use crate::distribution::state::UnrealizedState;
|
||||
|
||||
use crate::internal::{
|
||||
HalveCents, HalveDollars, HalveSats, HalveSatsToBitcoin, LazyValuePerBlock, SpotValuePerBlock,
|
||||
HalveCents, HalveDollars, HalveSats, HalveSatsToBitcoin, LazySpotValuePerBlock,
|
||||
LazyValuePerBlock,
|
||||
};
|
||||
|
||||
use crate::distribution::metrics::{AllSupplyCache, ImportConfig};
|
||||
|
||||
use super::SupplyBase;
|
||||
|
||||
/// Core supply metrics: total + halved + in_profit/in_loss (4 stored vecs).
|
||||
/// Core supply metrics: total + halved + in_profit/in_loss.
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct SupplyCore<M: StorageMode = Rw> {
|
||||
#[deref]
|
||||
@@ -24,22 +28,86 @@ pub struct SupplyCore<M: StorageMode = Rw> {
|
||||
pub base: SupplyBase<M>,
|
||||
|
||||
pub half: LazyValuePerBlock,
|
||||
pub in_profit: SpotValuePerBlock<M>,
|
||||
pub in_loss: SpotValuePerBlock<M>,
|
||||
pub in_profit: LazySpotValuePerBlock,
|
||||
pub in_loss: LazySpotValuePerBlock,
|
||||
#[traversable(hidden)]
|
||||
in_profit_source: Option<M::Stored<EagerVec<PcoVec<Height, Sats>>>>,
|
||||
#[traversable(hidden)]
|
||||
in_loss_source: Option<M::Stored<EagerVec<PcoVec<Height, Sats>>>>,
|
||||
}
|
||||
|
||||
impl SupplyCore {
|
||||
pub(crate) fn forced_import(cfg: &ImportConfig, all_supply: &AllSupplyCache) -> Result<Self> {
|
||||
Self::forced_import_with_base(cfg, SupplyBase::forced_import(cfg, all_supply)?)
|
||||
let base = SupplyBase::forced_import(cfg, all_supply)?;
|
||||
let (in_profit, in_profit_source) = Self::import_spot(cfg, "supply_in_profit")?;
|
||||
let (in_loss, in_loss_source) = Self::import_spot(cfg, "supply_in_loss")?;
|
||||
Ok(Self::new(
|
||||
cfg,
|
||||
base,
|
||||
in_profit,
|
||||
in_loss,
|
||||
Some(in_profit_source),
|
||||
Some(in_loss_source),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn forced_import_all(cfg: &ImportConfig) -> Result<Self> {
|
||||
Self::forced_import_with_base(cfg, SupplyBase::forced_import_all(cfg)?)
|
||||
pub(crate) fn from_lazy(
|
||||
cfg: &ImportConfig,
|
||||
total: LazySpotValuePerBlock,
|
||||
in_profit: LazySpotValuePerBlock,
|
||||
in_loss: LazySpotValuePerBlock,
|
||||
all_supply: &AllSupplyCache,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
cfg,
|
||||
SupplyBase::from_lazy(cfg, total, all_supply),
|
||||
in_profit,
|
||||
in_loss,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn forced_import_with_base(cfg: &ImportConfig, base: SupplyBase) -> Result<Self> {
|
||||
let v0 = Version::ZERO;
|
||||
pub(crate) fn from_lazy_all(
|
||||
cfg: &ImportConfig,
|
||||
total: LazySpotValuePerBlock,
|
||||
in_profit: LazySpotValuePerBlock,
|
||||
in_loss: LazySpotValuePerBlock,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
cfg,
|
||||
SupplyBase::from_lazy_all(cfg, total),
|
||||
in_profit,
|
||||
in_loss,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn import_spot(
|
||||
cfg: &ImportConfig,
|
||||
suffix: &str,
|
||||
) -> Result<(LazySpotValuePerBlock, EagerVec<PcoVec<Height, Sats>>)> {
|
||||
let name = cfg.name(suffix);
|
||||
let source = EagerVec::forced_import(cfg.db, &format!("{name}_sats"), cfg.version)?;
|
||||
let series = LazySpotValuePerBlock::from_sats_source(
|
||||
&name,
|
||||
cfg.version,
|
||||
source.read_only_clone(),
|
||||
cfg.indexes,
|
||||
cfg.spot_price,
|
||||
);
|
||||
Ok((series, source))
|
||||
}
|
||||
|
||||
fn new(
|
||||
cfg: &ImportConfig,
|
||||
base: SupplyBase,
|
||||
in_profit: LazySpotValuePerBlock,
|
||||
in_loss: LazySpotValuePerBlock,
|
||||
in_profit_source: Option<EagerVec<PcoVec<Height, Sats>>>,
|
||||
in_loss_source: Option<EagerVec<PcoVec<Height, Sats>>>,
|
||||
) -> Self {
|
||||
let half = LazyValuePerBlock::from_spot_block_source::<
|
||||
HalveSats,
|
||||
HalveSatsToBitcoin,
|
||||
@@ -47,12 +115,14 @@ impl SupplyCore {
|
||||
HalveDollars,
|
||||
>(&cfg.name("supply_half"), &base.total, cfg.version);
|
||||
|
||||
Ok(Self {
|
||||
Self {
|
||||
base,
|
||||
half,
|
||||
in_profit: cfg.import("supply_in_profit", v0)?,
|
||||
in_loss: cfg.import("supply_in_loss", v0)?,
|
||||
})
|
||||
in_profit,
|
||||
in_loss,
|
||||
in_profit_source,
|
||||
in_loss_source,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn min_len(&self) -> usize {
|
||||
@@ -64,14 +134,26 @@ impl SupplyCore {
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_profitability(&mut self, state: &UnrealizedState) {
|
||||
self.in_profit.sats.height.push(state.supply_in_profit);
|
||||
self.in_loss.sats.height.push(state.supply_in_loss);
|
||||
if let Some(source) = self.in_profit_source.as_mut() {
|
||||
source.push(state.supply_in_profit);
|
||||
}
|
||||
if let Some(source) = self.in_loss_source.as_mut() {
|
||||
source.push(state.supply_in_loss);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
let mut vecs = self.base.collect_vecs_mut();
|
||||
vecs.push(&mut self.in_profit.sats.height as &mut dyn AnyStoredVec);
|
||||
vecs.push(&mut self.in_loss.sats.height);
|
||||
vecs.extend(
|
||||
self.in_profit_source
|
||||
.iter_mut()
|
||||
.map(|source| source as &mut dyn AnyStoredVec),
|
||||
);
|
||||
vecs.extend(
|
||||
self.in_loss_source
|
||||
.iter_mut()
|
||||
.map(|source| source as &mut dyn AnyStoredVec),
|
||||
);
|
||||
vecs
|
||||
}
|
||||
|
||||
@@ -88,8 +170,26 @@ impl SupplyCore {
|
||||
let base_refs: Vec<&SupplyBase> = others.iter().map(|o| &o.base).collect();
|
||||
self.base
|
||||
.compute_from_stateful(starting_lengths, &base_refs, exit)?;
|
||||
sum_others!(self, starting_lengths, others, exit; in_profit.sats.height);
|
||||
sum_others!(self, starting_lengths, others, exit; in_loss.sats.height);
|
||||
if let Some(source) = self.in_profit_source.as_mut() {
|
||||
source.compute_sum_of_others(
|
||||
starting_lengths.height,
|
||||
&others
|
||||
.iter()
|
||||
.map(|other| &other.in_profit.sats.height)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
if let Some(source) = self.in_loss_source.as_mut() {
|
||||
source.compute_sum_of_others(
|
||||
starting_lengths.height,
|
||||
&others
|
||||
.iter()
|
||||
.map(|other| &other.in_loss.sats.height)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
mod age_range;
|
||||
mod avg_amount;
|
||||
mod base;
|
||||
mod cache;
|
||||
mod core;
|
||||
|
||||
pub(crate) use age_range::AgeRangeSupplySources;
|
||||
pub use self::core::SupplyCore;
|
||||
pub use avg_amount::AvgAmountVecs;
|
||||
pub use base::SupplyBase;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use brk_cohort::{ByAddrType, EntryPrice, Filter};
|
||||
use brk_cohort::{AddrTypeId, EntryPrice};
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_traversable::Traversable;
|
||||
@@ -65,6 +65,7 @@ impl AddrMetricsVecs {
|
||||
self.funded.reset_height()?;
|
||||
self.empty.reset_height()?;
|
||||
self.activity.reset_height()?;
|
||||
self.total.reset_height()?;
|
||||
self.reused.reset_height()?;
|
||||
self.respent.reset_height()?;
|
||||
self.exposed.reset_height()?;
|
||||
@@ -91,9 +92,9 @@ impl AddrMetricsVecs {
|
||||
.par_iter_height_mut()
|
||||
.chain(self.empty.par_iter_height_mut())
|
||||
.chain(self.activity.par_iter_height_mut())
|
||||
.chain(self.reused.par_iter_height_mut())
|
||||
.chain(self.respent.par_iter_height_mut())
|
||||
.chain(self.exposed.par_iter_height_mut())
|
||||
.chain(self.reused.par_iter_stateful_height_mut())
|
||||
.chain(self.respent.par_iter_stateful_height_mut())
|
||||
.chain(self.exposed.par_iter_stateful_height_mut())
|
||||
}
|
||||
|
||||
/// All height-indexed vecs including derived (`avg_amount`). Used for
|
||||
@@ -105,6 +106,7 @@ impl AddrMetricsVecs {
|
||||
.par_iter_height_mut()
|
||||
.chain(self.empty.par_iter_height_mut())
|
||||
.chain(self.activity.par_iter_height_mut())
|
||||
.chain(self.total.par_iter_height_mut())
|
||||
.chain(self.reused.par_iter_height_mut())
|
||||
.chain(self.respent.par_iter_height_mut())
|
||||
.chain(self.exposed.par_iter_height_mut())
|
||||
@@ -660,15 +662,17 @@ impl Vecs {
|
||||
r2?;
|
||||
}
|
||||
|
||||
// 6b. Compute address count sum (by addr_type -> all)
|
||||
self.addrs.funded.compute_rest(&starting_lengths, exit)?;
|
||||
self.addrs.empty.compute_rest(&starting_lengths, exit)?;
|
||||
// 6b. Compute address metrics derived from stored per-type sources.
|
||||
let t = &self.utxo_cohorts.type_;
|
||||
let type_supply_sats = ByAddrType::new(|filter| {
|
||||
let Filter::Type(ot) = filter else {
|
||||
unreachable!()
|
||||
};
|
||||
&t.get(ot).metrics.supply.total.sats.height
|
||||
let type_supply_sats = AddrTypeId::series(|column, _| {
|
||||
&t.get(column.output_type()).metrics.supply.total.sats.height
|
||||
});
|
||||
let type_utxo_counts = AddrTypeId::series(|column, _| {
|
||||
&t.get(column.output_type())
|
||||
.metrics
|
||||
.outputs
|
||||
.unspent_count
|
||||
.height
|
||||
});
|
||||
self.addrs
|
||||
.reused
|
||||
@@ -680,23 +684,15 @@ impl Vecs {
|
||||
.exposed
|
||||
.compute_rest(&starting_lengths, &type_supply_sats, exit)?;
|
||||
|
||||
// Average amount (supply / utxo_count, supply / funded_addr_count) for `all` and per addr type.
|
||||
for ((ot, avg), (_, funded)) in self
|
||||
.addrs
|
||||
.avg_amount
|
||||
.by_addr_type
|
||||
.iter_mut()
|
||||
.zip(self.addrs.funded.by_addr_type.iter())
|
||||
{
|
||||
let type_m = &t.get(ot).metrics;
|
||||
avg.compute(
|
||||
&type_m.supply.total.sats.height,
|
||||
&type_m.outputs.unspent_count.height,
|
||||
&funded.height,
|
||||
starting_lengths.height,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
let type_funded_addr_counts =
|
||||
AddrTypeId::series(|column, _| &column.select(&self.addrs.funded.by_addr_type).height);
|
||||
self.addrs.avg_amount.compute(
|
||||
&type_supply_sats,
|
||||
&type_utxo_counts,
|
||||
&type_funded_addr_counts,
|
||||
starting_lengths.height,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
// 6c. Compute total_addr_count = addr_count + empty_addr_count
|
||||
self.addrs.total.compute(
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
use brk_error::Result;
|
||||
use brk_indexer::{Indexer, Lengths};
|
||||
use brk_types::{Bitcoin, Cents, Height, Sats, StoredF64, Timestamp, Version};
|
||||
use vecdb::{AnyStoredVec, Exit, ReadableVec, WritableVec};
|
||||
use vecdb::{AnyStoredVec, ColumnId, Exit, ReadableVec, WritableVec};
|
||||
|
||||
use brk_cohort::{AGE_RANGE_FILTERS, ByTerm, TERM_FILTERS};
|
||||
use brk_cohort::{AgeRange, AgeRangeId, ByTerm, TERM_FILTERS};
|
||||
|
||||
use super::super::cointime;
|
||||
use super::{
|
||||
AGE_COHORT_COUNT, AgeBand, AggregateVecs, CohortVecs, HORIZON_COUNT, Horizons,
|
||||
MINIMUM_DURATION_DAYS, Vecs, age_bounds_days, horizon_mobility, mobility,
|
||||
AGE_COHORT_COUNT, AgeBand, AggregateVecs, Horizons, MINIMUM_DURATION_DAYS, Vecs,
|
||||
age_bounds_days, horizon_mobility, mobility,
|
||||
};
|
||||
use crate::{
|
||||
distribution,
|
||||
frameworks::{WeightedCohortContribution, WeightedCohortState, WeightedRatio, realized_price},
|
||||
frameworks::{WeightedCohortContribution, WeightedCohortState, WeightedRatio},
|
||||
indexes,
|
||||
internal::db_utils::validate_any_computed_version_or_reset,
|
||||
price,
|
||||
@@ -50,8 +50,8 @@ impl AggregateState {
|
||||
loss_supply: Sats,
|
||||
total_cap: Cents,
|
||||
mobility: StoredF64,
|
||||
horizon_mobilities: &Horizons<[f64; AGE_COHORT_COUNT]>,
|
||||
age: usize,
|
||||
horizon_mobilities: &Horizons<AgeRange<f64>>,
|
||||
age: AgeRangeId,
|
||||
) -> WeightedCohortContribution {
|
||||
let contribution = self
|
||||
.weighted
|
||||
@@ -63,7 +63,7 @@ impl AggregateState {
|
||||
.iter_mut()
|
||||
.zip(horizon_mobilities.iter())
|
||||
{
|
||||
ratio.add(loss, total, weights[age]);
|
||||
ratio.add(loss, total, *age.select(weights));
|
||||
}
|
||||
contribution
|
||||
}
|
||||
@@ -92,42 +92,46 @@ impl Vecs {
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let starting_lengths = indexer.safe_lengths();
|
||||
let source_cohorts: Vec<_> = distribution.utxo_cohorts.age_range.iter().collect();
|
||||
let transfer_volumes: Vec<_> = source_cohorts
|
||||
.iter()
|
||||
.map(|cohort| {
|
||||
&cohort
|
||||
.metrics
|
||||
.activity
|
||||
.transfer_volume
|
||||
.cumulative
|
||||
.sats
|
||||
.height
|
||||
})
|
||||
.collect();
|
||||
let supplies: Vec<_> = source_cohorts
|
||||
.iter()
|
||||
.map(|cohort| &cohort.metrics.supply.total.sats.height)
|
||||
.collect();
|
||||
let loss_supplies: Vec<_> = source_cohorts
|
||||
.iter()
|
||||
.map(|cohort| &cohort.metrics.supply.in_loss.sats.height)
|
||||
.collect();
|
||||
let realized_caps: Vec<_> = source_cohorts
|
||||
.iter()
|
||||
.map(|cohort| &cohort.metrics.realized.cap.cents.height)
|
||||
.collect();
|
||||
let coindays_created: Vec<_> = cointime
|
||||
.age_range
|
||||
.iter()
|
||||
.map(|cohort| &cohort.coindays_created.cumulative.height)
|
||||
.collect();
|
||||
let transfer_volumes = AgeRange::from_fn(|id| {
|
||||
&id.select(&distribution.utxo_cohorts.age_range)
|
||||
.metrics
|
||||
.activity
|
||||
.transfer_volume
|
||||
.cumulative
|
||||
.sats
|
||||
.height
|
||||
});
|
||||
let supplies = AgeRange::from_fn(|id| {
|
||||
&id.select(&distribution.utxo_cohorts.age_range)
|
||||
.metrics
|
||||
.supply
|
||||
.total
|
||||
.sats
|
||||
.height
|
||||
});
|
||||
let loss_supplies = AgeRange::from_fn(|id| {
|
||||
&id.select(&distribution.utxo_cohorts.age_range)
|
||||
.metrics
|
||||
.supply
|
||||
.in_loss
|
||||
.sats
|
||||
.height
|
||||
});
|
||||
let realized_caps = AgeRange::from_fn(|id| {
|
||||
&id.select(&distribution.utxo_cohorts.age_range)
|
||||
.metrics
|
||||
.realized
|
||||
.cap
|
||||
.cents
|
||||
.height
|
||||
});
|
||||
let coindays_created = &cointime.age_range.coindays_created.cumulative;
|
||||
|
||||
self.compute_primary(
|
||||
&starting_lengths,
|
||||
&indexes.timestamp.monotonic,
|
||||
&transfer_volumes,
|
||||
&coindays_created,
|
||||
coindays_created,
|
||||
&supplies,
|
||||
&loss_supplies,
|
||||
&realized_caps,
|
||||
@@ -142,28 +146,22 @@ impl Vecs {
|
||||
&mut self,
|
||||
starting_lengths: &Lengths,
|
||||
timestamps: &T,
|
||||
transfer_volumes: &[&S],
|
||||
coindays_created: &[&D],
|
||||
supplies: &[&S],
|
||||
loss_supplies: &[&S],
|
||||
realized_caps: &[&C],
|
||||
transfer_volumes: &AgeRange<&S>,
|
||||
coindays_created: &D,
|
||||
supplies: &AgeRange<&S>,
|
||||
loss_supplies: &AgeRange<&S>,
|
||||
realized_caps: &AgeRange<&C>,
|
||||
exit: &Exit,
|
||||
) -> Result<Height>
|
||||
where
|
||||
T: ReadableVec<Height, Timestamp>,
|
||||
D: ReadableVec<Height, StoredF64>,
|
||||
D: ReadableVec<Height, [StoredF64; AGE_COHORT_COUNT]>,
|
||||
S: ReadableVec<Height, Sats>,
|
||||
C: ReadableVec<Height, Cents>,
|
||||
{
|
||||
debug_assert_eq!(transfer_volumes.len(), AGE_COHORT_COUNT);
|
||||
debug_assert_eq!(coindays_created.len(), AGE_COHORT_COUNT);
|
||||
debug_assert_eq!(supplies.len(), AGE_COHORT_COUNT);
|
||||
debug_assert_eq!(loss_supplies.len(), AGE_COHORT_COUNT);
|
||||
debug_assert_eq!(realized_caps.len(), AGE_COHORT_COUNT);
|
||||
|
||||
let source_version: Version = std::iter::once(timestamps.version())
|
||||
.chain(transfer_volumes.iter().map(|vec| vec.version()))
|
||||
.chain(coindays_created.iter().map(|vec| vec.version()))
|
||||
.chain(std::iter::once(coindays_created.version()))
|
||||
.chain(supplies.iter().map(|vec| vec.version()))
|
||||
.chain(loss_supplies.iter().map(|vec| vec.version()))
|
||||
.chain(realized_caps.iter().map(|vec| vec.version()))
|
||||
@@ -175,7 +173,6 @@ impl Vecs {
|
||||
|
||||
let start = self
|
||||
.primary_vecs_mut()
|
||||
.into_iter()
|
||||
.map(|vec| vec.len())
|
||||
.min()
|
||||
.unwrap_or_default()
|
||||
@@ -188,7 +185,7 @@ impl Vecs {
|
||||
let source_end = transfer_volumes
|
||||
.iter()
|
||||
.map(|vec| vec.len())
|
||||
.chain(coindays_created.iter().map(|vec| vec.len()))
|
||||
.chain(std::iter::once(coindays_created.len()))
|
||||
.chain(supplies.iter().map(|vec| vec.len()))
|
||||
.chain(loss_supplies.iter().map(|vec| vec.len()))
|
||||
.chain(realized_caps.iter().map(|vec| vec.len()))
|
||||
@@ -204,61 +201,63 @@ impl Vecs {
|
||||
.collect_one(Height::ZERO)
|
||||
.unwrap_or(Timestamp::ZERO);
|
||||
let bounds = age_bounds_days();
|
||||
let mut age_filters = AGE_RANGE_FILTERS.iter();
|
||||
let is_sth: [bool; AGE_COHORT_COUNT] =
|
||||
std::array::from_fn(|_| TERM_FILTERS.short.includes(age_filters.next().unwrap()));
|
||||
|
||||
let mut chunk_start = start;
|
||||
while chunk_start < source_end {
|
||||
let chunk_end = (chunk_start + WRITE_INTERVAL).min(source_end);
|
||||
let timestamp_batch = timestamps.collect_range_at(chunk_start, chunk_end);
|
||||
let transfer_batches: Vec<_> = transfer_volumes
|
||||
.iter()
|
||||
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
|
||||
.collect();
|
||||
let coinday_batches: Vec<_> = coindays_created
|
||||
.iter()
|
||||
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
|
||||
.collect();
|
||||
let supply_batches: Vec<_> = supplies
|
||||
.iter()
|
||||
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
|
||||
.collect();
|
||||
let loss_supply_batches: Vec<_> = loss_supplies
|
||||
.iter()
|
||||
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
|
||||
.collect();
|
||||
let cap_batches: Vec<_> = realized_caps
|
||||
.iter()
|
||||
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
|
||||
.collect();
|
||||
let transfer_batches = AgeRange::from_fn(|id| {
|
||||
id.select(transfer_volumes)
|
||||
.collect_range_at(chunk_start, chunk_end)
|
||||
});
|
||||
let coinday_batch = coindays_created.collect_range_at(chunk_start, chunk_end);
|
||||
let supply_batches = AgeRange::from_fn(|id| {
|
||||
id.select(supplies).collect_range_at(chunk_start, chunk_end)
|
||||
});
|
||||
let loss_supply_batches = AgeRange::from_fn(|id| {
|
||||
id.select(loss_supplies)
|
||||
.collect_range_at(chunk_start, chunk_end)
|
||||
});
|
||||
let cap_batches = AgeRange::from_fn(|id| {
|
||||
id.select(realized_caps)
|
||||
.collect_range_at(chunk_start, chunk_end)
|
||||
});
|
||||
|
||||
for offset in 0..(chunk_end - chunk_start) {
|
||||
let hazards = std::array::from_fn(|index| {
|
||||
let hazards = AgeRange::from_fn(|id| {
|
||||
spending_rate(
|
||||
transfer_batches[index][offset],
|
||||
coinday_batches[index][offset],
|
||||
id.select(&transfer_batches)[offset],
|
||||
*id.get(&coinday_batch[offset]),
|
||||
)
|
||||
});
|
||||
let network_age = timestamp_batch[offset]
|
||||
.difference_in_days_between_float(genesis_timestamp)
|
||||
.max(MINIMUM_DURATION_DAYS);
|
||||
let exposures = spending_exposures(&hazards, network_age, &bounds);
|
||||
let mobilities = exposures.map(mobility);
|
||||
let horizon_mobilities: Horizons<[f64; AGE_COHORT_COUNT]> =
|
||||
let exposures = DecayFit::exposures(&hazards, network_age, &bounds);
|
||||
let mobilities = AgeRange::from_fn(|id| mobility(*id.select(&exposures)));
|
||||
let horizon_mobilities: Horizons<AgeRange<f64>> =
|
||||
Horizons::from_fn(|_, horizon| {
|
||||
std::array::from_fn(|age| horizon_mobility(&hazards, age, horizon, &bounds))
|
||||
AgeRange::from_fn(|age| horizon_mobility(&hazards, age, horizon, &bounds))
|
||||
});
|
||||
self.age_range.spending_rate.push(AgeRangeId::from_fn(|id| {
|
||||
StoredF64::from(*id.select(&hazards))
|
||||
}));
|
||||
self.age_range
|
||||
.spending_exposure
|
||||
.push(AgeRangeId::from_fn(|id| {
|
||||
StoredF64::from(*id.select(&exposures))
|
||||
}));
|
||||
|
||||
let mut terms = ByTerm::<AggregateState>::default();
|
||||
let mut mobile_supply = AgeRange::default();
|
||||
let mut immobile_supply = AgeRange::default();
|
||||
|
||||
for (index, cohort) in self.age_range.iter_mut().enumerate() {
|
||||
let mobility = StoredF64::from(mobilities[index]);
|
||||
let total_supply = supply_batches[index][offset];
|
||||
let total_cap = cap_batches[index][offset];
|
||||
let loss_supply = loss_supply_batches[index][offset];
|
||||
for &id in AgeRangeId::ALL {
|
||||
let mobility = StoredF64::from(*id.select(&mobilities));
|
||||
let total_supply = id.select(&supply_batches)[offset];
|
||||
let total_cap = id.select(&cap_batches)[offset];
|
||||
let loss_supply = id.select(&loss_supply_batches)[offset];
|
||||
|
||||
let term = if is_sth[index] {
|
||||
let term = if TERM_FILTERS.short.includes(id.filter()) {
|
||||
&mut terms.short
|
||||
} else {
|
||||
&mut terms.long
|
||||
@@ -269,31 +268,22 @@ impl Vecs {
|
||||
total_cap,
|
||||
mobility,
|
||||
&horizon_mobilities,
|
||||
index,
|
||||
id,
|
||||
);
|
||||
|
||||
cohort
|
||||
.spending_rate
|
||||
.height
|
||||
.push(StoredF64::from(hazards[index]));
|
||||
cohort
|
||||
.spending_exposure
|
||||
.height
|
||||
.push(StoredF64::from(exposures[index]));
|
||||
cohort
|
||||
.supply
|
||||
.mobile
|
||||
.sats
|
||||
.height
|
||||
.push(contribution.weighted_supply);
|
||||
cohort
|
||||
.supply
|
||||
.immobile
|
||||
.sats
|
||||
.height
|
||||
.push(contribution.complement_supply);
|
||||
*id.select_mut(&mut mobile_supply) = contribution.weighted_supply;
|
||||
*id.select_mut(&mut immobile_supply) = contribution.complement_supply;
|
||||
}
|
||||
|
||||
self.age_range
|
||||
.supply
|
||||
.mobile
|
||||
.push(AgeRangeId::from_fn(|id| *id.select(&mobile_supply)));
|
||||
self.age_range
|
||||
.supply
|
||||
.immobile
|
||||
.push(AgeRangeId::from_fn(|id| *id.select(&immobile_supply)));
|
||||
|
||||
self.all.push(terms.short.merged(terms.long));
|
||||
self.sth.push(terms.short);
|
||||
self.lth.push(terms.long);
|
||||
@@ -311,17 +301,17 @@ impl Vecs {
|
||||
Ok(Height::from(start))
|
||||
}
|
||||
|
||||
fn primary_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
let mut vecs = Vec::with_capacity(AGE_COHORT_COUNT * 4 + 3 * (5 + HORIZON_COUNT));
|
||||
|
||||
for cohort in self.age_range.iter_mut() {
|
||||
vecs.extend(cohort.primary_vecs_mut());
|
||||
}
|
||||
|
||||
vecs.extend(self.all.primary_vecs_mut());
|
||||
vecs.extend(self.sth.primary_vecs_mut());
|
||||
vecs.extend(self.lth.primary_vecs_mut());
|
||||
vecs
|
||||
fn primary_vecs_mut(&mut self) -> impl Iterator<Item = &mut dyn AnyStoredVec> {
|
||||
[
|
||||
self.age_range.spending_rate.stored_mut(),
|
||||
self.age_range.spending_exposure.stored_mut(),
|
||||
self.age_range.supply.mobile.stored_mut(),
|
||||
self.age_range.supply.immobile.stored_mut(),
|
||||
]
|
||||
.into_iter()
|
||||
.chain(self.all.primary_vecs_mut())
|
||||
.chain(self.sth.primary_vecs_mut())
|
||||
.chain(self.lth.primary_vecs_mut())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,38 +338,26 @@ impl AggregateVecs {
|
||||
output.supply_in_loss_share.height.push(ratio.value());
|
||||
}
|
||||
self.cap.cents.height.push(state.weighted.weighted_cap);
|
||||
self.price.cents.height.push(realized_price(
|
||||
state.weighted.weighted_cap,
|
||||
state.weighted.weighted_supply,
|
||||
));
|
||||
self.price
|
||||
.cents
|
||||
.height
|
||||
.push(state.weighted.realized_price());
|
||||
}
|
||||
|
||||
fn primary_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
let mut vecs = Vec::with_capacity(5 + HORIZON_COUNT);
|
||||
vecs.extend([
|
||||
fn primary_vecs_mut(&mut self) -> impl Iterator<Item = &mut dyn AnyStoredVec> {
|
||||
[
|
||||
&mut self.supply.mobile.sats.height as &mut dyn AnyStoredVec,
|
||||
&mut self.supply.immobile.sats.height,
|
||||
&mut self.supply_in_loss_share.height,
|
||||
&mut self.cap.cents.height,
|
||||
&mut self.price.cents.height,
|
||||
]);
|
||||
vecs.extend(
|
||||
]
|
||||
.into_iter()
|
||||
.chain(
|
||||
self.horizon
|
||||
.iter_mut()
|
||||
.map(|horizon| &mut horizon.supply_in_loss_share.height as &mut dyn AnyStoredVec),
|
||||
);
|
||||
vecs
|
||||
}
|
||||
}
|
||||
|
||||
impl CohortVecs {
|
||||
fn primary_vecs_mut(&mut self) -> [&mut dyn AnyStoredVec; 4] {
|
||||
[
|
||||
&mut self.spending_rate.height,
|
||||
&mut self.spending_exposure.height,
|
||||
&mut self.supply.mobile.sats.height,
|
||||
&mut self.supply.immobile.sats.height,
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,135 +371,135 @@ fn spending_rate(transfer_volume: Sats, coindays_created: StoredF64) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
fn fit_decay(
|
||||
hazards: &[f64; AGE_COHORT_COUNT],
|
||||
network_age: f64,
|
||||
bounds: &[AgeBand; AGE_COHORT_COUNT],
|
||||
) -> Option<DecayFit> {
|
||||
let mut total_duration = 0.0;
|
||||
let mut weighted_age = 0.0;
|
||||
let mut weighted_log_hazard = 0.0;
|
||||
let mut anchor = None;
|
||||
impl DecayFit {
|
||||
fn fit(hazards: &AgeRange<f64>, network_age: f64, bounds: &AgeRange<AgeBand>) -> Option<Self> {
|
||||
let mut total_duration = 0.0;
|
||||
let mut weighted_age = 0.0;
|
||||
let mut weighted_log_hazard = 0.0;
|
||||
let mut anchor = None;
|
||||
|
||||
for (index, band) in bounds[..AGE_COHORT_COUNT - 1].iter().enumerate() {
|
||||
let hazard = hazards[index];
|
||||
if band.upper > network_age || !hazard.is_finite() || hazard <= 0.0 {
|
||||
continue;
|
||||
for &id in &AgeRangeId::ALL[..AGE_COHORT_COUNT - 1] {
|
||||
let band = *id.select(bounds);
|
||||
let hazard = *id.select(hazards);
|
||||
if band.upper > network_age || !hazard.is_finite() || hazard <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let age = (band.lower + band.upper) / 2.0;
|
||||
let duration = band.upper - band.lower;
|
||||
let log_hazard = hazard.ln();
|
||||
total_duration += duration;
|
||||
weighted_age += duration * age;
|
||||
weighted_log_hazard += duration * log_hazard;
|
||||
anchor = Some((band.upper, hazard));
|
||||
}
|
||||
|
||||
let age = (band.lower + band.upper) / 2.0;
|
||||
let duration = band.upper - band.lower;
|
||||
let log_hazard = hazard.ln();
|
||||
total_duration += duration;
|
||||
weighted_age += duration * age;
|
||||
weighted_log_hazard += duration * log_hazard;
|
||||
anchor = Some((band.upper, hazard));
|
||||
}
|
||||
|
||||
if total_duration <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mean_age = weighted_age / total_duration;
|
||||
let mean_log_hazard = weighted_log_hazard / total_duration;
|
||||
let mut covariance = 0.0;
|
||||
let mut age_variance = 0.0;
|
||||
|
||||
for (index, band) in bounds[..AGE_COHORT_COUNT - 1].iter().enumerate() {
|
||||
let hazard = hazards[index];
|
||||
if band.upper > network_age || !hazard.is_finite() || hazard <= 0.0 {
|
||||
continue;
|
||||
if total_duration <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let age = (band.lower + band.upper) / 2.0;
|
||||
let duration = band.upper - band.lower;
|
||||
let log_hazard = hazard.ln();
|
||||
let age_offset = age - mean_age;
|
||||
covariance += duration * age_offset * (log_hazard - mean_log_hazard);
|
||||
age_variance += duration * age_offset.powi(2);
|
||||
}
|
||||
let mean_age = weighted_age / total_duration;
|
||||
let mean_log_hazard = weighted_log_hazard / total_duration;
|
||||
let mut covariance = 0.0;
|
||||
let mut age_variance = 0.0;
|
||||
|
||||
if age_variance <= f64::EPSILON {
|
||||
return None;
|
||||
}
|
||||
for &id in &AgeRangeId::ALL[..AGE_COHORT_COUNT - 1] {
|
||||
let band = *id.select(bounds);
|
||||
let hazard = *id.select(hazards);
|
||||
if band.upper > network_age || !hazard.is_finite() || hazard <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let slope = covariance / age_variance;
|
||||
if slope >= 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let tau = -1.0 / slope;
|
||||
if !tau.is_finite() || tau <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (anchor_age, anchor_hazard) = anchor?;
|
||||
Some(DecayFit {
|
||||
slope,
|
||||
tau,
|
||||
anchor_age,
|
||||
anchor_hazard,
|
||||
})
|
||||
}
|
||||
|
||||
fn spending_exposures(
|
||||
hazards: &[f64; AGE_COHORT_COUNT],
|
||||
network_age: f64,
|
||||
bounds: &[AgeBand; AGE_COHORT_COUNT],
|
||||
) -> [f64; AGE_COHORT_COUNT] {
|
||||
let Some(fit) = fit_decay(hazards, network_age, bounds) else {
|
||||
return [0.0; AGE_COHORT_COUNT];
|
||||
};
|
||||
|
||||
std::array::from_fn(|start_band| {
|
||||
spending_exposure(hazards, start_band, network_age, bounds, fit)
|
||||
})
|
||||
}
|
||||
|
||||
fn spending_exposure(
|
||||
hazards: &[f64; AGE_COHORT_COUNT],
|
||||
start_band: usize,
|
||||
network_age: f64,
|
||||
bounds: &[AgeBand; AGE_COHORT_COUNT],
|
||||
fit: DecayFit,
|
||||
) -> f64 {
|
||||
let start = bounds[start_band];
|
||||
let occupied_upper = if start.upper.is_finite() {
|
||||
start.upper.min(network_age.max(start.lower))
|
||||
} else {
|
||||
start.lower
|
||||
};
|
||||
let mut age = if start.upper.is_finite() {
|
||||
(start.lower + occupied_upper) / 2.0
|
||||
} else {
|
||||
start.lower
|
||||
};
|
||||
let mut exposure = 0.0;
|
||||
|
||||
for band_index in start_band..AGE_COHORT_COUNT - 1 {
|
||||
let band = bounds[band_index];
|
||||
let duration = (band.upper - age.max(band.lower)).max(MINIMUM_DURATION_DAYS);
|
||||
let hazard = hazards[band_index];
|
||||
let observed = band.upper <= network_age && hazard.is_finite() && hazard > 0.0;
|
||||
if !observed {
|
||||
break;
|
||||
let age = (band.lower + band.upper) / 2.0;
|
||||
let duration = band.upper - band.lower;
|
||||
let log_hazard = hazard.ln();
|
||||
let age_offset = age - mean_age;
|
||||
covariance += duration * age_offset * (log_hazard - mean_log_hazard);
|
||||
age_variance += duration * age_offset.powi(2);
|
||||
}
|
||||
|
||||
exposure += hazard * duration;
|
||||
age = band.upper;
|
||||
if age_variance <= f64::EPSILON {
|
||||
return None;
|
||||
}
|
||||
|
||||
let slope = covariance / age_variance;
|
||||
if slope >= 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let tau = -1.0 / slope;
|
||||
if !tau.is_finite() || tau <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (anchor_age, anchor_hazard) = anchor?;
|
||||
Some(Self {
|
||||
slope,
|
||||
tau,
|
||||
anchor_age,
|
||||
anchor_hazard,
|
||||
})
|
||||
}
|
||||
|
||||
let tail = bounds[AGE_COHORT_COUNT - 1];
|
||||
let tail_hazard = hazards[AGE_COHORT_COUNT - 1];
|
||||
let observed_tail = network_age > tail.lower && tail_hazard.is_finite() && tail_hazard > 0.0;
|
||||
let (anchor_age, anchor_hazard) = if observed_tail {
|
||||
(tail.lower, tail_hazard)
|
||||
} else {
|
||||
(fit.anchor_age, fit.anchor_hazard)
|
||||
};
|
||||
let continuation_age = age.max(anchor_age);
|
||||
let continuation_hazard = anchor_hazard * (fit.slope * (continuation_age - anchor_age)).exp();
|
||||
exposure + (continuation_hazard * fit.tau).max(0.0)
|
||||
fn exposures(
|
||||
hazards: &AgeRange<f64>,
|
||||
network_age: f64,
|
||||
bounds: &AgeRange<AgeBand>,
|
||||
) -> AgeRange<f64> {
|
||||
let Some(fit) = Self::fit(hazards, network_age, bounds) else {
|
||||
return AgeRange::default();
|
||||
};
|
||||
|
||||
AgeRange::from_fn(|start_band| fit.exposure(hazards, start_band, network_age, bounds))
|
||||
}
|
||||
|
||||
fn exposure(
|
||||
self,
|
||||
hazards: &AgeRange<f64>,
|
||||
start_band: AgeRangeId,
|
||||
network_age: f64,
|
||||
bounds: &AgeRange<AgeBand>,
|
||||
) -> f64 {
|
||||
let start = *start_band.select(bounds);
|
||||
let occupied_upper = if start.upper.is_finite() {
|
||||
start.upper.min(network_age.max(start.lower))
|
||||
} else {
|
||||
start.lower
|
||||
};
|
||||
let mut age = if start.upper.is_finite() {
|
||||
(start.lower + occupied_upper) / 2.0
|
||||
} else {
|
||||
start.lower
|
||||
};
|
||||
let mut exposure = 0.0;
|
||||
|
||||
for &band_id in &AgeRangeId::ALL[start_band.index()..AGE_COHORT_COUNT - 1] {
|
||||
let band = *band_id.select(bounds);
|
||||
let duration = (band.upper - age.max(band.lower)).max(MINIMUM_DURATION_DAYS);
|
||||
let hazard = *band_id.select(hazards);
|
||||
let observed = band.upper <= network_age && hazard.is_finite() && hazard > 0.0;
|
||||
if !observed {
|
||||
break;
|
||||
}
|
||||
|
||||
exposure += hazard * duration;
|
||||
age = band.upper;
|
||||
}
|
||||
|
||||
let tail = bounds.over_15y;
|
||||
let tail_hazard = hazards.over_15y;
|
||||
let observed_tail =
|
||||
network_age > tail.lower && tail_hazard.is_finite() && tail_hazard > 0.0;
|
||||
let (anchor_age, anchor_hazard) = if observed_tail {
|
||||
(tail.lower, tail_hazard)
|
||||
} else {
|
||||
(self.anchor_age, self.anchor_hazard)
|
||||
};
|
||||
let continuation_age = age.max(anchor_age);
|
||||
let continuation_hazard =
|
||||
anchor_hazard * (self.slope * (continuation_age - anchor_age)).exp();
|
||||
exposure + (continuation_hazard * self.tau).max(0.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -541,8 +519,8 @@ mod tests {
|
||||
#[test]
|
||||
fn fixed_horizon_compounds_hazards_across_age_ranges() {
|
||||
let bounds = age_bounds_days();
|
||||
let hazards = [0.01; AGE_COHORT_COUNT];
|
||||
let probability = horizon_mobility(&hazards, 2, 30.0, &bounds);
|
||||
let hazards = AgeRange::from_fn(|_| 0.01);
|
||||
let probability = horizon_mobility(&hazards, AgeRangeId::From1DTo1W, 30.0, &bounds);
|
||||
|
||||
assert!((probability - mobility(0.3)).abs() < 1e-12);
|
||||
}
|
||||
@@ -551,8 +529,8 @@ mod tests {
|
||||
fn decay_fit_recovers_an_exponential_lifetime() {
|
||||
let bounds = age_bounds_days();
|
||||
let expected_tau = 1_000.0;
|
||||
let hazards = std::array::from_fn(|index| {
|
||||
let band = bounds[index];
|
||||
let hazards = AgeRange::from_fn(|id| {
|
||||
let band = *id.select(&bounds);
|
||||
let age = if band.upper.is_finite() {
|
||||
(band.lower + band.upper) / 2.0
|
||||
} else {
|
||||
@@ -561,7 +539,7 @@ mod tests {
|
||||
(-age / expected_tau).exp()
|
||||
});
|
||||
|
||||
let fit = fit_decay(&hazards, 20.0 * 365.0, &bounds).unwrap();
|
||||
let fit = DecayFit::fit(&hazards, 20.0 * 365.0, &bounds).unwrap();
|
||||
|
||||
assert!((fit.tau - expected_tau).abs() < 1e-9);
|
||||
}
|
||||
@@ -569,8 +547,8 @@ mod tests {
|
||||
#[test]
|
||||
fn oldest_cohort_exposure_is_its_observed_tail_lifetime() {
|
||||
let bounds = age_bounds_days();
|
||||
let hazards = std::array::from_fn(|index| {
|
||||
let band = bounds[index];
|
||||
let hazards = AgeRange::from_fn(|id| {
|
||||
let band = *id.select(&bounds);
|
||||
let age = if band.upper.is_finite() {
|
||||
(band.lower + band.upper) / 2.0
|
||||
} else {
|
||||
@@ -579,13 +557,10 @@ mod tests {
|
||||
(-age / 1_000.0).exp()
|
||||
});
|
||||
let network_age = 20.0 * 365.0;
|
||||
let fit = fit_decay(&hazards, network_age, &bounds).unwrap();
|
||||
let exposures = spending_exposures(&hazards, network_age, &bounds);
|
||||
let fit = DecayFit::fit(&hazards, network_age, &bounds).unwrap();
|
||||
let exposures = DecayFit::exposures(&hazards, network_age, &bounds);
|
||||
|
||||
assert!(
|
||||
(exposures[AGE_COHORT_COUNT - 1] - hazards[AGE_COHORT_COUNT - 1] * fit.tau).abs()
|
||||
< 1e-12
|
||||
);
|
||||
assert!((exposures.over_15y - hazards.over_15y * fit.tau).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -605,7 +580,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn term_aggregates_merge_into_all() {
|
||||
let horizons = Horizons::from_fn(|_, _| [0.25; AGE_COHORT_COUNT]);
|
||||
let horizons = Horizons::from_fn(|_, _| AgeRange::from_fn(|_| 0.25));
|
||||
let mut direct = AggregateState::default();
|
||||
direct.add(
|
||||
Sats::from(100_u64),
|
||||
@@ -613,7 +588,7 @@ mod tests {
|
||||
Cents::from(1_000_u64),
|
||||
StoredF64::from(0.3),
|
||||
&horizons,
|
||||
0,
|
||||
AgeRangeId::Under1H,
|
||||
);
|
||||
direct.add(
|
||||
Sats::from(200_u64),
|
||||
@@ -621,7 +596,7 @@ mod tests {
|
||||
Cents::from(3_000_u64),
|
||||
StoredF64::from(0.4),
|
||||
&horizons,
|
||||
1,
|
||||
AgeRangeId::From1HTo1D,
|
||||
);
|
||||
|
||||
let mut sth = AggregateState::default();
|
||||
@@ -631,7 +606,7 @@ mod tests {
|
||||
Cents::from(1_000_u64),
|
||||
StoredF64::from(0.3),
|
||||
&horizons,
|
||||
0,
|
||||
AgeRangeId::Under1H,
|
||||
);
|
||||
let mut lth = AggregateState::default();
|
||||
lth.add(
|
||||
@@ -640,7 +615,7 @@ mod tests {
|
||||
Cents::from(3_000_u64),
|
||||
StoredF64::from(0.4),
|
||||
&horizons,
|
||||
1,
|
||||
AgeRangeId::From1HTo1D,
|
||||
);
|
||||
let merged = sth.merged(lth);
|
||||
|
||||
@@ -668,10 +643,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn age_ranges_belong_to_exactly_one_term() {
|
||||
for filter in AGE_RANGE_FILTERS.iter() {
|
||||
for id in AgeRangeId::ALL {
|
||||
assert_ne!(
|
||||
TERM_FILTERS.short.includes(filter),
|
||||
TERM_FILTERS.long.includes(filter)
|
||||
TERM_FILTERS.short.includes(id.filter()),
|
||||
TERM_FILTERS.long.includes(id.filter())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
use brk_cohort::{AgeRange, CohortContext, TERM_NAMES};
|
||||
use brk_cohort::{AgeRangeId, CohortContext, TERM_NAMES};
|
||||
use brk_error::Result;
|
||||
use brk_types::{Cents, Height, StoredF64, Version};
|
||||
use vecdb::{CachedBoxedVec, Database, ReadableCloneableVec, UnaryTransform};
|
||||
|
||||
use super::{AggregateVecs, CohortVecs, HorizonVecs, Horizons, Split, Vecs, mobility};
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{FiatPerBlock, LazyPerBlock, PerBlock, PriceWithRatioPerBlock, SpotValuePerBlock},
|
||||
use vecdb::{
|
||||
CachedBoxedVec, Database, PcoVec, ReadOnlyColumnarVec, ReadableCloneableVec, UnaryTransform,
|
||||
};
|
||||
|
||||
const VERSION: Version = Version::TWO;
|
||||
use super::{
|
||||
AgeRangeVecs, AggregateVecs, HorizonVecs, Horizons, SpendingExposureSeries, Split, Vecs,
|
||||
mobility,
|
||||
};
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
ColumnarPerBlock, FiatPerBlock, LazyColumnPerBlock, LazyColumnSpotValuePerBlock,
|
||||
LazyPerBlock, PerBlock, PriceWithRatioPerBlock, SpotValuePerBlock,
|
||||
},
|
||||
};
|
||||
|
||||
const VERSION: Version = Version::new(4);
|
||||
|
||||
struct ExposureToMobility;
|
||||
|
||||
@@ -20,46 +28,35 @@ impl UnaryTransform<StoredF64, StoredF64> for ExposureToMobility {
|
||||
}
|
||||
}
|
||||
|
||||
fn import_split<T>(mut import: impl FnMut(&str) -> Result<T>) -> Result<Split<T>> {
|
||||
Ok(Split {
|
||||
mobile: import("mobile")?,
|
||||
immobile: import("immobile")?,
|
||||
})
|
||||
}
|
||||
|
||||
impl CohortVecs {
|
||||
fn forced_import(
|
||||
db: &vecdb::Database,
|
||||
name: &str,
|
||||
impl SpendingExposureSeries {
|
||||
fn new(
|
||||
version: Version,
|
||||
source: &ReadOnlyColumnarVec<PcoVec<Height, StoredF64>, AgeRangeId>,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let spending_rate =
|
||||
PerBlock::forced_import(db, &format!("{name}_spending_rate"), version, indexes)?;
|
||||
let spending_exposure =
|
||||
PerBlock::forced_import(db, &format!("{name}_spending_exposure"), version, indexes)?;
|
||||
let mobility = LazyPerBlock::from_computed::<ExposureToMobility>(
|
||||
&format!("{name}_mobility"),
|
||||
version,
|
||||
spending_exposure.height.read_only_boxed_clone(),
|
||||
&spending_exposure,
|
||||
);
|
||||
) -> Self {
|
||||
let age_range = AgeRangeId::series(CohortContext::Utxo, |column, name| {
|
||||
LazyColumnPerBlock::new(
|
||||
&format!("{name}_spending_exposure"),
|
||||
version,
|
||||
source,
|
||||
column,
|
||||
indexes,
|
||||
)
|
||||
});
|
||||
let mobility = AgeRangeId::series(CohortContext::Utxo, |column, name| {
|
||||
let exposure = column.select(&age_range);
|
||||
LazyPerBlock::from_resolutions::<ExposureToMobility>(
|
||||
&format!("{name}_mobility"),
|
||||
version,
|
||||
exposure.height.read_only_boxed_clone(),
|
||||
&exposure.resolutions,
|
||||
)
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
spending_rate,
|
||||
spending_exposure,
|
||||
Self {
|
||||
age_range,
|
||||
mobility,
|
||||
supply: import_split(|side| {
|
||||
SpotValuePerBlock::forced_import(
|
||||
db,
|
||||
&format!("{name}_{side}_supply"),
|
||||
version,
|
||||
indexes,
|
||||
spot_price,
|
||||
)
|
||||
})?,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +75,7 @@ impl AggregateVecs {
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
supply: import_split(|side| {
|
||||
supply: Split::try_from_fn(|side| {
|
||||
SpotValuePerBlock::forced_import(
|
||||
db,
|
||||
&format!("{prefix}{side}_supply"),
|
||||
@@ -130,12 +127,54 @@ impl Vecs {
|
||||
let version = parent_version + VERSION;
|
||||
let aggregate_version = version + Version::ONE;
|
||||
let spot_price = prices.spot.cents.height.read_only_cached_boxed_clone();
|
||||
let spending_rate = ColumnarPerBlock::forced_import(
|
||||
db,
|
||||
&CohortContext::Utxo.prefixed("age_range_spending_rate"),
|
||||
version,
|
||||
|source| {
|
||||
AgeRangeId::series(CohortContext::Utxo, |column, name| {
|
||||
LazyColumnPerBlock::new(
|
||||
&format!("{name}_spending_rate"),
|
||||
version,
|
||||
source,
|
||||
column,
|
||||
indexes,
|
||||
)
|
||||
})
|
||||
},
|
||||
)?;
|
||||
let spending_exposure = ColumnarPerBlock::forced_import(
|
||||
db,
|
||||
&CohortContext::Utxo.prefixed("age_range_spending_exposure"),
|
||||
version,
|
||||
|source| SpendingExposureSeries::new(version, source, indexes),
|
||||
)?;
|
||||
let supply = Split::try_from_fn(|side| {
|
||||
ColumnarPerBlock::forced_import(
|
||||
db,
|
||||
&CohortContext::Utxo.prefixed(&format!("age_range_{side}_supply_sats")),
|
||||
version,
|
||||
|source| {
|
||||
AgeRangeId::series(CohortContext::Utxo, |column, name| {
|
||||
LazyColumnSpotValuePerBlock::new(
|
||||
&format!("{name}_{side}_supply"),
|
||||
version,
|
||||
source,
|
||||
column,
|
||||
indexes,
|
||||
&spot_price,
|
||||
)
|
||||
})
|
||||
},
|
||||
)
|
||||
})?;
|
||||
|
||||
let this = Self {
|
||||
age_range: AgeRange::try_new(|_, name| {
|
||||
let name = CohortContext::Utxo.prefixed(name);
|
||||
CohortVecs::forced_import(db, &name, version, indexes, &spot_price)
|
||||
})?,
|
||||
age_range: AgeRangeVecs {
|
||||
spending_rate,
|
||||
spending_exposure,
|
||||
supply,
|
||||
},
|
||||
all: AggregateVecs::forced_import(db, "", version, indexes, &spot_price)?,
|
||||
sth: AggregateVecs::forced_import(
|
||||
db,
|
||||
|
||||
@@ -2,32 +2,34 @@ mod compute;
|
||||
mod import;
|
||||
mod vecs;
|
||||
|
||||
use brk_cohort::AGE_RANGE_BOUNDS;
|
||||
use brk_cohort::{AgeRange, AgeRangeId};
|
||||
use vecdb::ColumnId;
|
||||
|
||||
pub(crate) use brk_cohort::AGE_RANGE_COUNT as AGE_COHORT_COUNT;
|
||||
pub(crate) use vecs::HORIZON_DAYS;
|
||||
pub use vecs::{AggregateVecs, CohortVecs, HorizonVecs, Horizons, Split, Vecs};
|
||||
pub use vecs::{
|
||||
AgeRangeVecs, AggregateVecs, HorizonVecs, Horizons, SpendingExposureSeries, Split, Vecs,
|
||||
};
|
||||
|
||||
pub(crate) const HORIZON_COUNT: usize = 7;
|
||||
pub(crate) const HOURS_PER_DAY: f64 = 24.0;
|
||||
pub(crate) const MINIMUM_DURATION_DAYS: f64 = 1.0 / HOURS_PER_DAY;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct AgeBand {
|
||||
pub lower: f64,
|
||||
pub upper: f64,
|
||||
}
|
||||
|
||||
pub(crate) fn age_bounds_days() -> [AgeBand; AGE_COHORT_COUNT] {
|
||||
let mut bounds = AGE_RANGE_BOUNDS.iter();
|
||||
std::array::from_fn(|index| {
|
||||
let bound = bounds.next().unwrap();
|
||||
pub(crate) fn age_bounds_days() -> AgeRange<AgeBand> {
|
||||
AgeRange::from_fn(|id| {
|
||||
let bound = id.bounds();
|
||||
AgeBand {
|
||||
lower: bound.start as f64 / HOURS_PER_DAY,
|
||||
upper: if index + 1 < AGE_COHORT_COUNT {
|
||||
bound.end as f64 / HOURS_PER_DAY
|
||||
} else {
|
||||
upper: if id == AgeRangeId::Over15Y {
|
||||
f64::INFINITY
|
||||
} else {
|
||||
bound.end as f64 / HOURS_PER_DAY
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -43,34 +45,34 @@ pub(crate) fn mobility(exposure: f64) -> f64 {
|
||||
}
|
||||
|
||||
pub(crate) fn horizon_mobility(
|
||||
hazards: &[f64; AGE_COHORT_COUNT],
|
||||
start_band: usize,
|
||||
hazards: &AgeRange<f64>,
|
||||
start_band: AgeRangeId,
|
||||
horizon: f64,
|
||||
bounds: &[AgeBand; AGE_COHORT_COUNT],
|
||||
bounds: &AgeRange<AgeBand>,
|
||||
) -> f64 {
|
||||
let start = bounds[start_band];
|
||||
let start = *start_band.select(bounds);
|
||||
let mut age = if start.upper.is_finite() {
|
||||
(start.lower + start.upper) / 2.0
|
||||
} else {
|
||||
start.lower
|
||||
};
|
||||
let mut remaining = horizon;
|
||||
let mut band = start_band;
|
||||
let mut exposure = 0.0;
|
||||
|
||||
while remaining > 0.0 && band < AGE_COHORT_COUNT {
|
||||
let upper = bounds[band].upper;
|
||||
for &band in &AgeRangeId::ALL[start_band.index()..] {
|
||||
if remaining <= 0.0 {
|
||||
break;
|
||||
}
|
||||
let bound = *band.select(bounds);
|
||||
let upper = bound.upper;
|
||||
let duration = if upper.is_finite() {
|
||||
remaining.min((upper - age).max(MINIMUM_DURATION_DAYS))
|
||||
} else {
|
||||
remaining
|
||||
};
|
||||
exposure += hazards[band].max(0.0) * duration;
|
||||
exposure += band.select(hazards).max(0.0) * duration;
|
||||
remaining -= duration;
|
||||
band += 1;
|
||||
if band < AGE_COHORT_COUNT {
|
||||
age = bounds[band].lower;
|
||||
}
|
||||
age = upper;
|
||||
}
|
||||
|
||||
mobility(exposure)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use brk_cohort::AgeRange;
|
||||
use brk_cohort::{AgeRange, AgeRangeId};
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Cents, StoredF64};
|
||||
use brk_types::{Cents, Sats, StoredF64};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{Rw, StorageMode};
|
||||
|
||||
use crate::internal::{
|
||||
FiatPerBlock, LazyPerBlock, PerBlock, PriceWithRatioPerBlock, SpotValuePerBlock,
|
||||
ColumnarPerBlock, FiatPerBlock, LazyColumnPerBlock, LazyColumnSpotValuePerBlock, LazyPerBlock,
|
||||
PerBlock, PriceWithRatioPerBlock, SpotValuePerBlock,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Traversable)]
|
||||
@@ -108,12 +110,36 @@ pub struct Split<T> {
|
||||
pub immobile: T,
|
||||
}
|
||||
|
||||
impl<T> Split<T> {
|
||||
pub(crate) fn try_from_fn<E>(mut create: impl FnMut(&str) -> Result<T, E>) -> Result<Self, E> {
|
||||
Ok(Self {
|
||||
mobile: create("mobile")?,
|
||||
immobile: create("immobile")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deref, DerefMut, Traversable)]
|
||||
pub struct SpendingExposureSeries {
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
#[traversable(flatten)]
|
||||
pub age_range: AgeRange<LazyColumnPerBlock<StoredF64, AgeRangeId>>,
|
||||
pub mobility: AgeRange<LazyPerBlock<StoredF64>>,
|
||||
}
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct CohortVecs<M: StorageMode = Rw> {
|
||||
pub spending_rate: PerBlock<StoredF64, M>,
|
||||
pub spending_exposure: PerBlock<StoredF64, M>,
|
||||
pub mobility: LazyPerBlock<StoredF64>,
|
||||
pub supply: Split<SpotValuePerBlock<M>>,
|
||||
pub struct AgeRangeVecs<M: StorageMode = Rw> {
|
||||
pub spending_rate: ColumnarPerBlock<
|
||||
StoredF64,
|
||||
AgeRangeId,
|
||||
AgeRange<LazyColumnPerBlock<StoredF64, AgeRangeId>>,
|
||||
M,
|
||||
>,
|
||||
pub spending_exposure: ColumnarPerBlock<StoredF64, AgeRangeId, SpendingExposureSeries, M>,
|
||||
pub supply: Split<
|
||||
ColumnarPerBlock<Sats, AgeRangeId, AgeRange<LazyColumnSpotValuePerBlock<AgeRangeId>>, M>,
|
||||
>,
|
||||
}
|
||||
|
||||
#[derive(Traversable)]
|
||||
@@ -128,7 +154,7 @@ pub struct AggregateVecs<M: StorageMode = Rw> {
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct Vecs<M: StorageMode = Rw> {
|
||||
pub age_range: AgeRange<CohortVecs<M>>,
|
||||
pub age_range: AgeRangeVecs<M>,
|
||||
#[traversable(flatten)]
|
||||
pub all: AggregateVecs<M>,
|
||||
pub sth: AggregateVecs<M>,
|
||||
|
||||
@@ -2,5 +2,4 @@ mod compute;
|
||||
mod import;
|
||||
mod vecs;
|
||||
|
||||
pub(crate) use compute::compute_rest;
|
||||
pub use vecs::{DerivedVecs, Vecs};
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use brk_cohort::{AGE_RANGE_BOUNDS, AGE_RANGE_COUNT};
|
||||
use brk_cohort::{AgeRange, AgeRangeId};
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_types::{Bitcoin, Height, ONE_DAY_IN_SEC_F64, Sats, StoredF64, Timestamp, Version};
|
||||
use vecdb::{AnyVec, Exit, ReadableVec};
|
||||
use vecdb::{AnyVec, CheckedSub, ColumnId, Exit, ReadableVec, StoredVec, WritableVec};
|
||||
|
||||
use super::super::activity;
|
||||
use super::{CohortVecs, SupplyVecs, Vecs};
|
||||
use crate::{distribution, indexes};
|
||||
use super::Vecs;
|
||||
use crate::{distribution, frameworks::WeightedCohortState, indexes};
|
||||
|
||||
const HOURS_PER_DAY: f64 = 24.0;
|
||||
const WRITE_INTERVAL: usize = 10_000;
|
||||
@@ -20,19 +19,29 @@ impl Vecs {
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let starting_height = indexer.safe_lengths().height;
|
||||
let source_cohorts: Vec<_> = distribution.utxo_cohorts.age_range.iter().collect();
|
||||
let supplies: Vec<_> = source_cohorts
|
||||
.iter()
|
||||
.map(|cohort| &cohort.metrics.supply.total.sats.height)
|
||||
.collect();
|
||||
let transfer_volumes: Vec<_> = source_cohorts
|
||||
.iter()
|
||||
.map(|cohort| &cohort.metrics.activity.transfer_volume.block.sats)
|
||||
.collect();
|
||||
let coindays_destroyed: Vec<_> = source_cohorts
|
||||
.iter()
|
||||
.map(|cohort| &cohort.metrics.activity.coindays_destroyed.block)
|
||||
.collect();
|
||||
let supplies = AgeRange::from_fn(|id| {
|
||||
&id.select(&distribution.utxo_cohorts.age_range)
|
||||
.metrics
|
||||
.supply
|
||||
.total
|
||||
.sats
|
||||
.height
|
||||
});
|
||||
let transfer_volumes = AgeRange::from_fn(|id| {
|
||||
&id.select(&distribution.utxo_cohorts.age_range)
|
||||
.metrics
|
||||
.activity
|
||||
.transfer_volume
|
||||
.block
|
||||
.sats
|
||||
});
|
||||
let coindays_destroyed = AgeRange::from_fn(|id| {
|
||||
&id.select(&distribution.utxo_cohorts.age_range)
|
||||
.metrics
|
||||
.activity
|
||||
.coindays_destroyed
|
||||
.block
|
||||
});
|
||||
|
||||
self.compute_created(
|
||||
starting_height,
|
||||
@@ -53,116 +62,73 @@ impl Vecs {
|
||||
&mut self,
|
||||
starting_height: Height,
|
||||
timestamps: &T,
|
||||
supplies: &[&S],
|
||||
supplies: &AgeRange<&S>,
|
||||
exit: &Exit,
|
||||
) -> Result<()>
|
||||
where
|
||||
T: ReadableVec<Height, Timestamp>,
|
||||
S: ReadableVec<Height, Sats>,
|
||||
{
|
||||
debug_assert_eq!(supplies.len(), AGE_RANGE_COUNT);
|
||||
|
||||
let created_version: Version = std::iter::once(timestamps.version())
|
||||
let version: Version = std::iter::once(timestamps.version())
|
||||
.chain(supplies.iter().map(|vec| vec.version()))
|
||||
.sum();
|
||||
let mut cohorts: Vec<&mut CohortVecs> = self.iter_mut().collect();
|
||||
|
||||
for cohort in cohorts.iter_mut() {
|
||||
cohort
|
||||
.coindays_created
|
||||
.validate_computed_version_or_reset(created_version)?;
|
||||
}
|
||||
|
||||
let start = cohorts
|
||||
.iter()
|
||||
.map(|cohort| cohort.coindays_created.cumulative.height.len())
|
||||
.min()
|
||||
.unwrap_or_default()
|
||||
.min(usize::from(starting_height));
|
||||
|
||||
for cohort in cohorts.iter_mut() {
|
||||
cohort.coindays_created.truncate_if_needed_at(start)?;
|
||||
}
|
||||
|
||||
let source_end = supplies
|
||||
.iter()
|
||||
.map(|vec| vec.len())
|
||||
.chain(std::iter::once(timestamps.len()))
|
||||
.min()
|
||||
.unwrap_or_default();
|
||||
self.coindays_created.cumulative.compute_batched_to(
|
||||
starting_height,
|
||||
source_end,
|
||||
version,
|
||||
WRITE_INTERVAL,
|
||||
|created, range| {
|
||||
let mut cumulative = created
|
||||
.collect_last()
|
||||
.unwrap_or_else(|| AgeRangeId::from_fn(|_| StoredF64::default()));
|
||||
let timestamp_start = range.start.saturating_sub(1);
|
||||
let timestamp_batch = timestamps.collect_range_at(timestamp_start, range.end);
|
||||
let supply_batches = AgeRange::from_fn(|id| {
|
||||
id.select(supplies).collect_range_at(range.start, range.end)
|
||||
});
|
||||
|
||||
let mut chunk_start = start;
|
||||
while chunk_start < source_end {
|
||||
let chunk_end = (chunk_start + WRITE_INTERVAL).min(source_end);
|
||||
let timestamp_start = chunk_start.saturating_sub(1);
|
||||
let timestamp_batch = timestamps.collect_range_at(timestamp_start, chunk_end);
|
||||
let supply_batches: Vec<_> = supplies
|
||||
.iter()
|
||||
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
|
||||
.collect();
|
||||
|
||||
for (offset, _) in supply_batches.first().unwrap().iter().enumerate() {
|
||||
let interval_seconds =
|
||||
monotonic_interval_seconds(×tamp_batch, chunk_start, offset);
|
||||
|
||||
for (index, cohort) in cohorts.iter_mut().enumerate() {
|
||||
cohort.coindays_created.push_block(coindays_created(
|
||||
supply_batches[index][offset],
|
||||
interval_seconds,
|
||||
));
|
||||
for offset in 0..range.len() {
|
||||
let interval_seconds =
|
||||
monotonic_interval_seconds(×tamp_batch, range.start, offset);
|
||||
created.push(AgeRangeId::from_fn(|column| {
|
||||
let value = column.get_mut(&mut cumulative);
|
||||
*value += coindays_created(
|
||||
column.select(&supply_batches)[offset],
|
||||
interval_seconds,
|
||||
);
|
||||
*value
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let _lock = exit.lock();
|
||||
for cohort in cohorts.iter_mut() {
|
||||
cohort.coindays_created.write()?;
|
||||
}
|
||||
}
|
||||
chunk_start = chunk_end;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compute_consumed<V, D>(
|
||||
&mut self,
|
||||
starting_height: Height,
|
||||
transfer_volumes: &[&V],
|
||||
source_coindays_destroyed: &[&D],
|
||||
transfer_volumes: &AgeRange<&V>,
|
||||
source_coindays_destroyed: &AgeRange<&D>,
|
||||
exit: &Exit,
|
||||
) -> Result<()>
|
||||
where
|
||||
V: ReadableVec<Height, Sats>,
|
||||
D: ReadableVec<Height, StoredF64>,
|
||||
{
|
||||
debug_assert_eq!(transfer_volumes.len(), AGE_RANGE_COUNT);
|
||||
debug_assert_eq!(source_coindays_destroyed.len(), AGE_RANGE_COUNT);
|
||||
|
||||
let destroyed_version: Version = transfer_volumes
|
||||
let version: Version = transfer_volumes
|
||||
.iter()
|
||||
.map(|vec| vec.version())
|
||||
.chain(source_coindays_destroyed.iter().map(|vec| vec.version()))
|
||||
.sum();
|
||||
let mut cohorts: Vec<&mut CohortVecs> = self.iter_mut().collect();
|
||||
|
||||
for cohort in cohorts.iter_mut() {
|
||||
cohort
|
||||
.coindays_consumed
|
||||
.validate_computed_version_or_reset(destroyed_version)?;
|
||||
}
|
||||
|
||||
let start = cohorts
|
||||
.iter()
|
||||
.map(|cohort| cohort.coindays_consumed.cumulative.height.len())
|
||||
.min()
|
||||
.unwrap_or_default()
|
||||
.min(usize::from(starting_height));
|
||||
|
||||
for cohort in cohorts.iter_mut() {
|
||||
cohort.coindays_consumed.truncate_if_needed_at(start)?;
|
||||
}
|
||||
|
||||
let source_end = transfer_volumes
|
||||
.iter()
|
||||
.map(|vec| vec.len())
|
||||
@@ -170,107 +136,156 @@ impl Vecs {
|
||||
.min()
|
||||
.unwrap_or_default();
|
||||
let bounds = age_bounds_days();
|
||||
|
||||
let mut chunk_start = start;
|
||||
while chunk_start < source_end {
|
||||
let chunk_end = (chunk_start + WRITE_INTERVAL).min(source_end);
|
||||
let transfer_batches: Vec<_> = transfer_volumes
|
||||
.iter()
|
||||
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
|
||||
.collect();
|
||||
let destroyed_batches: Vec<_> = source_coindays_destroyed
|
||||
.iter()
|
||||
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
|
||||
.collect();
|
||||
|
||||
for offset in 0..(chunk_end - chunk_start) {
|
||||
let volumes_btc: [f64; AGE_RANGE_COUNT] = std::array::from_fn(|index| {
|
||||
f64::from(Bitcoin::from(transfer_batches[index][offset]))
|
||||
self.coindays_consumed.cumulative.compute_batched_to(
|
||||
starting_height,
|
||||
source_end,
|
||||
version,
|
||||
WRITE_INTERVAL,
|
||||
|target, range| {
|
||||
let mut cumulative = target
|
||||
.collect_last()
|
||||
.unwrap_or_else(|| AgeRangeId::from_fn(|_| StoredF64::default()));
|
||||
let transfer_batches = AgeRange::from_fn(|id| {
|
||||
id.select(transfer_volumes)
|
||||
.collect_range_at(range.start, range.end)
|
||||
});
|
||||
let destroyed_batches = AgeRange::from_fn(|id| {
|
||||
id.select(source_coindays_destroyed)
|
||||
.collect_range_at(range.start, range.end)
|
||||
});
|
||||
let cdd: [f64; AGE_RANGE_COUNT] =
|
||||
std::array::from_fn(|index| f64::from(destroyed_batches[index][offset]));
|
||||
let consumed = allocate_consumed_coindays(volumes_btc, cdd, &bounds);
|
||||
|
||||
for (index, cohort) in cohorts.iter_mut().enumerate() {
|
||||
cohort
|
||||
.coindays_consumed
|
||||
.push_block(StoredF64::from(consumed[index]));
|
||||
for offset in 0..range.len() {
|
||||
let volumes_btc = AgeRange::from_fn(|id| {
|
||||
f64::from(Bitcoin::from(id.select(&transfer_batches)[offset]))
|
||||
});
|
||||
let coindays_destroyed =
|
||||
AgeRange::from_fn(|id| f64::from(id.select(&destroyed_batches)[offset]));
|
||||
let consumed =
|
||||
allocate_consumed_coindays(volumes_btc, coindays_destroyed, &bounds);
|
||||
target.push(AgeRangeId::from_fn(|column| {
|
||||
let value = column.get_mut(&mut cumulative);
|
||||
*value += StoredF64::from(*column.select(&consumed));
|
||||
*value
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let _lock = exit.lock();
|
||||
for cohort in cohorts.iter_mut() {
|
||||
cohort.coindays_consumed.write()?;
|
||||
}
|
||||
}
|
||||
chunk_start = chunk_end;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compute_rest<S>(
|
||||
&mut self,
|
||||
starting_height: Height,
|
||||
supplies: &[&S],
|
||||
supplies: &AgeRange<&S>,
|
||||
exit: &Exit,
|
||||
) -> Result<()>
|
||||
where
|
||||
S: ReadableVec<Height, Sats>,
|
||||
{
|
||||
debug_assert_eq!(supplies.len(), AGE_RANGE_COUNT);
|
||||
|
||||
for (cohort, &total_supply) in self.iter_mut().zip(supplies) {
|
||||
let CohortVecs {
|
||||
coindays_created,
|
||||
coindays_consumed,
|
||||
coindays_stored,
|
||||
activity: activity_vecs,
|
||||
supply,
|
||||
} = cohort;
|
||||
|
||||
activity::compute_rest(
|
||||
{
|
||||
let created = self.coindays_created.cumulative.read_only_clone();
|
||||
let consumed = self.coindays_consumed.cumulative.read_only_clone();
|
||||
self.coindays_stored.cumulative.compute_transform2_batched(
|
||||
starting_height,
|
||||
coindays_created,
|
||||
coindays_consumed,
|
||||
coindays_stored,
|
||||
&mut activity_vecs.wakefulness,
|
||||
&created,
|
||||
&consumed,
|
||||
WRITE_INTERVAL,
|
||||
|(height, created, consumed, ..)| {
|
||||
let stored = AgeRangeId::from_fn(|column| {
|
||||
(*column.get(&created))
|
||||
.checked_sub(*column.get(&consumed))
|
||||
.expect("coindays stored underflow")
|
||||
});
|
||||
(height, stored)
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
|
||||
supply.compute_from(
|
||||
self.activity.height.compute_transform2_batched(
|
||||
starting_height,
|
||||
total_supply,
|
||||
&activity_vecs.wakefulness.height,
|
||||
&activity_vecs.dormancy.height,
|
||||
&consumed,
|
||||
&created,
|
||||
WRITE_INTERVAL,
|
||||
|(height, consumed, created, ..)| {
|
||||
let wakefulness = AgeRangeId::from_fn(|column| {
|
||||
*column.get(&consumed) / *column.get(&created)
|
||||
});
|
||||
(height, wakefulness)
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
self.compute_supply(starting_height, supplies, exit)
|
||||
}
|
||||
}
|
||||
|
||||
impl SupplyVecs {
|
||||
fn compute_from(
|
||||
fn compute_supply<S>(
|
||||
&mut self,
|
||||
starting_height: Height,
|
||||
total_supply: &impl ReadableVec<Height, Sats>,
|
||||
wakefulness: &impl ReadableVec<Height, StoredF64>,
|
||||
dormancy: &impl ReadableVec<Height, StoredF64>,
|
||||
supplies: &AgeRange<&S>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.awake.sats.height.compute_multiply(
|
||||
starting_height,
|
||||
total_supply,
|
||||
wakefulness,
|
||||
exit,
|
||||
)?;
|
||||
self.dormant
|
||||
.sats
|
||||
.height
|
||||
.compute_multiply(starting_height, total_supply, dormancy, exit)?;
|
||||
) -> Result<()>
|
||||
where
|
||||
S: ReadableVec<Height, Sats>,
|
||||
{
|
||||
let wakefulness = self.activity.height.read_only_clone();
|
||||
let source_version: Version = std::iter::once(wakefulness.version())
|
||||
.chain(supplies.iter().map(|vec| vec.version()))
|
||||
.sum();
|
||||
let supply = &mut self.supply;
|
||||
supply
|
||||
.awake
|
||||
.validate_computed_version_or_reset(source_version)?;
|
||||
supply
|
||||
.dormant
|
||||
.validate_computed_version_or_reset(source_version)?;
|
||||
|
||||
let start = [supply.awake.height.len(), supply.dormant.height.len()]
|
||||
.into_iter()
|
||||
.min()
|
||||
.unwrap_or_default()
|
||||
.min(usize::from(starting_height));
|
||||
supply.awake.truncate_if_needed_at(start)?;
|
||||
supply.dormant.truncate_if_needed_at(start)?;
|
||||
|
||||
let source_end = supplies
|
||||
.iter()
|
||||
.map(|vec| vec.len())
|
||||
.chain(std::iter::once(wakefulness.len()))
|
||||
.min()
|
||||
.unwrap_or_default();
|
||||
let mut chunk_start = start;
|
||||
while chunk_start < source_end {
|
||||
let chunk_end = (chunk_start + WRITE_INTERVAL).min(source_end);
|
||||
let supply_batches = AgeRange::from_fn(|id| {
|
||||
id.select(supplies).collect_range_at(chunk_start, chunk_end)
|
||||
});
|
||||
let wakefulness_batch = wakefulness.collect_range_at(chunk_start, chunk_end);
|
||||
|
||||
for (offset, weights) in wakefulness_batch.iter().enumerate() {
|
||||
let split = AgeRange::from_fn(|id| {
|
||||
WeightedCohortState::split_supply(
|
||||
id.select(&supply_batches)[offset],
|
||||
*id.get(weights),
|
||||
)
|
||||
});
|
||||
supply
|
||||
.awake
|
||||
.push(AgeRangeId::from_fn(|id| id.select(&split).0));
|
||||
supply
|
||||
.dormant
|
||||
.push(AgeRangeId::from_fn(|id| id.select(&split).1));
|
||||
}
|
||||
|
||||
{
|
||||
let _lock = exit.lock();
|
||||
supply.awake.write()?;
|
||||
supply.dormant.write()?;
|
||||
}
|
||||
chunk_start = chunk_end;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -295,34 +310,34 @@ fn coindays_created(supply: Sats, interval_seconds: u32) -> StoredF64 {
|
||||
StoredF64::from(f64::from(Bitcoin::from(supply)) * interval_seconds as f64 / ONE_DAY_IN_SEC_F64)
|
||||
}
|
||||
|
||||
fn age_bounds_days() -> [(f64, f64); AGE_RANGE_COUNT] {
|
||||
let mut bounds = AGE_RANGE_BOUNDS.iter();
|
||||
std::array::from_fn(|index| {
|
||||
let bound = bounds.next().unwrap();
|
||||
fn age_bounds_days() -> AgeRange<(f64, f64)> {
|
||||
AgeRange::from_fn(|id| {
|
||||
let bound = id.bounds();
|
||||
let lower = bound.start as f64 / HOURS_PER_DAY;
|
||||
let width = if index + 1 < AGE_RANGE_COUNT {
|
||||
(bound.end - bound.start) as f64 / HOURS_PER_DAY
|
||||
} else {
|
||||
let width = if id == AgeRangeId::Over15Y {
|
||||
0.0
|
||||
} else {
|
||||
(bound.end - bound.start) as f64 / HOURS_PER_DAY
|
||||
};
|
||||
(lower, width)
|
||||
})
|
||||
}
|
||||
|
||||
fn allocate_consumed_coindays(
|
||||
transfer_volume_btc: [f64; AGE_RANGE_COUNT],
|
||||
coindays_destroyed: [f64; AGE_RANGE_COUNT],
|
||||
bounds: &[(f64, f64); AGE_RANGE_COUNT],
|
||||
) -> [f64; AGE_RANGE_COUNT] {
|
||||
let mut result = [0.0; AGE_RANGE_COUNT];
|
||||
transfer_volume_btc: AgeRange<f64>,
|
||||
coindays_destroyed: AgeRange<f64>,
|
||||
bounds: &AgeRange<(f64, f64)>,
|
||||
) -> AgeRange<f64> {
|
||||
let mut result = AgeRange::default();
|
||||
let mut older_transfer_volume = 0.0;
|
||||
|
||||
for index in (0..AGE_RANGE_COUNT).rev() {
|
||||
let (lower_days, width_days) = bounds[index];
|
||||
for &id in AgeRangeId::ALL.iter().rev() {
|
||||
let (lower_days, width_days) = *id.select(bounds);
|
||||
let transfer_volume = *id.select(&transfer_volume_btc);
|
||||
let within_cohort =
|
||||
(coindays_destroyed[index] - transfer_volume_btc[index] * lower_days).max(0.0);
|
||||
result[index] = within_cohort + older_transfer_volume * width_days;
|
||||
older_transfer_volume += transfer_volume_btc[index];
|
||||
(*id.select(&coindays_destroyed) - transfer_volume * lower_days).max(0.0);
|
||||
*id.select_mut(&mut result) = within_cohort + older_transfer_volume * width_days;
|
||||
older_transfer_volume += transfer_volume;
|
||||
}
|
||||
|
||||
result
|
||||
@@ -330,6 +345,8 @@ fn allocate_consumed_coindays(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use brk_cohort::AGE_RANGE_COUNT;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@@ -355,67 +372,76 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn destruction_at_a_boundary_stays_in_the_ranges_already_traversed() {
|
||||
let mut volumes = [0.0; AGE_RANGE_COUNT];
|
||||
let mut cdd = [0.0; AGE_RANGE_COUNT];
|
||||
volumes[2] = 1.0;
|
||||
cdd[2] = 1.0;
|
||||
let mut volumes = AgeRange::default();
|
||||
let mut cdd = AgeRange::default();
|
||||
volumes._1d_to_1w = 1.0;
|
||||
cdd._1d_to_1w = 1.0;
|
||||
|
||||
let allocated = allocate_consumed_coindays(volumes, cdd, &age_bounds_days());
|
||||
|
||||
assert!((allocated[0] - 1.0 / HOURS_PER_DAY).abs() < 1e-12);
|
||||
assert!((allocated[1] - 23.0 / HOURS_PER_DAY).abs() < 1e-12);
|
||||
assert!(allocated[2].abs() < 1e-12);
|
||||
assert!((allocated.under_1h - 1.0 / HOURS_PER_DAY).abs() < 1e-12);
|
||||
assert!((allocated._1h_to_1d - 23.0 / HOURS_PER_DAY).abs() < 1e-12);
|
||||
assert!(allocated._1d_to_1w.abs() < 1e-12);
|
||||
assert!((allocated.iter().sum::<f64>() - 1.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consumed_coindays_cover_every_traversed_cohort() {
|
||||
let mut volumes = [0.0; AGE_RANGE_COUNT];
|
||||
let mut cdd = [0.0; AGE_RANGE_COUNT];
|
||||
volumes[2] = 2.0;
|
||||
cdd[2] = 20.0;
|
||||
let mut volumes = AgeRange::default();
|
||||
let mut cdd = AgeRange::default();
|
||||
volumes._1d_to_1w = 2.0;
|
||||
cdd._1d_to_1w = 20.0;
|
||||
|
||||
let allocated = allocate_consumed_coindays(volumes, cdd, &age_bounds_days());
|
||||
|
||||
assert!((allocated[0] - 2.0 / HOURS_PER_DAY).abs() < 1e-12);
|
||||
assert!((allocated[1] - 46.0 / HOURS_PER_DAY).abs() < 1e-12);
|
||||
assert!((allocated[2] - 18.0).abs() < 1e-12);
|
||||
assert!((allocated.under_1h - 2.0 / HOURS_PER_DAY).abs() < 1e-12);
|
||||
assert!((allocated._1h_to_1d - 46.0 / HOURS_PER_DAY).abs() < 1e-12);
|
||||
assert!((allocated._1d_to_1w - 18.0).abs() < 1e-12);
|
||||
assert!((allocated.iter().sum::<f64>() - 20.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocated_coindays_conserve_mixed_cohort_destruction() {
|
||||
let bounds = age_bounds_days();
|
||||
let mut volumes = [0.0; AGE_RANGE_COUNT];
|
||||
let mut cdd = [0.0; AGE_RANGE_COUNT];
|
||||
let mut volumes = AgeRange::default();
|
||||
let mut cdd = AgeRange::default();
|
||||
|
||||
for index in [0, 1, 2, 10, 20, AGE_RANGE_COUNT - 2, AGE_RANGE_COUNT - 1] {
|
||||
let (lower, width) = bounds[index];
|
||||
let volume = index as f64 + 1.0;
|
||||
for id in [
|
||||
AgeRangeId::Under1H,
|
||||
AgeRangeId::From1HTo1D,
|
||||
AgeRangeId::From1DTo1W,
|
||||
AgeRangeId::From9MTo1Y,
|
||||
AgeRangeId::From10YTo12Y,
|
||||
AgeRangeId::From12YTo15Y,
|
||||
AgeRangeId::Over15Y,
|
||||
] {
|
||||
let (lower, width) = *id.select(&bounds);
|
||||
let volume = id.index() as f64 + 1.0;
|
||||
let age = lower + if width > 0.0 { width / 2.0 } else { 30.0 };
|
||||
volumes[index] = volume;
|
||||
cdd[index] = volume * age;
|
||||
*id.select_mut(&mut volumes) = volume;
|
||||
*id.select_mut(&mut cdd) = volume * age;
|
||||
}
|
||||
|
||||
let expected = cdd.iter().sum::<f64>();
|
||||
let allocated = allocate_consumed_coindays(volumes, cdd, &bounds);
|
||||
|
||||
assert!((allocated.iter().sum::<f64>() - cdd.iter().sum::<f64>()).abs() < 1e-9);
|
||||
assert!((allocated.iter().sum::<f64>() - expected).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounds_and_allocation_cover_every_canonical_age_range() {
|
||||
let bounds = age_bounds_days();
|
||||
let mut volumes = [0.0; AGE_RANGE_COUNT];
|
||||
let mut cdd = [0.0; AGE_RANGE_COUNT];
|
||||
let last = AGE_RANGE_COUNT - 1;
|
||||
let mut volumes = AgeRange::default();
|
||||
let mut cdd = AgeRange::default();
|
||||
|
||||
volumes[last] = 1.0;
|
||||
cdd[last] = bounds[last].0 + 30.0;
|
||||
volumes.over_15y = 1.0;
|
||||
cdd.over_15y = bounds.over_15y.0 + 30.0;
|
||||
|
||||
let expected = cdd.iter().sum::<f64>();
|
||||
let allocated = allocate_consumed_coindays(volumes, cdd, &bounds);
|
||||
|
||||
assert_eq!(bounds.len(), AGE_RANGE_BOUNDS.iter().count());
|
||||
assert!(allocated[last] > 0.0);
|
||||
assert!((allocated.iter().sum::<f64>() - cdd.iter().sum::<f64>()).abs() < 1e-9);
|
||||
assert_eq!(bounds.iter().count(), AGE_RANGE_COUNT);
|
||||
assert!(allocated.over_15y > 0.0);
|
||||
assert!((allocated.iter().sum::<f64>() - expected).abs() < 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,110 +1,59 @@
|
||||
use brk_cohort::{AgeRange, CohortContext};
|
||||
use brk_cohort::{AgeRangeId, CohortContext};
|
||||
use brk_error::Result;
|
||||
use brk_types::{Cents, Height, Version};
|
||||
use vecdb::{CachedBoxedVec, Database, ReadableCloneableVec};
|
||||
use brk_types::{Cents, Height, StoredF64, Version};
|
||||
use vecdb::{CachedBoxedVec, Database, PcoVec, ReadOnlyColumnarVec, ReadableCloneableVec};
|
||||
|
||||
use super::{ActivityVecs, CohortVecs, SupplyVecs, Vecs};
|
||||
use super::{ActivitySeries, SupplyVecs, Vecs};
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
CachedWindowStartVec, LazyPerBlock, OddsF64, OneMinusF64, PerBlock,
|
||||
PerBlockCumulativeRolling, SpotValuePerBlock, Windows,
|
||||
CachedWindowStartVec, ColumnarPerBlock, ColumnarPerBlockCumulativeRolling,
|
||||
LazyColumnPerBlock, LazyColumnPerBlockCumulativeRolling, LazyColumnSpotValuePerBlock,
|
||||
LazyPerBlock, OddsF64, OneMinusF64, Windows,
|
||||
},
|
||||
};
|
||||
|
||||
const VERSION: Version = Version::ONE;
|
||||
const VERSION: Version = Version::TWO;
|
||||
|
||||
impl ActivityVecs {
|
||||
fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
impl ActivitySeries {
|
||||
fn new(
|
||||
version: Version,
|
||||
source: &ReadOnlyColumnarVec<PcoVec<Height, StoredF64>, AgeRangeId>,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
let wakefulness =
|
||||
PerBlock::forced_import(db, &format!("{name}_wakefulness"), version, indexes)?;
|
||||
let dormancy = LazyPerBlock::from_computed::<OneMinusF64>(
|
||||
&format!("{name}_dormancy"),
|
||||
version,
|
||||
wakefulness.height.read_only_boxed_clone(),
|
||||
&wakefulness,
|
||||
);
|
||||
let wakefulness_to_dormancy = LazyPerBlock::from_computed::<OddsF64>(
|
||||
&format!("{name}_wakefulness_to_dormancy"),
|
||||
version,
|
||||
wakefulness.height.read_only_boxed_clone(),
|
||||
&wakefulness,
|
||||
);
|
||||
) -> Self {
|
||||
let wakefulness = AgeRangeId::series(CohortContext::Utxo, |column, name| {
|
||||
LazyColumnPerBlock::new(
|
||||
&format!("{name}_wakefulness"),
|
||||
version,
|
||||
source,
|
||||
column,
|
||||
indexes,
|
||||
)
|
||||
});
|
||||
let dormancy = AgeRangeId::series(CohortContext::Utxo, |column, name| {
|
||||
let wakefulness = column.select(&wakefulness);
|
||||
LazyPerBlock::from_resolutions::<OneMinusF64>(
|
||||
&format!("{name}_dormancy"),
|
||||
version,
|
||||
wakefulness.height.read_only_boxed_clone(),
|
||||
&wakefulness.resolutions,
|
||||
)
|
||||
});
|
||||
let wakefulness_to_dormancy = AgeRangeId::series(CohortContext::Utxo, |column, name| {
|
||||
let wakefulness = column.select(&wakefulness);
|
||||
LazyPerBlock::from_resolutions::<OddsF64>(
|
||||
&format!("{name}_wakefulness_to_dormancy"),
|
||||
version,
|
||||
wakefulness.height.read_only_boxed_clone(),
|
||||
&wakefulness.resolutions,
|
||||
)
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
Self {
|
||||
wakefulness,
|
||||
dormancy,
|
||||
wakefulness_to_dormancy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SupplyVecs {
|
||||
fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
awake: SpotValuePerBlock::forced_import(
|
||||
db,
|
||||
&format!("{name}_awake_supply"),
|
||||
version,
|
||||
indexes,
|
||||
spot_price,
|
||||
)?,
|
||||
dormant: SpotValuePerBlock::forced_import(
|
||||
db,
|
||||
&format!("{name}_dormant_supply"),
|
||||
version,
|
||||
indexes,
|
||||
spot_price,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CohortVecs {
|
||||
fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
coindays_created: PerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
&format!("{name}_coindays_created"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?,
|
||||
coindays_consumed: PerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
&format!("{name}_coindays_consumed"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?,
|
||||
coindays_stored: PerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
&format!("{name}_coindays_stored"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?,
|
||||
activity: ActivityVecs::forced_import(db, name, version, indexes)?,
|
||||
supply: SupplyVecs::forced_import(db, name, version, indexes, spot_price)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,10 +66,93 @@ impl Vecs {
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let version = parent_version + VERSION;
|
||||
let coindays_created = ColumnarPerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
&CohortContext::Utxo.prefixed("age_range_coindays_created_cumulative"),
|
||||
version,
|
||||
|source| {
|
||||
AgeRangeId::series(CohortContext::Utxo, |column, name| {
|
||||
LazyColumnPerBlockCumulativeRolling::new(
|
||||
&format!("{name}_coindays_created"),
|
||||
version,
|
||||
source,
|
||||
column,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)
|
||||
})
|
||||
},
|
||||
)?;
|
||||
let coindays_consumed = ColumnarPerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
&CohortContext::Utxo.prefixed("age_range_coindays_consumed_cumulative"),
|
||||
version,
|
||||
|source| {
|
||||
AgeRangeId::series(CohortContext::Utxo, |column, name| {
|
||||
LazyColumnPerBlockCumulativeRolling::new(
|
||||
&format!("{name}_coindays_consumed"),
|
||||
version,
|
||||
source,
|
||||
column,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)
|
||||
})
|
||||
},
|
||||
)?;
|
||||
let coindays_stored = ColumnarPerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
&CohortContext::Utxo.prefixed("age_range_coindays_stored_cumulative"),
|
||||
version,
|
||||
|source| {
|
||||
AgeRangeId::series(CohortContext::Utxo, |column, name| {
|
||||
LazyColumnPerBlockCumulativeRolling::new(
|
||||
&format!("{name}_coindays_stored"),
|
||||
version,
|
||||
source,
|
||||
column,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)
|
||||
})
|
||||
},
|
||||
)?;
|
||||
let activity = ColumnarPerBlock::forced_import(
|
||||
db,
|
||||
&CohortContext::Utxo.prefixed("age_range_wakefulness"),
|
||||
version,
|
||||
|source| ActivitySeries::new(version, source, indexes),
|
||||
)?;
|
||||
let import_supply = |side: &str| {
|
||||
ColumnarPerBlock::forced_import(
|
||||
db,
|
||||
&CohortContext::Utxo.prefixed(&format!("age_range_{side}_supply_sats")),
|
||||
version,
|
||||
|source| {
|
||||
AgeRangeId::series(CohortContext::Utxo, |column, name| {
|
||||
LazyColumnSpotValuePerBlock::new(
|
||||
&format!("{name}_{side}_supply"),
|
||||
version,
|
||||
source,
|
||||
column,
|
||||
indexes,
|
||||
spot_price,
|
||||
)
|
||||
})
|
||||
},
|
||||
)
|
||||
};
|
||||
let supply = SupplyVecs {
|
||||
awake: import_supply("awake")?,
|
||||
dormant: import_supply("dormant")?,
|
||||
};
|
||||
|
||||
Ok(Self(AgeRange::try_new(|_, name| {
|
||||
let name = CohortContext::Utxo.prefixed(name);
|
||||
CohortVecs::forced_import(db, &name, version, indexes, cached_starts, spot_price)
|
||||
})?))
|
||||
Ok(Self {
|
||||
coindays_created,
|
||||
coindays_consumed,
|
||||
coindays_stored,
|
||||
activity,
|
||||
supply,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ mod compute;
|
||||
mod import;
|
||||
mod vecs;
|
||||
|
||||
pub use vecs::{ActivityVecs, CohortVecs, SupplyVecs, Vecs};
|
||||
pub use vecs::{ActivitySeries, SupplyVecs, Vecs};
|
||||
|
||||
@@ -1,36 +1,48 @@
|
||||
use brk_cohort::AgeRange;
|
||||
use brk_cohort::{AgeRange, AgeRangeId};
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::StoredF64;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use brk_types::{Sats, StoredF64};
|
||||
use vecdb::{Rw, StorageMode};
|
||||
|
||||
use crate::internal::{LazyPerBlock, PerBlock, PerBlockCumulativeRolling, SpotValuePerBlock};
|
||||
use crate::internal::{
|
||||
ColumnarPerBlock, ColumnarPerBlockCumulativeRolling, LazyColumnPerBlock,
|
||||
LazyColumnPerBlockCumulativeRolling, LazyColumnSpotValuePerBlock, LazyPerBlock,
|
||||
};
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct ActivityVecs<M: StorageMode = Rw> {
|
||||
pub wakefulness: PerBlock<StoredF64, M>,
|
||||
pub dormancy: LazyPerBlock<StoredF64>,
|
||||
pub wakefulness_to_dormancy: LazyPerBlock<StoredF64>,
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct ActivitySeries {
|
||||
pub wakefulness: AgeRange<LazyColumnPerBlock<StoredF64, AgeRangeId>>,
|
||||
pub dormancy: AgeRange<LazyPerBlock<StoredF64>>,
|
||||
pub wakefulness_to_dormancy: AgeRange<LazyPerBlock<StoredF64>>,
|
||||
}
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct SupplyVecs<M: StorageMode = Rw> {
|
||||
pub awake: SpotValuePerBlock<M>,
|
||||
pub dormant: SpotValuePerBlock<M>,
|
||||
pub struct SupplyVecs<T> {
|
||||
pub awake: T,
|
||||
pub dormant: T,
|
||||
}
|
||||
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct CohortVecs<M: StorageMode = Rw> {
|
||||
pub coindays_created: PerBlockCumulativeRolling<StoredF64, M>,
|
||||
pub coindays_consumed: PerBlockCumulativeRolling<StoredF64, M>,
|
||||
pub coindays_stored: PerBlockCumulativeRolling<StoredF64, M>,
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
#[traversable(flatten)]
|
||||
pub activity: ActivityVecs<M>,
|
||||
pub supply: SupplyVecs<M>,
|
||||
#[derive(Traversable)]
|
||||
pub struct Vecs<M: StorageMode = Rw> {
|
||||
pub coindays_created: ColumnarPerBlockCumulativeRolling<
|
||||
StoredF64,
|
||||
AgeRangeId,
|
||||
AgeRange<LazyColumnPerBlockCumulativeRolling<StoredF64, AgeRangeId>>,
|
||||
M,
|
||||
>,
|
||||
pub coindays_consumed: ColumnarPerBlockCumulativeRolling<
|
||||
StoredF64,
|
||||
AgeRangeId,
|
||||
AgeRange<LazyColumnPerBlockCumulativeRolling<StoredF64, AgeRangeId>>,
|
||||
M,
|
||||
>,
|
||||
pub coindays_stored: ColumnarPerBlockCumulativeRolling<
|
||||
StoredF64,
|
||||
AgeRangeId,
|
||||
AgeRange<LazyColumnPerBlockCumulativeRolling<StoredF64, AgeRangeId>>,
|
||||
M,
|
||||
>,
|
||||
pub activity: ColumnarPerBlock<StoredF64, AgeRangeId, ActivitySeries, M>,
|
||||
pub supply: SupplyVecs<
|
||||
ColumnarPerBlock<Sats, AgeRangeId, AgeRange<LazyColumnSpotValuePerBlock<AgeRangeId>>, M>,
|
||||
>,
|
||||
}
|
||||
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
#[traversable(transparent)]
|
||||
pub struct Vecs<M: StorageMode = Rw>(pub AgeRange<CohortVecs<M>>);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use brk_cohort::{AGE_RANGE_COUNT, AGE_RANGE_FILTERS, ByTerm, TERM_FILTERS};
|
||||
use brk_cohort::{AGE_RANGE_COUNT, AgeRange, AgeRangeId, ByTerm, TERM_FILTERS};
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_types::{Cents, Height, Sats, StoredF64, Version};
|
||||
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, WritableVec};
|
||||
use vecdb::{AnyStoredVec, AnyVec, ColumnId, Exit, ReadableVec, WritableVec};
|
||||
|
||||
use super::{AllCohortVecs, StoredCohortVecs, Vecs};
|
||||
use crate::{
|
||||
distribution,
|
||||
frameworks::{WeightedCohortState, realized_price},
|
||||
frameworks::WeightedCohortState,
|
||||
internal::{PerBlock, db_utils::validate_any_computed_version_or_reset},
|
||||
};
|
||||
|
||||
@@ -25,30 +25,38 @@ impl Vecs {
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let starting_height = indexer.safe_lengths().height;
|
||||
let source_cohorts: Vec<_> = distribution.utxo_cohorts.age_range.iter().collect();
|
||||
let supplies: Vec<_> = source_cohorts
|
||||
.iter()
|
||||
.map(|cohort| &cohort.metrics.supply.total.sats.height)
|
||||
.collect();
|
||||
let loss_supplies: Vec<_> = source_cohorts
|
||||
.iter()
|
||||
.map(|cohort| &cohort.metrics.supply.in_loss.sats.height)
|
||||
.collect();
|
||||
let realized_caps: Vec<_> = source_cohorts
|
||||
.iter()
|
||||
.map(|cohort| &cohort.metrics.realized.cap.cents.height)
|
||||
.collect();
|
||||
let weights: Vec<_> = age_range
|
||||
.iter()
|
||||
.map(|cohort| &cohort.wakefulness.height)
|
||||
.collect();
|
||||
let supplies = AgeRange::from_fn(|id| {
|
||||
&id.select(&distribution.utxo_cohorts.age_range)
|
||||
.metrics
|
||||
.supply
|
||||
.total
|
||||
.sats
|
||||
.height
|
||||
});
|
||||
let loss_supplies = AgeRange::from_fn(|id| {
|
||||
&id.select(&distribution.utxo_cohorts.age_range)
|
||||
.metrics
|
||||
.supply
|
||||
.in_loss
|
||||
.sats
|
||||
.height
|
||||
});
|
||||
let realized_caps = AgeRange::from_fn(|id| {
|
||||
&id.select(&distribution.utxo_cohorts.age_range)
|
||||
.metrics
|
||||
.realized
|
||||
.cap
|
||||
.cents
|
||||
.height
|
||||
});
|
||||
let weights = &age_range.activity.height;
|
||||
|
||||
self.compute_primary(
|
||||
starting_height,
|
||||
&supplies,
|
||||
&loss_supplies,
|
||||
&realized_caps,
|
||||
&weights,
|
||||
weights,
|
||||
all_supply_in_loss_share,
|
||||
exit,
|
||||
)
|
||||
@@ -58,29 +66,24 @@ impl Vecs {
|
||||
fn compute_primary<S, C, W>(
|
||||
&mut self,
|
||||
starting_height: Height,
|
||||
supplies: &[&S],
|
||||
loss_supplies: &[&S],
|
||||
realized_caps: &[&C],
|
||||
weights: &[&W],
|
||||
supplies: &AgeRange<&S>,
|
||||
loss_supplies: &AgeRange<&S>,
|
||||
realized_caps: &AgeRange<&C>,
|
||||
weights: &W,
|
||||
all_supply_in_loss_share: &mut PerBlock<StoredF64>,
|
||||
exit: &Exit,
|
||||
) -> Result<()>
|
||||
where
|
||||
S: ReadableVec<Height, Sats>,
|
||||
C: ReadableVec<Height, Cents>,
|
||||
W: ReadableVec<Height, StoredF64>,
|
||||
W: ReadableVec<Height, [StoredF64; AGE_RANGE_COUNT]>,
|
||||
{
|
||||
debug_assert_eq!(supplies.len(), AGE_RANGE_COUNT);
|
||||
debug_assert_eq!(loss_supplies.len(), AGE_RANGE_COUNT);
|
||||
debug_assert_eq!(realized_caps.len(), AGE_RANGE_COUNT);
|
||||
debug_assert_eq!(weights.len(), AGE_RANGE_COUNT);
|
||||
|
||||
let source_version: Version = supplies
|
||||
.iter()
|
||||
.map(|vec| vec.version())
|
||||
.chain(loss_supplies.iter().map(|vec| vec.version()))
|
||||
.chain(realized_caps.iter().map(|vec| vec.version()))
|
||||
.chain(weights.iter().map(|vec| vec.version()))
|
||||
.chain(std::iter::once(weights.version()))
|
||||
.sum();
|
||||
|
||||
for vec in self.primary_vecs_mut() {
|
||||
@@ -91,7 +94,6 @@ impl Vecs {
|
||||
|
||||
let start = self
|
||||
.primary_vecs_mut()
|
||||
.into_iter()
|
||||
.map(|vec| vec.len())
|
||||
.chain(std::iter::once(all_supply_in_loss_share.len()))
|
||||
.min()
|
||||
@@ -108,47 +110,39 @@ impl Vecs {
|
||||
.map(|vec| vec.len())
|
||||
.chain(loss_supplies.iter().map(|vec| vec.len()))
|
||||
.chain(realized_caps.iter().map(|vec| vec.len()))
|
||||
.chain(weights.iter().map(|vec| vec.len()))
|
||||
.chain(std::iter::once(weights.len()))
|
||||
.min()
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut age_filters = AGE_RANGE_FILTERS.iter();
|
||||
let is_sth: [bool; AGE_RANGE_COUNT] =
|
||||
std::array::from_fn(|_| TERM_FILTERS.short.includes(age_filters.next().unwrap()));
|
||||
|
||||
let mut chunk_start = start;
|
||||
while chunk_start < source_end {
|
||||
let chunk_end = (chunk_start + WRITE_INTERVAL).min(source_end);
|
||||
let supply_batches: Vec<_> = supplies
|
||||
.iter()
|
||||
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
|
||||
.collect();
|
||||
let loss_batches: Vec<_> = loss_supplies
|
||||
.iter()
|
||||
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
|
||||
.collect();
|
||||
let cap_batches: Vec<_> = realized_caps
|
||||
.iter()
|
||||
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
|
||||
.collect();
|
||||
let weight_batches: Vec<_> = weights
|
||||
.iter()
|
||||
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
|
||||
.collect();
|
||||
let supply_batches = AgeRange::from_fn(|id| {
|
||||
id.select(supplies).collect_range_at(chunk_start, chunk_end)
|
||||
});
|
||||
let loss_batches = AgeRange::from_fn(|id| {
|
||||
id.select(loss_supplies)
|
||||
.collect_range_at(chunk_start, chunk_end)
|
||||
});
|
||||
let cap_batches = AgeRange::from_fn(|id| {
|
||||
id.select(realized_caps)
|
||||
.collect_range_at(chunk_start, chunk_end)
|
||||
});
|
||||
let weight_batch = weights.collect_range_at(chunk_start, chunk_end);
|
||||
|
||||
for offset in 0..(chunk_end - chunk_start) {
|
||||
for (offset, weights) in weight_batch.iter().enumerate() {
|
||||
let mut terms = ByTerm::<WeightedCohortState>::default();
|
||||
for age in 0..AGE_RANGE_COUNT {
|
||||
let term = if is_sth[age] {
|
||||
for &id in AgeRangeId::ALL {
|
||||
let term = if TERM_FILTERS.short.includes(id.filter()) {
|
||||
&mut terms.short
|
||||
} else {
|
||||
&mut terms.long
|
||||
};
|
||||
term.add(
|
||||
supply_batches[age][offset],
|
||||
loss_batches[age][offset],
|
||||
cap_batches[age][offset],
|
||||
weight_batches[age][offset],
|
||||
id.select(&supply_batches)[offset],
|
||||
id.select(&loss_batches)[offset],
|
||||
id.select(&cap_batches)[offset],
|
||||
*id.get(weights),
|
||||
);
|
||||
}
|
||||
let all = terms.short.merged(terms.long);
|
||||
@@ -171,12 +165,12 @@ impl Vecs {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn primary_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
let mut vecs = Vec::with_capacity(ALL_PRIMARY_VEC_COUNT + 2 * STORED_PRIMARY_VEC_COUNT);
|
||||
vecs.extend(self.all.primary_vecs_mut());
|
||||
vecs.extend(self.sth.primary_vecs_mut());
|
||||
vecs.extend(self.lth.primary_vecs_mut());
|
||||
vecs
|
||||
fn primary_vecs_mut(&mut self) -> impl Iterator<Item = &mut dyn AnyStoredVec> {
|
||||
self.all
|
||||
.primary_vecs_mut()
|
||||
.into_iter()
|
||||
.chain(self.sth.primary_vecs_mut())
|
||||
.chain(self.lth.primary_vecs_mut())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,11 +183,7 @@ impl AllCohortVecs {
|
||||
.height
|
||||
.push(state.complement_supply);
|
||||
self.awake.cap.cents.height.push(state.weighted_cap);
|
||||
self.awake
|
||||
.price
|
||||
.cents
|
||||
.height
|
||||
.push(realized_price(state.weighted_cap, state.weighted_supply));
|
||||
self.awake.price.cents.height.push(state.realized_price());
|
||||
}
|
||||
|
||||
fn primary_vecs_mut(&mut self) -> [&mut dyn AnyStoredVec; ALL_PRIMARY_VEC_COUNT] {
|
||||
@@ -219,11 +209,7 @@ impl StoredCohortVecs {
|
||||
.height
|
||||
.push(state.supply_in_loss.value());
|
||||
self.awake.cap.cents.height.push(state.weighted_cap);
|
||||
self.awake
|
||||
.price
|
||||
.cents
|
||||
.height
|
||||
.push(realized_price(state.weighted_cap, state.weighted_supply));
|
||||
self.awake.price.cents.height.push(state.realized_price());
|
||||
}
|
||||
|
||||
fn primary_vecs_mut(&mut self) -> [&mut dyn AnyStoredVec; STORED_PRIMARY_VEC_COUNT] {
|
||||
|
||||
@@ -58,6 +58,11 @@ pub(crate) struct WeightedCohortState {
|
||||
}
|
||||
|
||||
impl WeightedCohortState {
|
||||
#[inline]
|
||||
pub(crate) fn split_supply(total: Sats, weight: StoredF64) -> (Sats, Sats) {
|
||||
(weight * total, (StoredF64::from(1.0) - weight) * total)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn add(
|
||||
&mut self,
|
||||
@@ -66,9 +71,10 @@ impl WeightedCohortState {
|
||||
total_cap: Cents,
|
||||
weight: StoredF64,
|
||||
) -> WeightedCohortContribution {
|
||||
let (weighted_supply, complement_supply) = Self::split_supply(total_supply, weight);
|
||||
let contribution = WeightedCohortContribution {
|
||||
weighted_supply: weight * total_supply,
|
||||
complement_supply: (StoredF64::from(1.0) - weight) * total_supply,
|
||||
weighted_supply,
|
||||
complement_supply,
|
||||
weighted_cap: if total_supply.is_zero() {
|
||||
Cents::ZERO
|
||||
} else {
|
||||
@@ -96,18 +102,18 @@ impl WeightedCohortState {
|
||||
self.supply_in_loss.merge(other.supply_in_loss);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn realized_price(cap: Cents, supply: Sats) -> Cents {
|
||||
if unlikely(cap.is_nan()) {
|
||||
return Cents::NAN;
|
||||
#[inline]
|
||||
pub(crate) fn realized_price(&self) -> Cents {
|
||||
if unlikely(self.weighted_cap.is_nan()) {
|
||||
return Cents::NAN;
|
||||
}
|
||||
|
||||
(self.weighted_cap.as_u128() * Sats::ONE_BTC_U128)
|
||||
.checked_div(self.weighted_supply.as_u128())
|
||||
.map(Cents::from)
|
||||
.unwrap_or(Cents::ZERO)
|
||||
}
|
||||
|
||||
(cap.as_u128() * Sats::ONE_BTC_U128)
|
||||
.checked_div(supply.as_u128())
|
||||
.map(Cents::from)
|
||||
.unwrap_or(Cents::ZERO)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -122,10 +128,7 @@ mod tests {
|
||||
|
||||
assert_eq!(contribution.weighted_cap, Cents::ZERO);
|
||||
assert_eq!(state.weighted_cap, Cents::ZERO);
|
||||
assert_eq!(
|
||||
realized_price(state.weighted_cap, state.weighted_supply),
|
||||
Cents::ZERO
|
||||
);
|
||||
assert_eq!(state.realized_price(), Cents::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -140,6 +143,6 @@ mod tests {
|
||||
);
|
||||
|
||||
assert!(state.weighted_cap.is_nan());
|
||||
assert!(realized_price(state.weighted_cap, state.weighted_supply).is_nan());
|
||||
assert!(state.realized_price().is_nan());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ pub struct Timestamps<M: StorageMode = Rw> {
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
#[traversable(flatten)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub resolutions: PerResolution<
|
||||
LazyVec<Minute10, Timestamp, Minute10, Height>,
|
||||
LazyVec<Minute30, Timestamp, Minute30, Height>,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use brk_cohort::SpendableTypeId;
|
||||
use brk_error::{OptionData, Result};
|
||||
use brk_indexer::Indexer;
|
||||
use vecdb::{AnyVec, Exit, ReadableVec, VecIndex};
|
||||
use brk_types::{StoredU16, StoredU64};
|
||||
use vecdb::{AnyVec, ColumnId, Exit, ReadableVec, VecIndex};
|
||||
|
||||
use super::Vecs;
|
||||
use crate::internal::{CoinbasePolicy, walk_blocks};
|
||||
@@ -18,13 +20,15 @@ impl Vecs {
|
||||
|
||||
self.input_count
|
||||
.validate_and_truncate(dep_version, starting_lengths.height)?;
|
||||
self.input_count.clear();
|
||||
self.tx_count
|
||||
.validate_and_truncate(dep_version, starting_lengths.height)?;
|
||||
|
||||
let skip = self
|
||||
.input_count
|
||||
.min_stateful_len()
|
||||
.min(self.tx_count.min_stateful_len());
|
||||
.height
|
||||
.len()
|
||||
.min(self.tx_count.cumulative.len());
|
||||
|
||||
let first_tx_index = &indexer.vecs().transactions.first_tx_index;
|
||||
let end = first_tx_index.len();
|
||||
@@ -59,8 +63,14 @@ impl Vecs {
|
||||
Ok(())
|
||||
},
|
||||
|agg| {
|
||||
self.input_count.push_block(&agg.entries_per_type);
|
||||
self.tx_count.push_block(&agg.txs_per_type);
|
||||
self.input_count.push(SpendableTypeId::from_fn(|column| {
|
||||
let value = agg.entries_per_type[column.output_type() as usize];
|
||||
debug_assert!(u16::try_from(value).is_ok());
|
||||
StoredU16::new(value as u16)
|
||||
}));
|
||||
self.tx_count.push_block(SpendableTypeId::from_fn(|column| {
|
||||
StoredU64::from(agg.txs_per_type[column.output_type() as usize])
|
||||
}));
|
||||
|
||||
height += 1;
|
||||
if height.is_multiple_of(WRITE_INTERVAL) {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use brk_cohort::SpendableTypeId;
|
||||
use brk_error::Result;
|
||||
use brk_types::{Height, StoredU64, Version};
|
||||
use brk_types::{Height, StoredU16, StoredU64, Version};
|
||||
use vecdb::Database;
|
||||
|
||||
use super::{Vecs, WithInputTypes};
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{CachedWindowStartVec, Windows},
|
||||
internal::{
|
||||
CachedWindowStartVec, ColumnarPerBlock, ColumnarPerBlockCumulativeRolling, Windows,
|
||||
},
|
||||
};
|
||||
|
||||
fn identity(_: Height, value: StoredU64) -> StoredU64 {
|
||||
@@ -23,34 +26,50 @@ impl Vecs {
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
let input_count_source = indexes.input_count_source();
|
||||
let input_count = WithInputTypes::forced_import_counts(
|
||||
let columnar_version = version + Version::ONE;
|
||||
let all_input_count = indexes.input_count_source();
|
||||
let input_count = ColumnarPerBlock::<StoredU16, SpendableTypeId, _>::forced_import(
|
||||
db,
|
||||
"input_count_bis",
|
||||
|t| format!("{t}_prevout_count"),
|
||||
version,
|
||||
(input_count_source, identity),
|
||||
indexes,
|
||||
cached_starts,
|
||||
"prevout_count_by_type",
|
||||
columnar_version,
|
||||
|source| {
|
||||
WithInputTypes::from_columnar_count_source(
|
||||
"input_count_bis",
|
||||
|t| format!("{t}_prevout_count"),
|
||||
columnar_version,
|
||||
(all_input_count, identity),
|
||||
source,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)
|
||||
},
|
||||
)?;
|
||||
let input_share = input_count.lazy_shares(
|
||||
version,
|
||||
columnar_version,
|
||||
|name| format!("{name}_prevout_share"),
|
||||
cached_starts,
|
||||
indexes,
|
||||
);
|
||||
let transaction_count_source = indexes.transaction_count_source();
|
||||
let tx_count = WithInputTypes::forced_import(
|
||||
db,
|
||||
"non_coinbase_tx_count",
|
||||
|t| format!("tx_count_with_{t}_prevout"),
|
||||
version,
|
||||
(transaction_count_source, without_coinbase),
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?;
|
||||
let tx_count =
|
||||
ColumnarPerBlockCumulativeRolling::<StoredU64, SpendableTypeId, _>::forced_import(
|
||||
db,
|
||||
"tx_count_with_prevout_by_type_cumulative",
|
||||
columnar_version,
|
||||
|source| {
|
||||
WithInputTypes::from_columnar_source(
|
||||
"non_coinbase_tx_count",
|
||||
|t| format!("tx_count_with_{t}_prevout"),
|
||||
columnar_version,
|
||||
(transaction_count_source, without_coinbase),
|
||||
source,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)
|
||||
},
|
||||
)?;
|
||||
let tx_share = tx_count.lazy_shares(
|
||||
version,
|
||||
columnar_version,
|
||||
|name| format!("tx_share_with_{name}_prevout"),
|
||||
cached_starts,
|
||||
indexes,
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
use brk_cohort::SpendableType;
|
||||
use brk_cohort::{SpendableType, SpendableTypeId};
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{PartsPerMillion32, StoredU64};
|
||||
use brk_types::{PartsPerMillion32, StoredU16, StoredU64};
|
||||
use vecdb::{Rw, StorageMode};
|
||||
|
||||
use super::WithInputTypes;
|
||||
use crate::internal::{
|
||||
CachedCountPerBlockCumulativeRolling, LazyPercentCumulativeRolling, PerBlockCumulativeRolling,
|
||||
ColumnarPerBlock, ColumnarPerBlockCumulativeRolling, LazyColumnCountPerBlockCumulativeRolling,
|
||||
LazyColumnPerBlockCumulativeRolling, LazyPercentCumulativeRolling,
|
||||
};
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct Vecs<M: StorageMode = Rw> {
|
||||
pub input_count: WithInputTypes<CachedCountPerBlockCumulativeRolling<M>>,
|
||||
pub input_count: ColumnarPerBlock<
|
||||
StoredU16,
|
||||
SpendableTypeId,
|
||||
WithInputTypes<LazyColumnCountPerBlockCumulativeRolling>,
|
||||
M,
|
||||
>,
|
||||
pub input_share: SpendableType<LazyPercentCumulativeRolling<PartsPerMillion32>>,
|
||||
pub tx_count: WithInputTypes<PerBlockCumulativeRolling<StoredU64, M>>,
|
||||
pub tx_count: ColumnarPerBlockCumulativeRolling<
|
||||
StoredU64,
|
||||
SpendableTypeId,
|
||||
WithInputTypes<LazyColumnPerBlockCumulativeRolling<StoredU64, SpendableTypeId>>,
|
||||
M,
|
||||
>,
|
||||
pub tx_share: SpendableType<LazyPercentCumulativeRolling<PartsPerMillion32>>,
|
||||
}
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
//! Generic `all` + per-input-type container (11 spendable types — no
|
||||
//! op_return since op_return outputs are non-spendable).
|
||||
|
||||
use brk_cohort::{ByAddrType, Filter, SpendableType};
|
||||
use brk_error::Result;
|
||||
use brk_cohort::{ByAddrType, Filter, SpendableType, SpendableTypeId};
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, PartsPerMillion32, StoredU64, Version};
|
||||
use brk_types::{Height, PartsPerMillion32, StoredU16, StoredU64, Version};
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, CachedBoxedVec, CachedReadableVec, Database, ReadableCloneableVec,
|
||||
ReadableVec, TypedVec, WritableVec,
|
||||
CachedBoxedVec, CachedReadableVec, PcoVec, ReadOnlyColumnarVec, ReadableCloneableVec,
|
||||
ReadableVec, TypedVec,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
CachedBlockCountReader, CachedCountPerBlockCumulativeRolling, CachedWindowStartVec,
|
||||
LazyPerBlockCumulativeRolling, LazyPercentCumulativeRolling, PerBlockCumulativeRolling,
|
||||
RatioU64, Windows,
|
||||
CachedBlockCountReader, CachedWindowStartVec, LazyColumnCountPerBlockCumulativeRolling,
|
||||
LazyColumnPerBlockCumulativeRolling, LazyPerBlockCumulativeRolling,
|
||||
LazyPercentCumulativeRolling, RatioU64, Windows,
|
||||
},
|
||||
};
|
||||
|
||||
/// `all` aggregate plus per-input-type breakdown across the 11 spendable
|
||||
/// output types. The "type" of an input is the previous output it spends.
|
||||
#[derive(Traversable)]
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct WithInputTypes<V> {
|
||||
pub all: LazyPerBlockCumulativeRolling<StoredU64>,
|
||||
#[traversable(skip)]
|
||||
@@ -64,20 +63,6 @@ impl<V> WithInputTypes<V> {
|
||||
}
|
||||
}
|
||||
|
||||
fn min_stateful_len_with(&self, len: impl Fn(&V) -> usize) -> usize {
|
||||
self.by_type.iter().map(len).min().unwrap()
|
||||
}
|
||||
|
||||
fn try_for_each_type_mut(&mut self, mut apply: impl FnMut(&mut V) -> Result<()>) -> Result<()> {
|
||||
self.by_type.iter_mut().try_for_each(&mut apply)
|
||||
}
|
||||
|
||||
fn push_block_with(&mut self, per_type: &[u64; 12], mut push: impl FnMut(&mut V, StoredU64)) {
|
||||
for (output_type, vec) in self.by_type.iter_typed_mut() {
|
||||
push(vec, StoredU64::from(per_type[output_type as usize]));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn lazy_share(
|
||||
&self,
|
||||
name: &str,
|
||||
@@ -102,16 +87,16 @@ impl<V> WithInputTypes<V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl WithInputTypes<CachedCountPerBlockCumulativeRolling> {
|
||||
pub(crate) fn forced_import_counts<S>(
|
||||
db: &Database,
|
||||
impl WithInputTypes<LazyColumnCountPerBlockCumulativeRolling> {
|
||||
pub(crate) fn from_columnar_count_source<S>(
|
||||
all_name: &str,
|
||||
per_type_name: impl Fn(&str) -> String,
|
||||
version: Version,
|
||||
all: (S, fn(Height, StoredU64) -> StoredU64),
|
||||
source: &ReadOnlyColumnarVec<PcoVec<Height, StoredU16>, SpendableTypeId>,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
) -> Result<Self>
|
||||
) -> Self
|
||||
where
|
||||
S: TypedVec<I = Height, T = StoredU64>
|
||||
+ ReadableVec<Height, StoredU64>
|
||||
@@ -119,36 +104,21 @@ impl WithInputTypes<CachedCountPerBlockCumulativeRolling> {
|
||||
+ Clone
|
||||
+ 'static,
|
||||
{
|
||||
let by_type = SpendableType::try_new(|_, name| {
|
||||
CachedCountPerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
&per_type_name(name),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)
|
||||
})?;
|
||||
Ok(Self::new(
|
||||
all_name,
|
||||
version,
|
||||
all,
|
||||
by_type,
|
||||
indexes,
|
||||
cached_starts,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.min_stateful_len_with(|vec| vec.min_stateful_len())
|
||||
}
|
||||
|
||||
pub(crate) fn cached_addr_type_counts(&self) -> ByAddrType<CachedBlockCountReader> {
|
||||
ByAddrType::new(|filter| {
|
||||
let by_type = SpendableType::new(|filter, name| {
|
||||
let Filter::Type(output_type) = filter else {
|
||||
unreachable!()
|
||||
};
|
||||
self.by_type.get(output_type).cached_cumulative()
|
||||
})
|
||||
LazyColumnCountPerBlockCumulativeRolling::new(
|
||||
&per_type_name(name),
|
||||
version,
|
||||
source,
|
||||
SpendableTypeId::from_output_type(output_type)
|
||||
.expect("spendable output type column"),
|
||||
indexes,
|
||||
cached_starts,
|
||||
)
|
||||
});
|
||||
Self::new(all_name, version, all, by_type, indexes, cached_starts)
|
||||
}
|
||||
|
||||
pub(crate) fn lazy_shares(
|
||||
@@ -173,38 +143,30 @@ impl WithInputTypes<CachedCountPerBlockCumulativeRolling> {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn write(&mut self) -> Result<()> {
|
||||
self.try_for_each_type_mut(|vec| vec.write())
|
||||
pub(crate) fn cached_addr_type_counts(&self) -> ByAddrType<CachedBlockCountReader> {
|
||||
ByAddrType::new(|filter| {
|
||||
let Filter::Type(output_type) = filter else {
|
||||
unreachable!()
|
||||
};
|
||||
self.by_type.get(output_type).cached_cumulative()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn validate_and_truncate(
|
||||
&mut self,
|
||||
dependency_version: Version,
|
||||
at_height: Height,
|
||||
) -> Result<()> {
|
||||
self.try_for_each_type_mut(|vec| vec.validate_and_truncate(dependency_version, at_height))
|
||||
}
|
||||
|
||||
pub(crate) fn truncate_if_needed_at(&mut self, len: usize) -> Result<()> {
|
||||
self.try_for_each_type_mut(|vec| vec.truncate_if_needed_at(len))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn push_block(&mut self, per_type: &[u64; 12]) {
|
||||
self.push_block_with(per_type, |vec, value| vec.push_block(value));
|
||||
pub(crate) fn clear(&self) {
|
||||
self.by_type.iter().for_each(|count| count.clear());
|
||||
}
|
||||
}
|
||||
|
||||
impl WithInputTypes<PerBlockCumulativeRolling<StoredU64>> {
|
||||
pub(crate) fn forced_import<S>(
|
||||
db: &Database,
|
||||
impl WithInputTypes<LazyColumnPerBlockCumulativeRolling<StoredU64, SpendableTypeId>> {
|
||||
pub(crate) fn from_columnar_source<S>(
|
||||
all_name: &str,
|
||||
per_type_name: impl Fn(&str) -> String,
|
||||
version: Version,
|
||||
all: (S, fn(Height, StoredU64) -> StoredU64),
|
||||
source: &ReadOnlyColumnarVec<PcoVec<Height, StoredU64>, SpendableTypeId>,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
) -> Result<Self>
|
||||
) -> Self
|
||||
where
|
||||
S: TypedVec<I = Height, T = StoredU64>
|
||||
+ ReadableVec<Height, StoredU64>
|
||||
@@ -212,27 +174,21 @@ impl WithInputTypes<PerBlockCumulativeRolling<StoredU64>> {
|
||||
+ Clone
|
||||
+ 'static,
|
||||
{
|
||||
let by_type = SpendableType::try_new(|_, name| {
|
||||
PerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
let by_type = SpendableType::new(|filter, name| {
|
||||
let Filter::Type(output_type) = filter else {
|
||||
unreachable!()
|
||||
};
|
||||
LazyColumnPerBlockCumulativeRolling::new(
|
||||
&per_type_name(name),
|
||||
version,
|
||||
source,
|
||||
SpendableTypeId::from_output_type(output_type)
|
||||
.expect("spendable output type column"),
|
||||
indexes,
|
||||
cached_starts,
|
||||
)
|
||||
})?;
|
||||
Ok(Self::new(
|
||||
all_name,
|
||||
version,
|
||||
all,
|
||||
by_type,
|
||||
indexes,
|
||||
cached_starts,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.min_stateful_len_with(|vec| vec.cumulative.height.len())
|
||||
});
|
||||
Self::new(all_name, version, all, by_type, indexes, cached_starts)
|
||||
}
|
||||
|
||||
pub(crate) fn lazy_shares(
|
||||
@@ -255,33 +211,4 @@ impl WithInputTypes<PerBlockCumulativeRolling<StoredU64>> {
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn write(&mut self) -> Result<()> {
|
||||
self.try_for_each_type_mut(|vec| {
|
||||
vec.cumulative.height.write()?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn validate_and_truncate(
|
||||
&mut self,
|
||||
dependency_version: Version,
|
||||
at_height: Height,
|
||||
) -> Result<()> {
|
||||
self.try_for_each_type_mut(|vec| {
|
||||
Ok(vec
|
||||
.cumulative
|
||||
.height
|
||||
.validate_and_truncate(dependency_version, at_height)?)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn truncate_if_needed_at(&mut self, len: usize) -> Result<()> {
|
||||
self.try_for_each_type_mut(|vec| Ok(vec.cumulative.height.truncate_if_needed_at(len)?))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn push_block(&mut self, per_type: &[u64; 12]) {
|
||||
self.push_block_with(per_type, |vec, value| vec.push_block(value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
//! to the caller.
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_types::TxIndex;
|
||||
use brk_types::{OutputType, TxIndex};
|
||||
use vecdb::VecIndex;
|
||||
|
||||
/// Aggregated per-block counters produced by [`walk_blocks`].
|
||||
pub(crate) struct BlockAggregate {
|
||||
pub entries_per_type: [u64; 12],
|
||||
pub txs_per_type: [u64; 12],
|
||||
pub entries_per_type: [u64; OutputType::COUNT],
|
||||
pub txs_per_type: [u64; OutputType::COUNT],
|
||||
}
|
||||
|
||||
/// Whether to include the coinbase tx (first tx in each block) in the walk.
|
||||
@@ -21,7 +21,7 @@ pub(crate) enum CoinbasePolicy {
|
||||
}
|
||||
|
||||
/// Walk every block in `fi_batch`, calling `scan_tx` once per tx (which
|
||||
/// fills a `[u32; 12]` with the per-output-type count for that tx),
|
||||
/// fills a per-output-type count array for that tx),
|
||||
/// aggregating into a [`BlockAggregate`] and handing it to `store`.
|
||||
///
|
||||
#[inline]
|
||||
@@ -29,7 +29,7 @@ pub(crate) fn walk_blocks(
|
||||
fi_batch: &[TxIndex],
|
||||
txid_len: usize,
|
||||
coinbase: CoinbasePolicy,
|
||||
mut scan_tx: impl FnMut(usize, &mut [u32; 12]) -> Result<()>,
|
||||
mut scan_tx: impl FnMut(usize, &mut [u32; OutputType::COUNT]) -> Result<()>,
|
||||
mut store: impl FnMut(BlockAggregate) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
for (j, first_tx) in fi_batch.iter().enumerate() {
|
||||
@@ -44,11 +44,11 @@ pub(crate) fn walk_blocks(
|
||||
CoinbasePolicy::Skip => fi + 1,
|
||||
};
|
||||
|
||||
let mut entries_per_type = [0u64; 12];
|
||||
let mut txs_per_type = [0u64; 12];
|
||||
let mut entries_per_type = [0u64; OutputType::COUNT];
|
||||
let mut txs_per_type = [0u64; OutputType::COUNT];
|
||||
|
||||
for tx_pos in start_tx..next_fi {
|
||||
let mut per_tx = [0u32; 12];
|
||||
let mut per_tx = [0u32; OutputType::COUNT];
|
||||
scan_tx(tx_pos, &mut per_tx)?;
|
||||
for (i, &n) in per_tx.iter().enumerate() {
|
||||
if n > 0 {
|
||||
|
||||
@@ -1,4 +1,96 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::Version;
|
||||
use vecdb::{ColumnId, VecValue};
|
||||
|
||||
const WINDOW_COUNT: usize = 4;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum WindowId {
|
||||
Day1,
|
||||
Week1,
|
||||
Month1,
|
||||
Year1,
|
||||
}
|
||||
|
||||
const WINDOW_IDS: [WindowId; WINDOW_COUNT] = [
|
||||
WindowId::Day1,
|
||||
WindowId::Week1,
|
||||
WindowId::Month1,
|
||||
WindowId::Year1,
|
||||
];
|
||||
|
||||
impl WindowId {
|
||||
pub const fn suffix(self) -> &'static str {
|
||||
match self {
|
||||
Self::Day1 => "24h",
|
||||
Self::Week1 => "1w",
|
||||
Self::Month1 => "1m",
|
||||
Self::Year1 => "1y",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn series<T>(mut create: impl FnMut(Self) -> T) -> Windows<T> {
|
||||
Windows {
|
||||
_24h: create(Self::Day1),
|
||||
_1w: create(Self::Week1),
|
||||
_1m: create(Self::Month1),
|
||||
_1y: create(Self::Year1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select<T>(self, windows: &Windows<T>) -> &T {
|
||||
match self {
|
||||
Self::Day1 => &windows._24h,
|
||||
Self::Week1 => &windows._1w,
|
||||
Self::Month1 => &windows._1m,
|
||||
Self::Year1 => &windows._1y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColumnId for WindowId {
|
||||
type Row<T>
|
||||
= [T; WINDOW_COUNT]
|
||||
where
|
||||
T: VecValue;
|
||||
|
||||
const VERSION: Version = Version::ONE;
|
||||
const ALL: &'static [Self] = &WINDOW_IDS;
|
||||
|
||||
#[inline]
|
||||
fn index(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
|
||||
&row[self.index()]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
|
||||
&mut row[self.index()]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn from_fn<T, F>(mut create: F) -> Self::Row<T>
|
||||
where
|
||||
T: VecValue,
|
||||
F: FnMut(Self) -> T,
|
||||
{
|
||||
std::array::from_fn(|index| create(WINDOW_IDS[index]))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn map<T, U, F>(row: Self::Row<T>, create: F) -> Self::Row<U>
|
||||
where
|
||||
T: VecValue,
|
||||
U: VecValue,
|
||||
F: FnMut(T) -> U,
|
||||
{
|
||||
row.map(create)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Traversable)]
|
||||
pub struct Windows<A> {
|
||||
@@ -79,3 +171,21 @@ impl<A, B> Windows<(A, B)> {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use vecdb::ColumnId;
|
||||
|
||||
use super::{WINDOW_IDS, WindowId};
|
||||
|
||||
#[test]
|
||||
fn window_columns_match_named_fields() {
|
||||
assert_eq!(WindowId::ALL, WINDOW_IDS);
|
||||
|
||||
let windows = WindowId::series(|window| window);
|
||||
assert_eq!(windows._24h, WindowId::Day1);
|
||||
assert_eq!(windows._1w, WindowId::Week1);
|
||||
assert_eq!(windows._1m, WindowId::Month1);
|
||||
assert_eq!(windows._1y, WindowId::Year1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,101 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::Version;
|
||||
use vecdb::{ColumnId, VecValue};
|
||||
|
||||
use super::Windows;
|
||||
|
||||
const WINDOW_FROM_1W_COUNT: usize = 3;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum WindowFrom1wId {
|
||||
Week1,
|
||||
Month1,
|
||||
Year1,
|
||||
}
|
||||
|
||||
const WINDOW_FROM_1W_IDS: [WindowFrom1wId; WINDOW_FROM_1W_COUNT] = [
|
||||
WindowFrom1wId::Week1,
|
||||
WindowFrom1wId::Month1,
|
||||
WindowFrom1wId::Year1,
|
||||
];
|
||||
|
||||
impl WindowFrom1wId {
|
||||
pub const fn suffix(self) -> &'static str {
|
||||
match self {
|
||||
Self::Week1 => "1w",
|
||||
Self::Month1 => "1m",
|
||||
Self::Year1 => "1y",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn series<T>(mut create: impl FnMut(Self) -> T) -> WindowsFrom1w<T> {
|
||||
WindowsFrom1w {
|
||||
_1w: create(Self::Week1),
|
||||
_1m: create(Self::Month1),
|
||||
_1y: create(Self::Year1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select<T>(self, windows: &WindowsFrom1w<T>) -> &T {
|
||||
match self {
|
||||
Self::Week1 => &windows._1w,
|
||||
Self::Month1 => &windows._1m,
|
||||
Self::Year1 => &windows._1y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_full<T>(self, windows: &Windows<T>) -> &T {
|
||||
match self {
|
||||
Self::Week1 => &windows._1w,
|
||||
Self::Month1 => &windows._1m,
|
||||
Self::Year1 => &windows._1y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColumnId for WindowFrom1wId {
|
||||
type Row<T>
|
||||
= [T; WINDOW_FROM_1W_COUNT]
|
||||
where
|
||||
T: VecValue;
|
||||
|
||||
const VERSION: Version = Version::ONE;
|
||||
const ALL: &'static [Self] = &WINDOW_FROM_1W_IDS;
|
||||
|
||||
#[inline]
|
||||
fn index(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
|
||||
&row[self.index()]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
|
||||
&mut row[self.index()]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn from_fn<T, F>(mut create: F) -> Self::Row<T>
|
||||
where
|
||||
T: VecValue,
|
||||
F: FnMut(Self) -> T,
|
||||
{
|
||||
std::array::from_fn(|index| create(WINDOW_FROM_1W_IDS[index]))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn map<T, U, F>(row: Self::Row<T>, create: F) -> Self::Row<U>
|
||||
where
|
||||
T: VecValue,
|
||||
U: VecValue,
|
||||
F: FnMut(T) -> U,
|
||||
{
|
||||
row.map(create)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct WindowsFrom1w<A> {
|
||||
@@ -28,3 +125,20 @@ impl<A> WindowsFrom1w<A> {
|
||||
[&mut self._1w, &mut self._1m, &mut self._1y]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use vecdb::ColumnId;
|
||||
|
||||
use super::{WINDOW_FROM_1W_IDS, WindowFrom1wId};
|
||||
|
||||
#[test]
|
||||
fn window_from_1w_columns_match_named_fields() {
|
||||
assert_eq!(WindowFrom1wId::ALL, WINDOW_FROM_1W_IDS);
|
||||
|
||||
let windows = WindowFrom1wId::series(|window| window);
|
||||
assert_eq!(windows._1w, WindowFrom1wId::Week1);
|
||||
assert_eq!(windows._1m, WindowFrom1wId::Month1);
|
||||
assert_eq!(windows._1y, WindowFrom1wId::Year1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ use brk_types::{
|
||||
Date, Day1, Day3, Epoch, Halving, Height, Hour1, Hour4, Hour12, Minute10, Minute30, Month1,
|
||||
Month3, Month6, Timestamp, Version, Week1, Year1, Year10,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
use vecdb::{
|
||||
AnyExportableVec, AnyVec, Database, EagerVec, Formattable, ImportableVec, LazyVec, PcoVec,
|
||||
PcoVecValue, ReadableBoxedVec, ReadableCloneableVec, ReadableVec, Rw, StorageMode, TypedVec,
|
||||
UnaryTransform, VecIndex, VecValue, short_type_name,
|
||||
AnyExportableVec, AnyStoredVec, AnyVec, ColumnId, ColumnarVec, Database, EagerVec, Formattable,
|
||||
ImportableVec, LazyColumnVec, LazyVec, PcoVec, PcoVecValue, ReadOnlyClone, ReadOnlyColumnarVec,
|
||||
ReadableBoxedVec, ReadableCloneableVec, ReadableColumnarVec, ReadableVec, Rw, StorageMode,
|
||||
TypedVec, UnaryTransform, VecIndex, VecValue, WritableVec, short_type_name,
|
||||
};
|
||||
|
||||
use crate::indexes;
|
||||
@@ -172,6 +174,83 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
#[traversable(merge)]
|
||||
pub struct LazyColumnDailyMetric<T, C>
|
||||
where
|
||||
T: DailyValue + PcoVecValue,
|
||||
C: ColumnId,
|
||||
{
|
||||
pub day1: LazyColumnVec<ReadOnlyColumnarVec<PcoVec<Day1, T>, C>, C>,
|
||||
#[traversable(flatten)]
|
||||
pub views: Box<DailyViews<T>>,
|
||||
}
|
||||
|
||||
impl<T, C> LazyColumnDailyMetric<T, C>
|
||||
where
|
||||
T: DailyValue + PcoVecValue,
|
||||
C: ColumnId,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source: &ReadOnlyColumnarVec<PcoVec<Day1, T>, C>,
|
||||
column: C,
|
||||
mappings: &DailyMappings,
|
||||
) -> Self {
|
||||
let day1 = source.column(name, version, column);
|
||||
let views = Box::new(DailyViews::new(
|
||||
name,
|
||||
day1.read_only_boxed_clone(),
|
||||
version,
|
||||
mappings,
|
||||
));
|
||||
|
||||
Self { day1, views }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct ColumnarDailyMetric<T, C, S: Clone, M: StorageMode = Rw>
|
||||
where
|
||||
T: DailyValue + PcoVecValue,
|
||||
C: ColumnId,
|
||||
{
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
#[traversable(flatten)]
|
||||
pub series: S,
|
||||
#[traversable(hidden)]
|
||||
pub day1: M::Stored<EagerVec<ColumnarVec<PcoVec<Day1, T>, C>>>,
|
||||
}
|
||||
|
||||
impl<T, C, S: Clone> ColumnarDailyMetric<T, C, S>
|
||||
where
|
||||
T: DailyValue + PcoVecValue,
|
||||
C: ColumnId,
|
||||
{
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
build_series: impl FnOnce(&ReadOnlyColumnarVec<PcoVec<Day1, T>, C>) -> S,
|
||||
) -> Result<Self> {
|
||||
let day1 = EagerVec::forced_import(db, name, version)?;
|
||||
let series = build_series(&day1.read_only_clone());
|
||||
|
||||
Ok(Self { series, day1 })
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push(&mut self, row: C::Row<T>) {
|
||||
self.day1.push(row);
|
||||
}
|
||||
|
||||
pub(crate) fn stored_mut(&mut self) -> &mut dyn AnyStoredVec {
|
||||
&mut self.day1
|
||||
}
|
||||
}
|
||||
|
||||
type LazyDay<T, S> = LazyVec<Day1, T, Day1, S>;
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::internal::{ComputedVecValue, PerResolution};
|
||||
#[derive(Clone, Deref, DerefMut, Traversable)]
|
||||
#[traversable(transparent)]
|
||||
pub struct LazyIndexes<T, S>(
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub PerResolution<
|
||||
LazyVec<Minute10, T, Minute10, S>,
|
||||
LazyVec<Minute30, T, Minute30, S>,
|
||||
|
||||
-217
@@ -1,217 +0,0 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, StoredU16, StoredU64, Version};
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, CachedVec, Database, EagerVec, ImportableVec, LazyVec, PcoVec, Rw,
|
||||
StorageMode, WritableVec,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
CachedBlockCountReader, CachedWindowStartVec, Identity, LazyPerBlock,
|
||||
LazyRollingAvgsFromHeight, LazyRollingSumsFromHeight, StoredU16ToStoredU64, Windows,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct CachedCountPerBlockCumulativeRolling<M: StorageMode = Rw> {
|
||||
pub block: LazyVec<Height, StoredU64, Height, StoredU16>,
|
||||
pub cumulative: LazyPerBlock<StoredU64>,
|
||||
pub sum: LazyRollingSumsFromHeight<StoredU64>,
|
||||
pub average: LazyRollingAvgsFromHeight<StoredU64>,
|
||||
#[traversable(hidden)]
|
||||
source: CachedVec<M::Stored<EagerVec<PcoVec<Height, StoredU16>>>>,
|
||||
#[traversable(skip)]
|
||||
cached_cumulative: CachedBlockCountReader,
|
||||
}
|
||||
|
||||
impl CachedCountPerBlockCumulativeRolling {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
let source = CachedVec::wrap(EagerVec::forced_import(db, name, version)?);
|
||||
let cached_cumulative = CachedBlockCountReader::new(source.read_only_cached_boxed_clone());
|
||||
let block = LazyVec::transformed::<StoredU16ToStoredU64>(
|
||||
name,
|
||||
version,
|
||||
source.read_only_boxed_clone(),
|
||||
);
|
||||
let cumulative = LazyPerBlock::from_uncached_height_source::<Identity<StoredU64>, _>(
|
||||
&format!("{name}_cumulative"),
|
||||
version,
|
||||
cached_cumulative.clone(),
|
||||
indexes,
|
||||
);
|
||||
let sum = LazyRollingSumsFromHeight::new_uncached(
|
||||
&format!("{name}_sum"),
|
||||
version,
|
||||
&cached_cumulative,
|
||||
cached_starts,
|
||||
indexes,
|
||||
);
|
||||
let average = LazyRollingAvgsFromHeight::new(
|
||||
&format!("{name}_average"),
|
||||
version,
|
||||
&cached_cumulative,
|
||||
cached_starts,
|
||||
indexes,
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
block,
|
||||
cumulative,
|
||||
sum,
|
||||
average,
|
||||
source,
|
||||
cached_cumulative,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn cached_cumulative(&self) -> CachedBlockCountReader {
|
||||
self.cached_cumulative.clone()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.source.len()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_block(&mut self, value: StoredU64) {
|
||||
let value = u64::from(value);
|
||||
debug_assert!(u16::try_from(value).is_ok());
|
||||
self.source.inner.push(StoredU16::new(value as u16));
|
||||
}
|
||||
|
||||
pub(crate) fn validate_and_truncate(
|
||||
&mut self,
|
||||
dependency_version: Version,
|
||||
at_height: Height,
|
||||
) -> Result<()> {
|
||||
self.source
|
||||
.inner
|
||||
.validate_and_truncate(dependency_version, at_height)?;
|
||||
self.source.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn truncate_if_needed_at(&mut self, len: usize) -> Result<()> {
|
||||
self.source.inner.truncate_if_needed_at(len)?;
|
||||
self.source.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn write(&mut self) -> Result<()> {
|
||||
self.source.inner.write()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use brk_indexer::Indexer;
|
||||
use brk_reader::Reader;
|
||||
use brk_rpc::{Auth, Client};
|
||||
use brk_traversable::Traversable;
|
||||
use vecdb::{AnyVec, ReadableVec};
|
||||
|
||||
use super::*;
|
||||
use crate::blocks::LookbackVecs;
|
||||
|
||||
#[test]
|
||||
fn owns_retains_and_refreshes_its_compact_source() {
|
||||
let suffix = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"brk-cached-count-owner-{}-{suffix}",
|
||||
std::process::id()
|
||||
));
|
||||
|
||||
let client = Client::new("http://127.0.0.1:1", Auth::None).unwrap();
|
||||
let reader = Reader::new_without_rlimit(path.join("blocks"), &client);
|
||||
let indexer = Indexer::import(&path, &reader).unwrap();
|
||||
let indexes = indexes::Vecs::forced_import(&path, Version::ONE, &indexer).unwrap();
|
||||
let lookback = LookbackVecs::new(
|
||||
Version::ONE,
|
||||
indexes.timestamp.monotonic.read_only_cached_boxed_clone(),
|
||||
);
|
||||
let cached_starts = lookback.cached_window_starts();
|
||||
let db = Database::open(&path.join("counts")).unwrap();
|
||||
let mut count = CachedCountPerBlockCumulativeRolling::forced_import(
|
||||
&db,
|
||||
"count",
|
||||
Version::ONE,
|
||||
&indexes,
|
||||
&cached_starts,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
count
|
||||
.validate_and_truncate(Version::ONE, Height::ZERO)
|
||||
.unwrap();
|
||||
for value in [1_u64, 2, 3] {
|
||||
count.push_block(StoredU64::from(value));
|
||||
}
|
||||
count.write().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
count.block.collect_range_at(0, 3),
|
||||
[1_u64, 2, 3].map(StoredU64::from)
|
||||
);
|
||||
assert_eq!(
|
||||
count.cumulative.height.collect_range_at(0, 3),
|
||||
[1_u64, 3, 6].map(StoredU64::from)
|
||||
);
|
||||
|
||||
let source_regions = count.source.region_names();
|
||||
assert!(!source_regions.is_empty());
|
||||
let exportable_regions = count
|
||||
.iter_any_exportable()
|
||||
.flat_map(AnyVec::region_names)
|
||||
.collect::<Vec<_>>();
|
||||
assert!(
|
||||
source_regions
|
||||
.iter()
|
||||
.all(|region| exportable_regions.contains(region))
|
||||
);
|
||||
assert!(
|
||||
count
|
||||
.iter_any_visible()
|
||||
.flat_map(AnyVec::region_names)
|
||||
.next()
|
||||
.is_none()
|
||||
);
|
||||
|
||||
count.truncate_if_needed_at(0).unwrap();
|
||||
for value in [4_u64, 5, 6] {
|
||||
count.push_block(StoredU64::from(value));
|
||||
}
|
||||
count.write().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
count.block.collect_range_at(0, 3),
|
||||
[4_u64, 5, 6].map(StoredU64::from)
|
||||
);
|
||||
assert_eq!(
|
||||
count.cumulative.height.collect_range_at(0, 3),
|
||||
[4_u64, 9, 15].map(StoredU64::from)
|
||||
);
|
||||
|
||||
drop(count);
|
||||
drop(db);
|
||||
drop(lookback);
|
||||
drop(indexes);
|
||||
drop(indexer);
|
||||
std::fs::remove_dir_all(path).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, ColumnId, ColumnarVec, Database, EagerVec, Exit, ImportableVec, PcoVec,
|
||||
PcoVecValue, ReadOnlyClone, ReadOnlyColumnarVec, ReadableVec, Rw, StorageMode, VecValue,
|
||||
WritableVec,
|
||||
};
|
||||
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct ColumnarPerBlock<T, C, S: Clone, M: StorageMode = Rw>
|
||||
where
|
||||
T: PcoVecValue,
|
||||
C: ColumnId,
|
||||
{
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
#[traversable(flatten)]
|
||||
pub series: S,
|
||||
pub height: M::Stored<EagerVec<ColumnarVec<PcoVec<Height, T>, C>>>,
|
||||
}
|
||||
|
||||
impl<T, C, S: Clone> ColumnarPerBlock<T, C, S>
|
||||
where
|
||||
T: PcoVecValue,
|
||||
C: ColumnId,
|
||||
{
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
build_series: impl FnOnce(&ReadOnlyColumnarVec<PcoVec<Height, T>, C>) -> S,
|
||||
) -> Result<Self> {
|
||||
let height = EagerVec::forced_import(db, name, version)?;
|
||||
let series = build_series(&height.read_only_clone());
|
||||
|
||||
Ok(Self { series, height })
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push(&mut self, row: C::Row<T>) {
|
||||
self.height.push(row);
|
||||
}
|
||||
|
||||
pub(crate) fn validate_and_truncate(&mut self, version: Version, height: Height) -> Result<()> {
|
||||
self.height
|
||||
.validate_and_truncate(version, height)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn validate_computed_version_or_reset(&mut self, version: Version) -> Result<()> {
|
||||
self.height
|
||||
.validate_computed_version_or_reset(version)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn truncate_if_needed_at(&mut self, len: usize) -> Result<()> {
|
||||
self.height.truncate_if_needed_at(len).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn write(&mut self) -> Result<()> {
|
||||
self.height.write().map(|_| ()).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn stored_mut(&mut self) -> &mut dyn AnyStoredVec {
|
||||
&mut self.height
|
||||
}
|
||||
|
||||
/// Computes one stored matrix from two scalar sources per column.
|
||||
pub(crate) fn compute_columns2<'a, A, B, V1, V2>(
|
||||
&mut self,
|
||||
max_from: Height,
|
||||
source1: impl Fn(C) -> &'a V1,
|
||||
source2: impl Fn(C) -> &'a V2,
|
||||
mut transform: impl FnMut(C, A, B) -> T,
|
||||
exit: &Exit,
|
||||
) -> Result<()>
|
||||
where
|
||||
A: VecValue,
|
||||
B: VecValue,
|
||||
V1: ReadableVec<Height, A> + 'a,
|
||||
V2: ReadableVec<Height, B> + 'a,
|
||||
{
|
||||
let dependency_version = C::ALL
|
||||
.iter()
|
||||
.flat_map(|&column| [source1(column).version(), source2(column).version()])
|
||||
.sum();
|
||||
let source_end = C::ALL
|
||||
.iter()
|
||||
.flat_map(|&column| [source1(column).len(), source2(column).len()])
|
||||
.min()
|
||||
.unwrap_or_default();
|
||||
|
||||
self.height
|
||||
.validate_computed_version_or_reset(dependency_version)?;
|
||||
self.height.truncate_if_needed(max_from)?;
|
||||
self.height.repeat_until_complete(exit, |target| {
|
||||
let start = target.len();
|
||||
let end = target.batch_end(source_end);
|
||||
if start >= end {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let source1_batches: Vec<_> = C::ALL
|
||||
.iter()
|
||||
.map(|&column| source1(column).collect_range_at(start, end))
|
||||
.collect();
|
||||
let source2_batches: Vec<_> = C::ALL
|
||||
.iter()
|
||||
.map(|&column| source2(column).collect_range_at(start, end))
|
||||
.collect();
|
||||
for offset in 0..(end - start) {
|
||||
target.push(C::from_fn(|column| {
|
||||
transform(
|
||||
column,
|
||||
source1_batches[column.index()][offset].clone(),
|
||||
source2_batches[column.index()][offset].clone(),
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Computes one stored matrix from a matrix source and one scalar source per column.
|
||||
pub(crate) fn compute_matrix_columns2<'a, A, B, V1, V2>(
|
||||
&mut self,
|
||||
max_from: Height,
|
||||
source1: &V1,
|
||||
source2: impl Fn(C) -> &'a V2,
|
||||
mut transform: impl FnMut(C, A, B) -> T,
|
||||
exit: &Exit,
|
||||
) -> Result<()>
|
||||
where
|
||||
A: VecValue,
|
||||
B: VecValue,
|
||||
V1: ReadableVec<Height, C::Row<A>>,
|
||||
V2: ReadableVec<Height, B> + 'a,
|
||||
{
|
||||
let dependency_version = C::ALL
|
||||
.iter()
|
||||
.map(|&column| source2(column).version())
|
||||
.sum::<Version>()
|
||||
+ source1.version();
|
||||
let source_end = C::ALL
|
||||
.iter()
|
||||
.map(|&column| source2(column).len())
|
||||
.chain(std::iter::once(source1.len()))
|
||||
.min()
|
||||
.unwrap_or_default();
|
||||
|
||||
self.height
|
||||
.validate_computed_version_or_reset(dependency_version)?;
|
||||
self.height.truncate_if_needed(max_from)?;
|
||||
self.height.repeat_until_complete(exit, |target| {
|
||||
let start = target.len();
|
||||
let end = target.batch_end(source_end);
|
||||
if start >= end {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let source1_rows = source1.collect_range_at(start, end);
|
||||
let source2_batches: Vec<_> = C::ALL
|
||||
.iter()
|
||||
.map(|&column| source2(column).collect_range_at(start, end))
|
||||
.collect();
|
||||
for (offset, source1_row) in source1_rows.into_iter().enumerate() {
|
||||
target.push(C::from_fn(|column| {
|
||||
transform(
|
||||
column,
|
||||
column.get(&source1_row).clone(),
|
||||
source2_batches[column.index()][offset].clone(),
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use brk_types::{Height, StoredU64, Version};
|
||||
use vecdb::{
|
||||
AnyStoredVec, ColumnId, Database, EagerVec, Exit, ImportableVec, PcoVec, ReadOnlyClone,
|
||||
ReadableVec, VecValue, WritableVec,
|
||||
};
|
||||
|
||||
use super::ColumnarPerBlock;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum Column {
|
||||
A,
|
||||
B,
|
||||
}
|
||||
|
||||
impl ColumnId for Column {
|
||||
type Row<T>
|
||||
= [T; 2]
|
||||
where
|
||||
T: VecValue;
|
||||
|
||||
const VERSION: Version = Version::ONE;
|
||||
const ALL: &'static [Self] = &[Self::A, Self::B];
|
||||
|
||||
fn index(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
|
||||
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
|
||||
&row[self.index()]
|
||||
}
|
||||
|
||||
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
|
||||
&mut row[self.index()]
|
||||
}
|
||||
|
||||
fn from_fn<T, F>(mut f: F) -> Self::Row<T>
|
||||
where
|
||||
T: VecValue,
|
||||
F: FnMut(Self) -> T,
|
||||
{
|
||||
[f(Self::A), f(Self::B)]
|
||||
}
|
||||
|
||||
fn map<T, U, F>(row: Self::Row<T>, mut f: F) -> Self::Row<U>
|
||||
where
|
||||
T: VecValue,
|
||||
U: VecValue,
|
||||
F: FnMut(T) -> U,
|
||||
{
|
||||
let [a, b] = row;
|
||||
[f(a), f(b)]
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn computes_from_scalar_columns_and_matrix_rows() {
|
||||
let suffix = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"brk-columnar-compute-{}-{suffix}",
|
||||
std::process::id()
|
||||
));
|
||||
let db = Database::open(&path).unwrap();
|
||||
|
||||
let mut left_a: EagerVec<PcoVec<Height, StoredU64>> =
|
||||
EagerVec::forced_import(&db, "left_a", Version::ONE).unwrap();
|
||||
let mut left_b: EagerVec<PcoVec<Height, StoredU64>> =
|
||||
EagerVec::forced_import(&db, "left_b", Version::ONE).unwrap();
|
||||
let mut right_a: EagerVec<PcoVec<Height, StoredU64>> =
|
||||
EagerVec::forced_import(&db, "right_a", Version::ONE).unwrap();
|
||||
let mut right_b: EagerVec<PcoVec<Height, StoredU64>> =
|
||||
EagerVec::forced_import(&db, "right_b", Version::ONE).unwrap();
|
||||
|
||||
for value in [1_u64, 2, 3] {
|
||||
left_a.push(value.into());
|
||||
}
|
||||
for value in [4_u64, 5] {
|
||||
left_b.push(value.into());
|
||||
}
|
||||
for value in [10_u64, 20, 30] {
|
||||
right_a.push(value.into());
|
||||
}
|
||||
for value in [40_u64, 50, 60] {
|
||||
right_b.push(value.into());
|
||||
}
|
||||
left_a.write().unwrap();
|
||||
left_b.write().unwrap();
|
||||
right_a.write().unwrap();
|
||||
right_b.write().unwrap();
|
||||
|
||||
let mut sums = ColumnarPerBlock::<StoredU64, Column, _>::forced_import(
|
||||
&db,
|
||||
"sums",
|
||||
Version::ONE,
|
||||
|_| (),
|
||||
)
|
||||
.unwrap();
|
||||
sums.compute_columns2(
|
||||
Height::ZERO,
|
||||
|column| match column {
|
||||
Column::A => &left_a,
|
||||
Column::B => &left_b,
|
||||
},
|
||||
|column| match column {
|
||||
Column::A => &right_a,
|
||||
Column::B => &right_b,
|
||||
},
|
||||
|_, left, right| StoredU64::from(u64::from(left) + u64::from(right)),
|
||||
&Exit::new(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
sums.height.collect_range_at(0, 3),
|
||||
[
|
||||
[StoredU64::from(11_u64), StoredU64::from(44_u64)],
|
||||
[StoredU64::from(22_u64), StoredU64::from(55_u64)],
|
||||
]
|
||||
);
|
||||
|
||||
let mut factor_a: EagerVec<PcoVec<Height, StoredU64>> =
|
||||
EagerVec::forced_import(&db, "factor_a", Version::ONE).unwrap();
|
||||
let mut factor_b: EagerVec<PcoVec<Height, StoredU64>> =
|
||||
EagerVec::forced_import(&db, "factor_b", Version::ONE).unwrap();
|
||||
for value in [2_u64, 3] {
|
||||
factor_a.push(value.into());
|
||||
}
|
||||
for value in [5_u64, 7] {
|
||||
factor_b.push(value.into());
|
||||
}
|
||||
factor_a.write().unwrap();
|
||||
factor_b.write().unwrap();
|
||||
|
||||
let sums = sums.height.read_only_clone();
|
||||
let mut products = ColumnarPerBlock::<StoredU64, Column, _>::forced_import(
|
||||
&db,
|
||||
"products",
|
||||
Version::ONE,
|
||||
|_| (),
|
||||
)
|
||||
.unwrap();
|
||||
products
|
||||
.compute_matrix_columns2(
|
||||
Height::ZERO,
|
||||
&sums,
|
||||
|column| match column {
|
||||
Column::A => &factor_a,
|
||||
Column::B => &factor_b,
|
||||
},
|
||||
|_, value, factor| StoredU64::from(u64::from(value) * u64::from(factor)),
|
||||
&Exit::new(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
products.height.collect_range_at(0, 3),
|
||||
[
|
||||
[StoredU64::from(22_u64), StoredU64::from(220_u64)],
|
||||
[StoredU64::from(66_u64), StoredU64::from(385_u64)],
|
||||
]
|
||||
);
|
||||
|
||||
drop(products);
|
||||
drop(sums);
|
||||
drop(factor_b);
|
||||
drop(factor_a);
|
||||
drop(right_b);
|
||||
drop(right_a);
|
||||
drop(left_b);
|
||||
drop(left_a);
|
||||
drop(db);
|
||||
std::fs::remove_dir_all(path).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, ColumnId, ColumnarVec, Database, EagerVec, Exit, ImportableVec, PcoVec,
|
||||
ReadOnlyClone, ReadOnlyColumnarVec, ReadableVec, Rw, StorageMode, VecValue, WritableVec,
|
||||
};
|
||||
|
||||
use crate::internal::NumericValue;
|
||||
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct ColumnarPerBlockCumulativeRolling<T, C, S: Clone, M: StorageMode = Rw>
|
||||
where
|
||||
T: NumericValue,
|
||||
C: ColumnId,
|
||||
{
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
#[traversable(flatten)]
|
||||
pub series: S,
|
||||
pub cumulative: M::Stored<EagerVec<ColumnarVec<PcoVec<Height, T>, C>>>,
|
||||
#[traversable(skip)]
|
||||
last_cumulative: Option<(usize, C::Row<T>)>,
|
||||
}
|
||||
|
||||
impl<T, C, S: Clone> ColumnarPerBlockCumulativeRolling<T, C, S>
|
||||
where
|
||||
T: NumericValue,
|
||||
C: ColumnId,
|
||||
{
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
build_series: impl FnOnce(&ReadOnlyColumnarVec<PcoVec<Height, T>, C>) -> S,
|
||||
) -> Result<Self> {
|
||||
let cumulative = EagerVec::forced_import(db, name, version)?;
|
||||
let last_cumulative = cumulative.collect_last().map(|row| (cumulative.len(), row));
|
||||
let series = build_series(&cumulative.read_only_clone());
|
||||
|
||||
Ok(Self {
|
||||
series,
|
||||
cumulative,
|
||||
last_cumulative,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_block(&mut self, row: C::Row<T>) {
|
||||
let len = self.cumulative.len();
|
||||
let mut cumulative = match &self.last_cumulative {
|
||||
Some((cached_len, row)) if *cached_len == len => row.clone(),
|
||||
_ => self
|
||||
.cumulative
|
||||
.collect_last()
|
||||
.unwrap_or_else(|| C::from_fn(|_| T::default())),
|
||||
};
|
||||
for &column in C::ALL {
|
||||
*column.get_mut(&mut cumulative) += *column.get(&row);
|
||||
}
|
||||
self.cumulative.push(cumulative.clone());
|
||||
self.last_cumulative = Some((len + 1, cumulative));
|
||||
}
|
||||
|
||||
pub(crate) fn reset(&mut self) -> Result<()> {
|
||||
self.last_cumulative = None;
|
||||
self.cumulative.reset().map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn stored_mut(&mut self) -> &mut dyn AnyStoredVec {
|
||||
self.last_cumulative = None;
|
||||
&mut self.cumulative
|
||||
}
|
||||
|
||||
pub(crate) fn validate_and_truncate(&mut self, version: Version, height: Height) -> Result<()> {
|
||||
self.cumulative
|
||||
.validate_and_truncate(version, height)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn truncate_if_needed_at(&mut self, len: usize) -> Result<()> {
|
||||
self.cumulative
|
||||
.truncate_if_needed_at(len)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn write(&mut self) -> Result<()> {
|
||||
self.cumulative.write().map(|_| ()).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Computes one cumulative matrix from one scalar per-block source per column.
|
||||
pub(crate) fn compute_columns<'a, V, U>(
|
||||
&mut self,
|
||||
max_from: Height,
|
||||
source: impl Fn(C) -> &'a V,
|
||||
exit: &Exit,
|
||||
) -> Result<()>
|
||||
where
|
||||
V: ReadableVec<Height, U> + 'a,
|
||||
U: VecValue + Into<T>,
|
||||
{
|
||||
let dependency_version = C::ALL.iter().map(|&column| source(column).version()).sum();
|
||||
let source_end = C::ALL
|
||||
.iter()
|
||||
.map(|&column| source(column).len())
|
||||
.min()
|
||||
.unwrap_or_default();
|
||||
|
||||
self.cumulative
|
||||
.validate_computed_version_or_reset(dependency_version)?;
|
||||
self.cumulative.truncate_if_needed(max_from)?;
|
||||
self.last_cumulative = None;
|
||||
let mut last_cumulative = None;
|
||||
self.cumulative.repeat_until_complete(exit, |target| {
|
||||
let start = target.len();
|
||||
let end = target.batch_end(source_end);
|
||||
if start >= end {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut batches: Vec<_> = C::ALL
|
||||
.iter()
|
||||
.map(|&column| source(column).collect_range_at(start, end).into_iter())
|
||||
.collect();
|
||||
let mut cumulative = last_cumulative.take().unwrap_or_else(|| {
|
||||
target
|
||||
.collect_last()
|
||||
.unwrap_or_else(|| C::from_fn(|_| T::default()))
|
||||
});
|
||||
for _ in start..end {
|
||||
for (&column, batch) in C::ALL.iter().zip(&mut batches) {
|
||||
*column.get_mut(&mut cumulative) +=
|
||||
batch.next().expect("complete source batch").into();
|
||||
}
|
||||
target.push(cumulative.clone());
|
||||
}
|
||||
last_cumulative = Some(cumulative);
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let len = self.cumulative.len();
|
||||
self.last_cumulative = last_cumulative
|
||||
.or_else(|| self.cumulative.collect_last())
|
||||
.map(|row| (len, row));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use brk_types::{Height, StoredU64, Version};
|
||||
use vecdb::{
|
||||
AnyStoredVec, ColumnId, Database, EagerVec, Exit, ImportableVec, PcoVec, ReadableVec,
|
||||
VecValue, WritableVec,
|
||||
};
|
||||
|
||||
use super::ColumnarPerBlockCumulativeRolling;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum Column {
|
||||
A,
|
||||
B,
|
||||
}
|
||||
|
||||
impl ColumnId for Column {
|
||||
type Row<T>
|
||||
= [T; 2]
|
||||
where
|
||||
T: VecValue;
|
||||
|
||||
const VERSION: Version = Version::ONE;
|
||||
const ALL: &'static [Self] = &[Self::A, Self::B];
|
||||
|
||||
fn index(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
|
||||
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
|
||||
&row[self.index()]
|
||||
}
|
||||
|
||||
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
|
||||
&mut row[self.index()]
|
||||
}
|
||||
|
||||
fn from_fn<T, F>(mut f: F) -> Self::Row<T>
|
||||
where
|
||||
T: VecValue,
|
||||
F: FnMut(Self) -> T,
|
||||
{
|
||||
[f(Self::A), f(Self::B)]
|
||||
}
|
||||
|
||||
fn map<T, U, F>(row: Self::Row<T>, mut f: F) -> Self::Row<U>
|
||||
where
|
||||
T: VecValue,
|
||||
U: VecValue,
|
||||
F: FnMut(T) -> U,
|
||||
{
|
||||
let [a, b] = row;
|
||||
[f(a), f(b)]
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pushes_delta_rows_and_recovers_after_truncation() {
|
||||
let suffix = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"brk-columnar-cumulative-{}-{suffix}",
|
||||
std::process::id()
|
||||
));
|
||||
let db = Database::open(&path).unwrap();
|
||||
let mut vec = ColumnarPerBlockCumulativeRolling::<StoredU64, Column, _>::forced_import(
|
||||
&db,
|
||||
"values",
|
||||
Version::ONE,
|
||||
|_| (),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vec.push_block([1_u64.into(), 2_u64.into()]);
|
||||
vec.push_block([3_u64.into(), 4_u64.into()]);
|
||||
assert_eq!(
|
||||
vec.cumulative.collect_range_at(0, 2),
|
||||
[[1_u64.into(), 2_u64.into()], [4_u64.into(), 6_u64.into()]]
|
||||
);
|
||||
|
||||
vec.truncate_if_needed_at(1).unwrap();
|
||||
vec.push_block([10_u64.into(), 20_u64.into()]);
|
||||
assert_eq!(
|
||||
vec.cumulative.collect_one(Height::new(1)),
|
||||
Some([11_u64.into(), 22_u64.into()])
|
||||
);
|
||||
|
||||
vec.write().unwrap();
|
||||
let mut a: EagerVec<PcoVec<Height, StoredU64>> =
|
||||
EagerVec::forced_import(&db, "a", Version::ONE).unwrap();
|
||||
let mut b: EagerVec<PcoVec<Height, StoredU64>> =
|
||||
EagerVec::forced_import(&db, "b", Version::ONE).unwrap();
|
||||
for value in [1_u64, 3] {
|
||||
a.push(value.into());
|
||||
}
|
||||
for value in [2_u64, 4] {
|
||||
b.push(value.into());
|
||||
}
|
||||
a.write().unwrap();
|
||||
b.write().unwrap();
|
||||
|
||||
vec.compute_columns(
|
||||
Height::ZERO,
|
||||
|column| match column {
|
||||
Column::A => &a,
|
||||
Column::B => &b,
|
||||
},
|
||||
&Exit::new(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
vec.cumulative.collect_range_at(0, 2),
|
||||
[
|
||||
[StoredU64::from(1_u64), StoredU64::from(2_u64)],
|
||||
[StoredU64::from(4_u64), StoredU64::from(6_u64)],
|
||||
]
|
||||
);
|
||||
|
||||
drop(vec);
|
||||
drop(b);
|
||||
drop(a);
|
||||
drop(db);
|
||||
std::fs::remove_dir_all(path).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
use vecdb::{
|
||||
ColumnId, Formattable, LazyColumnVec, PcoVec, PcoVecValue, ReadOnlyColumnarVec,
|
||||
ReadableColumnarVec,
|
||||
};
|
||||
|
||||
use crate::{indexes, internal::Resolutions};
|
||||
|
||||
#[derive(Clone, Deref, DerefMut, Traversable)]
|
||||
#[traversable(merge)]
|
||||
pub struct LazyColumnPerBlock<T, C>
|
||||
where
|
||||
T: PcoVecValue + Formattable + PartialOrd + Serialize + JsonSchema,
|
||||
C: ColumnId,
|
||||
{
|
||||
pub height: LazyColumnVec<ReadOnlyColumnarVec<PcoVec<Height, T>, C>, C>,
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
#[traversable(flatten)]
|
||||
pub resolutions: Box<Resolutions<T>>,
|
||||
}
|
||||
|
||||
impl<T, C> LazyColumnPerBlock<T, C>
|
||||
where
|
||||
T: PcoVecValue + Formattable + PartialOrd + Serialize + JsonSchema + 'static,
|
||||
C: ColumnId,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source: &ReadOnlyColumnarVec<PcoVec<Height, T>, C>,
|
||||
column: C,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Self {
|
||||
let height = source.column(name, version, column);
|
||||
let resolutions = Resolutions::forced_import(name, height.clone(), version, indexes);
|
||||
|
||||
Self {
|
||||
height,
|
||||
resolutions: Box::new(resolutions),
|
||||
}
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, StoredU16, StoredU64, Version};
|
||||
use vecdb::{
|
||||
CachedReadableVec, CachedVec, ColumnId, LazyVec, PcoVec, ReadOnlyColumnarVec,
|
||||
ReadableColumnarVec,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
CachedBlockCountReader, CachedWindowStartVec, Identity, LazyPerBlock,
|
||||
LazyRollingAvgsFromHeight, LazyRollingSumsFromHeight, StoredU16ToStoredU64, Windows,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct LazyColumnCountPerBlockCumulativeRolling {
|
||||
pub block: LazyVec<Height, StoredU64, Height, StoredU16>,
|
||||
pub cumulative: LazyPerBlock<StoredU64>,
|
||||
pub sum: LazyRollingSumsFromHeight<StoredU64>,
|
||||
pub average: LazyRollingAvgsFromHeight<StoredU64>,
|
||||
#[traversable(skip)]
|
||||
cached_cumulative: CachedBlockCountReader,
|
||||
}
|
||||
|
||||
impl LazyColumnCountPerBlockCumulativeRolling {
|
||||
pub(crate) fn new<C>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source: &ReadOnlyColumnarVec<PcoVec<Height, StoredU16>, C>,
|
||||
column: C,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
) -> Self
|
||||
where
|
||||
C: ColumnId,
|
||||
{
|
||||
let column = CachedVec::wrap(source.column(name, version, column));
|
||||
let cached_cumulative = CachedBlockCountReader::new(column.cached_boxed_clone());
|
||||
let block = LazyVec::transformed::<StoredU16ToStoredU64>(name, version, Box::new(column));
|
||||
let cumulative = LazyPerBlock::from_uncached_height_source::<Identity<StoredU64>, _>(
|
||||
&format!("{name}_cumulative"),
|
||||
version,
|
||||
cached_cumulative.clone(),
|
||||
indexes,
|
||||
);
|
||||
let sum = LazyRollingSumsFromHeight::new_uncached(
|
||||
&format!("{name}_sum"),
|
||||
version,
|
||||
&cached_cumulative,
|
||||
cached_starts,
|
||||
indexes,
|
||||
);
|
||||
let average = LazyRollingAvgsFromHeight::new(
|
||||
&format!("{name}_average"),
|
||||
version,
|
||||
&cached_cumulative,
|
||||
cached_starts,
|
||||
indexes,
|
||||
);
|
||||
|
||||
Self {
|
||||
block,
|
||||
cumulative,
|
||||
sum,
|
||||
average,
|
||||
cached_cumulative,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn cached_cumulative(&self) -> CachedBlockCountReader {
|
||||
self.cached_cumulative.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn clear(&self) {
|
||||
self.cached_cumulative.clear();
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Version};
|
||||
use schemars::JsonSchema;
|
||||
use vecdb::{ColumnId, PcoVec, ReadOnlyColumnarVec};
|
||||
|
||||
use super::{super::lazy_cumulative_rolling::lazy_parts, LazyColumnPerBlock};
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
CachedWindowStartVec, LazyPreviousDeltaVec, LazyRollingAvgsFromHeight,
|
||||
LazyRollingSumsFromHeight, NumericValue, Windows,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct LazyColumnPerBlockCumulativeRolling<T, C>
|
||||
where
|
||||
T: NumericValue + JsonSchema,
|
||||
C: ColumnId,
|
||||
{
|
||||
pub block: LazyPreviousDeltaVec<Height, T>,
|
||||
pub cumulative: LazyColumnPerBlock<T, C>,
|
||||
pub sum: LazyRollingSumsFromHeight<T>,
|
||||
pub average: LazyRollingAvgsFromHeight<T>,
|
||||
}
|
||||
|
||||
impl<T, C> LazyColumnPerBlockCumulativeRolling<T, C>
|
||||
where
|
||||
T: NumericValue + JsonSchema,
|
||||
C: ColumnId,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source: &ReadOnlyColumnarVec<PcoVec<Height, T>, C>,
|
||||
column: C,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
) -> Self {
|
||||
let cumulative = LazyColumnPerBlock::new(
|
||||
&format!("{name}_cumulative"),
|
||||
version,
|
||||
source,
|
||||
column,
|
||||
indexes,
|
||||
);
|
||||
let (block, sum, average) =
|
||||
lazy_parts(name, version, &cumulative.height, cached_starts, indexes);
|
||||
|
||||
Self {
|
||||
block,
|
||||
cumulative,
|
||||
sum,
|
||||
average,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
mod base;
|
||||
mod cumulative_rolling;
|
||||
mod lazy;
|
||||
mod lazy_count_cumulative_rolling;
|
||||
mod lazy_cumulative_rolling;
|
||||
|
||||
pub use base::*;
|
||||
pub use cumulative_rolling::*;
|
||||
pub use lazy::*;
|
||||
pub use lazy_count_cumulative_rolling::*;
|
||||
pub use lazy_cumulative_rolling::*;
|
||||
@@ -78,22 +78,6 @@ where
|
||||
self.last_cumulative = Some((len + 1, cumulative));
|
||||
}
|
||||
|
||||
pub(crate) fn compute_cumulative<S>(
|
||||
&mut self,
|
||||
max_from: Height,
|
||||
source: &impl ReadableVec<Height, S>,
|
||||
exit: &Exit,
|
||||
) -> Result<()>
|
||||
where
|
||||
S: VecValue + Into<T>,
|
||||
T: Copy,
|
||||
{
|
||||
Ok(self
|
||||
.cumulative
|
||||
.height
|
||||
.compute_cumulative(max_from, source, exit)?)
|
||||
}
|
||||
|
||||
pub(crate) fn compute_cumulative_transformed<S>(
|
||||
&mut self,
|
||||
max_from: Height,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Version};
|
||||
use schemars::JsonSchema;
|
||||
use vecdb::{Ident, ReadableCloneableVec, UnaryTransform};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
CachedWindowStartVec, LazyPreviousDeltaVec, LazyRollingAvgsFromHeight, NumericValue,
|
||||
Windows,
|
||||
},
|
||||
};
|
||||
|
||||
/// Lazy exact per-block values and rolling averages backed by one cumulative source.
|
||||
#[derive(Traversable)]
|
||||
pub struct LazyPerBlockCumulativeAverage<T, C = T, F = Ident>
|
||||
where
|
||||
T: NumericValue + JsonSchema,
|
||||
C: NumericValue + JsonSchema,
|
||||
F: UnaryTransform<C, T>,
|
||||
{
|
||||
pub block: LazyPreviousDeltaVec<Height, C, T, F>,
|
||||
#[traversable(flatten)]
|
||||
pub average: LazyRollingAvgsFromHeight<C>,
|
||||
}
|
||||
|
||||
impl<T, C, F> Clone for LazyPerBlockCumulativeAverage<T, C, F>
|
||||
where
|
||||
T: NumericValue + JsonSchema,
|
||||
C: NumericValue + JsonSchema,
|
||||
F: UnaryTransform<C, T>,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
block: self.block.clone(),
|
||||
average: self.average.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, C, F> LazyPerBlockCumulativeAverage<T, C, F>
|
||||
where
|
||||
T: NumericValue + JsonSchema,
|
||||
C: NumericValue + JsonSchema,
|
||||
F: UnaryTransform<C, T>,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
name: &str,
|
||||
version: Version,
|
||||
cumulative: &(impl ReadableCloneableVec<Height, C> + 'static),
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
) -> Self {
|
||||
Self {
|
||||
block: LazyPreviousDeltaVec::transformed(
|
||||
name,
|
||||
version,
|
||||
cumulative.read_only_boxed_clone(),
|
||||
),
|
||||
average: LazyRollingAvgsFromHeight::new(
|
||||
&format!("{name}_average"),
|
||||
version + Version::TWO,
|
||||
cumulative,
|
||||
cached_starts,
|
||||
indexes,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,13 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Version};
|
||||
use schemars::JsonSchema;
|
||||
use vecdb::{ReadableCloneableVec, ReadableVec, TypedVec, VecValue};
|
||||
use vecdb::{ColumnId, ReadableCloneableVec, ReadableVec, TypedVec, VecValue};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
CachedWindowStartVec, Identity, LazyPerBlock, LazyPreviousDeltaVec,
|
||||
LazyRollingAvgsFromHeight, LazyRollingSumsFromHeight, NumericValue, PerBlock, Windows,
|
||||
CachedWindowStartVec, Identity, LazyColumnPerBlock, LazyPerBlock, LazyPreviousDeltaVec,
|
||||
LazyRollingAvgsFromHeight, LazyRollingSumsFromHeight, NumericValue, Windows,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -99,18 +99,34 @@ where
|
||||
Self::from_cumulative(name, version, cumulative, cached_starts, indexes)
|
||||
}
|
||||
|
||||
pub(crate) fn from_source(
|
||||
pub(crate) fn from_lazy_source(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source: &PerBlock<T>,
|
||||
source: &LazyPerBlock<T>,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Self {
|
||||
let cumulative = LazyPerBlock::from_computed::<Identity<T>>(
|
||||
let cumulative = LazyPerBlock::from_lazy::<Identity<T>, T>(
|
||||
&format!("{name}_cumulative"),
|
||||
version,
|
||||
source,
|
||||
);
|
||||
|
||||
Self::from_cumulative(name, version, cumulative, cached_starts, indexes)
|
||||
}
|
||||
|
||||
pub(crate) fn from_column_source<C: ColumnId>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source: &LazyColumnPerBlock<T, C>,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Self {
|
||||
let cumulative = LazyPerBlock::from_resolutions::<Identity<T>>(
|
||||
&format!("{name}_cumulative"),
|
||||
version,
|
||||
source.height.read_only_boxed_clone(),
|
||||
source,
|
||||
&source.resolutions,
|
||||
);
|
||||
|
||||
Self::from_cumulative(name, version, cumulative, cached_starts, indexes)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
mod aggregated;
|
||||
mod base;
|
||||
mod cached;
|
||||
mod cached_count_cumulative_rolling;
|
||||
mod cached_cumulative_rolling;
|
||||
mod cached_rolling;
|
||||
mod columnar;
|
||||
mod cumulative_rolling;
|
||||
mod distribution;
|
||||
mod full;
|
||||
mod full_from_cumulative;
|
||||
mod lazy_cumulative_average;
|
||||
mod lazy_cumulative_rolling;
|
||||
mod lazy_distribution;
|
||||
mod lazy_rolling;
|
||||
@@ -18,13 +19,14 @@ mod with_deltas;
|
||||
pub use aggregated::*;
|
||||
pub use base::*;
|
||||
pub use cached::*;
|
||||
pub use cached_count_cumulative_rolling::*;
|
||||
pub use cached_cumulative_rolling::*;
|
||||
pub use cached_rolling::*;
|
||||
pub use columnar::*;
|
||||
pub use cumulative_rolling::*;
|
||||
pub use distribution::*;
|
||||
pub use full::*;
|
||||
pub use full_from_cumulative::*;
|
||||
pub use lazy_cumulative_average::*;
|
||||
pub use lazy_cumulative_rolling::*;
|
||||
pub use lazy_distribution::*;
|
||||
pub use lazy_rolling::*;
|
||||
|
||||
@@ -66,7 +66,6 @@ where
|
||||
#[derive(Clone, Deref, DerefMut, Traversable)]
|
||||
#[traversable(transparent)]
|
||||
pub struct Resolutions<T>(
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub PerResolution<
|
||||
LazyAggVec<Minute10, Option<T>, Height, Height, T>,
|
||||
LazyAggVec<Minute30, Option<T>, Height, Height, T>,
|
||||
|
||||
@@ -30,6 +30,10 @@ impl CachedBlockCountReader {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear(&self) {
|
||||
self.block.clear();
|
||||
}
|
||||
|
||||
pub fn cumulative_at(&self, index: usize) -> Option<StoredU64> {
|
||||
let (block, checkpoints) = self.snapshot();
|
||||
(index < block.len())
|
||||
|
||||
@@ -17,7 +17,6 @@ use super::{LazyTransformLast, MapOption};
|
||||
#[derive(Clone, Deref, DerefMut, Traversable)]
|
||||
#[traversable(transparent)]
|
||||
pub struct DerivedResolutions<T, S1T = T>(
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub PerResolution<
|
||||
LazyTransformLast<Minute10, Option<T>, Option<S1T>>,
|
||||
LazyTransformLast<Minute30, Option<T>, Option<S1T>>,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, StoredF32, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{ColumnId, PcoVec, ReadOnlyColumnarVec, ReadableCloneableVec};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{FixedRatio, LazyColumnPerBlock, LazyPerBlock, Percent},
|
||||
};
|
||||
|
||||
/// Fixed-point column projection with lazy ratio and percentage views.
|
||||
#[derive(Clone, Deref, DerefMut, Traversable)]
|
||||
#[traversable(transparent)]
|
||||
pub struct LazyColumnPercentPerBlock<B: FixedRatio, C: ColumnId>(
|
||||
pub Percent<LazyColumnPerBlock<B, C>, LazyPerBlock<StoredF32, B>>,
|
||||
);
|
||||
|
||||
impl<B: FixedRatio, C: ColumnId> LazyColumnPercentPerBlock<B, C> {
|
||||
pub(crate) fn new(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source: &ReadOnlyColumnarVec<PcoVec<Height, B>, C>,
|
||||
column: C,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Self {
|
||||
let ppm = LazyColumnPerBlock::new(
|
||||
&format!("{name}_{}", B::SUFFIX),
|
||||
version,
|
||||
source,
|
||||
column,
|
||||
indexes,
|
||||
);
|
||||
let ratio = LazyPerBlock::from_resolutions::<B::ToRatio>(
|
||||
&format!("{name}_ratio"),
|
||||
version,
|
||||
ppm.height.read_only_boxed_clone(),
|
||||
&ppm.resolutions,
|
||||
);
|
||||
let percent = LazyPerBlock::from_resolutions::<B::ToPercent>(
|
||||
name,
|
||||
version,
|
||||
ppm.height.read_only_boxed_clone(),
|
||||
&ppm.resolutions,
|
||||
);
|
||||
|
||||
Self(Percent {
|
||||
ppm,
|
||||
ratio,
|
||||
percent,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ use crate::internal::{FixedRatio, Percent};
|
||||
/// Fully lazy lightweight percent container with no derived resolutions.
|
||||
#[derive(Clone, Deref, DerefMut, Traversable)]
|
||||
#[traversable(transparent)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct LazyPercentVec<B: FixedRatio, S: VecValue>(
|
||||
pub Percent<LazyVec<Height, B, Height, S>, LazyVec<Height, StoredF32, Height, B>>,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod base;
|
||||
mod columnar;
|
||||
mod lazy;
|
||||
mod lazy_cumulative_rolling;
|
||||
mod lazy_vec;
|
||||
@@ -6,6 +7,7 @@ mod lazy_windows;
|
||||
mod windows;
|
||||
|
||||
pub use base::*;
|
||||
pub use columnar::*;
|
||||
pub use lazy::*;
|
||||
pub use lazy_cumulative_rolling::*;
|
||||
pub use lazy_vec::*;
|
||||
|
||||
@@ -6,26 +6,38 @@ use vecdb::{Database, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{FixedRatio, PercentPerBlock, Windows},
|
||||
internal::{ColumnarPerBlock, FixedRatio, LazyColumnPercentPerBlock, WindowId, Windows},
|
||||
};
|
||||
|
||||
/// 4 rolling window vecs (24h, 1w, 1m, 1y), each storing fixed-point values
|
||||
/// with lazy ratio and percent float views.
|
||||
/// Four named fixed-point percentage views backed by one columnar source.
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
#[traversable(transparent)]
|
||||
pub struct PercentRollingWindows<B: FixedRatio, M: StorageMode = Rw>(
|
||||
pub Windows<PercentPerBlock<B, M>>,
|
||||
pub struct ColumnarPercentRollingWindows<B: FixedRatio, M: StorageMode = Rw>(
|
||||
pub ColumnarPerBlock<B, WindowId, Windows<LazyColumnPercentPerBlock<B, WindowId>>, M>,
|
||||
);
|
||||
|
||||
impl<B: FixedRatio> PercentRollingWindows<B> {
|
||||
impl<B: FixedRatio> ColumnarPercentRollingWindows<B> {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(Windows::try_from_fn(|suffix| {
|
||||
PercentPerBlock::forced_import(db, &format!("{name}_{suffix}"), version, indexes)
|
||||
})?))
|
||||
Ok(Self(ColumnarPerBlock::forced_import(
|
||||
db,
|
||||
&format!("{name}_{}", B::SUFFIX),
|
||||
version,
|
||||
|source| {
|
||||
WindowId::series(|window| {
|
||||
LazyColumnPercentPerBlock::new(
|
||||
&format!("{name}_{}", window.suffix()),
|
||||
version,
|
||||
source,
|
||||
window,
|
||||
indexes,
|
||||
)
|
||||
})
|
||||
},
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,73 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::{Traversable, TreeNode};
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Cents, Version};
|
||||
use vecdb::{AnyExportableVec, Database, ReadOnlyClone, Ro, Rw, StorageMode, WritableVec};
|
||||
pub use brk_types::{PERCENTILES, PERCENTILES_LEN, PercentileId};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{Database, Rw, StorageMode};
|
||||
|
||||
use crate::indexes;
|
||||
use crate::internal::{PerBlock, Price};
|
||||
use crate::internal::{ColumnarPerBlock, LazyColumnPerBlock, Price};
|
||||
|
||||
pub const PERCENTILES: [u8; 19] = [
|
||||
5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95,
|
||||
];
|
||||
pub const PERCENTILES_LEN: usize = PERCENTILES.len();
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct PercentilePrices<T> {
|
||||
pub pct05: T,
|
||||
pub pct10: T,
|
||||
pub pct15: T,
|
||||
pub pct20: T,
|
||||
pub pct25: T,
|
||||
pub pct30: T,
|
||||
pub pct35: T,
|
||||
pub pct40: T,
|
||||
pub pct45: T,
|
||||
pub pct50: T,
|
||||
pub pct55: T,
|
||||
pub pct60: T,
|
||||
pub pct65: T,
|
||||
pub pct70: T,
|
||||
pub pct75: T,
|
||||
pub pct80: T,
|
||||
pub pct85: T,
|
||||
pub pct90: T,
|
||||
pub pct95: T,
|
||||
}
|
||||
|
||||
impl<T> PercentilePrices<T> {
|
||||
fn from_fn(mut f: impl FnMut(PercentileId) -> T) -> Self {
|
||||
Self {
|
||||
pct05: f(PercentileId::Pct05),
|
||||
pct10: f(PercentileId::Pct10),
|
||||
pct15: f(PercentileId::Pct15),
|
||||
pct20: f(PercentileId::Pct20),
|
||||
pct25: f(PercentileId::Pct25),
|
||||
pct30: f(PercentileId::Pct30),
|
||||
pct35: f(PercentileId::Pct35),
|
||||
pct40: f(PercentileId::Pct40),
|
||||
pct45: f(PercentileId::Pct45),
|
||||
pct50: f(PercentileId::Pct50),
|
||||
pct55: f(PercentileId::Pct55),
|
||||
pct60: f(PercentileId::Pct60),
|
||||
pct65: f(PercentileId::Pct65),
|
||||
pct70: f(PercentileId::Pct70),
|
||||
pct75: f(PercentileId::Pct75),
|
||||
pct80: f(PercentileId::Pct80),
|
||||
pct85: f(PercentileId::Pct85),
|
||||
pct90: f(PercentileId::Pct90),
|
||||
pct95: f(PercentileId::Pct95),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct PercentilesVecs<M: StorageMode = Rw> {
|
||||
pub vecs: [Price<PerBlock<Cents, M>>; PERCENTILES_LEN],
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
#[traversable(flatten)]
|
||||
pub prices: ColumnarPerBlock<
|
||||
Cents,
|
||||
PercentileId,
|
||||
PercentilePrices<Price<LazyColumnPerBlock<Cents, PercentileId>>>,
|
||||
M,
|
||||
>,
|
||||
}
|
||||
|
||||
const VERSION: Version = Version::ONE;
|
||||
@@ -24,64 +79,36 @@ impl PercentilesVecs {
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
let vecs = PERCENTILES
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
let series_name = format!("{prefix}_pct{p:02}");
|
||||
Price::forced_import(db, &series_name, version + VERSION, indexes)
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
.try_into()
|
||||
.ok()
|
||||
.expect("PERCENTILES length mismatch");
|
||||
let version = version + VERSION;
|
||||
let prices = ColumnarPerBlock::<Cents, PercentileId, _>::forced_import(
|
||||
db,
|
||||
&format!("{prefix}_cents"),
|
||||
version,
|
||||
|source| {
|
||||
PercentilePrices::from_fn(|id| {
|
||||
Price::from_columnar_source(
|
||||
&format!("{prefix}_pct{:02}", id.percentile()),
|
||||
version,
|
||||
source,
|
||||
id,
|
||||
indexes,
|
||||
)
|
||||
})
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(Self { vecs })
|
||||
Ok(Self { prices })
|
||||
}
|
||||
|
||||
/// Push percentile prices (in cents).
|
||||
#[inline(always)]
|
||||
pub(crate) fn push(&mut self, percentile_prices: &[Cents; PERCENTILES_LEN]) {
|
||||
for (i, v) in self.vecs.iter_mut().enumerate() {
|
||||
v.cents.height.push(percentile_prices[i]);
|
||||
}
|
||||
self.prices.push(*percentile_prices);
|
||||
}
|
||||
|
||||
/// Validate computed versions or reset if mismatched.
|
||||
pub(crate) fn validate_computed_version_or_reset(&mut self, version: Version) -> Result<()> {
|
||||
for vec in self.vecs.iter_mut() {
|
||||
vec.cents
|
||||
.height
|
||||
.validate_computed_version_or_reset(version)?;
|
||||
}
|
||||
self.prices.validate_computed_version_or_reset(version)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadOnlyClone for PercentilesVecs {
|
||||
type ReadOnly = PercentilesVecs<Ro>;
|
||||
|
||||
fn read_only_clone(&self) -> Self::ReadOnly {
|
||||
PercentilesVecs {
|
||||
vecs: self.vecs.each_ref().map(|v| v.read_only_clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: StorageMode> Traversable for PercentilesVecs<M>
|
||||
where
|
||||
Price<PerBlock<Cents, M>>: Traversable,
|
||||
{
|
||||
fn to_tree_node(&self) -> TreeNode {
|
||||
TreeNode::Branch(
|
||||
PERCENTILES
|
||||
.iter()
|
||||
.zip(self.vecs.iter())
|
||||
.map(|(p, v)| (format!("pct{p:02}"), v.to_tree_node()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn iter_any_exportable(&self) -> impl Iterator<Item = &dyn AnyExportableVec> {
|
||||
self.vecs.iter().flat_map(|p| p.iter_any_exportable())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Cents, Dollars, SatsFract, Version};
|
||||
use brk_types::{Cents, Dollars, Height, SatsFract, Version};
|
||||
use schemars::JsonSchema;
|
||||
use vecdb::{Database, ReadableCloneableVec, ReadableVec, TypedVec, UnaryTransform};
|
||||
use vecdb::{
|
||||
ColumnId, Database, PcoVec, ReadOnlyColumnarVec, ReadableCloneableVec, ReadableVec, TypedVec,
|
||||
UnaryTransform,
|
||||
};
|
||||
|
||||
use super::{LazyPerBlock, PerBlock};
|
||||
use super::{LazyColumnPerBlock, LazyPerBlock, PerBlock};
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{CentsUnsignedToDollars, ComputedVecValue, DollarsToSatsFract},
|
||||
@@ -48,6 +51,35 @@ impl Price<PerBlock<Cents>> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Price<LazyColumnPerBlock<Cents, C>>
|
||||
where
|
||||
C: ColumnId,
|
||||
{
|
||||
pub(crate) fn from_columnar_source(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source: &ReadOnlyColumnarVec<PcoVec<Height, Cents>, C>,
|
||||
column: C,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Self {
|
||||
let cents =
|
||||
LazyColumnPerBlock::new(&format!("{name}_cents"), version, source, column, indexes);
|
||||
let usd = LazyPerBlock::from_resolutions::<CentsUnsignedToDollars>(
|
||||
name,
|
||||
version,
|
||||
cents.height.read_only_boxed_clone(),
|
||||
¢s.resolutions,
|
||||
);
|
||||
let sats = LazyPerBlock::from_lazy::<DollarsToSatsFract, Cents>(
|
||||
&format!("{name}_sats"),
|
||||
version,
|
||||
&usd,
|
||||
);
|
||||
|
||||
Self { usd, cents, sats }
|
||||
}
|
||||
}
|
||||
|
||||
impl Price<LazyPerBlock<Cents, Cents>> {
|
||||
pub(crate) fn from_lazy_cents_source<F, S>(
|
||||
name: &str,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, StoredF32, Version};
|
||||
use vecdb::{ColumnId, PcoVec, ReadOnlyColumnarVec, ReadableCloneableVec};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{FixedRatio, LazyColumnPerBlock, LazyPerBlock},
|
||||
};
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct LazyColumnRatioPerBlock<R, C>
|
||||
where
|
||||
R: FixedRatio,
|
||||
C: ColumnId,
|
||||
{
|
||||
pub ppm: LazyColumnPerBlock<R, C>,
|
||||
pub ratio: LazyPerBlock<StoredF32, R>,
|
||||
}
|
||||
|
||||
impl<R, C> LazyColumnRatioPerBlock<R, C>
|
||||
where
|
||||
R: FixedRatio,
|
||||
C: ColumnId,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source: &ReadOnlyColumnarVec<PcoVec<Height, R>, C>,
|
||||
column: C,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Self {
|
||||
let ppm = LazyColumnPerBlock::new(
|
||||
&format!("{name}_{}", R::SUFFIX),
|
||||
version,
|
||||
source,
|
||||
column,
|
||||
indexes,
|
||||
);
|
||||
let ratio = LazyPerBlock::from_resolutions::<R::ToRatio>(
|
||||
name,
|
||||
version,
|
||||
ppm.height.read_only_boxed_clone(),
|
||||
&ppm.resolutions,
|
||||
);
|
||||
|
||||
Self { ppm, ratio }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Cents, Dollars, Height, PartsPerMillion64, SatsFract, StoredF32, Version};
|
||||
use vecdb::{CachedBoxedVec, ColumnId, PcoVec, ReadOnlyColumnarVec, ReadableCloneableVec};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{LazyColumnPerBlock, LazyIndexedVec, LazyPerBlock, LazyRatioPerBlock, Price},
|
||||
};
|
||||
|
||||
use super::price::price_ratio;
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct LazyColumnPriceWithRatioPerBlock<C>
|
||||
where
|
||||
C: ColumnId,
|
||||
{
|
||||
pub usd: LazyPerBlock<Dollars, Cents>,
|
||||
pub cents: LazyColumnPerBlock<Cents, C>,
|
||||
pub sats: LazyPerBlock<SatsFract, Dollars>,
|
||||
pub ppm: LazyPerBlock<PartsPerMillion64>,
|
||||
pub ratio: LazyPerBlock<StoredF32, PartsPerMillion64>,
|
||||
}
|
||||
|
||||
impl<C> LazyColumnPriceWithRatioPerBlock<C>
|
||||
where
|
||||
C: ColumnId,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source: &ReadOnlyColumnarVec<PcoVec<Height, Cents>, C>,
|
||||
column: C,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Self {
|
||||
let price = Price::from_columnar_source(name, version, source, column, indexes);
|
||||
let ratio_version = version + Version::new(4);
|
||||
let ppm_source = LazyIndexedVec::new(
|
||||
&format!("{name}_ratio_ppm_source"),
|
||||
ratio_version,
|
||||
price.cents.height.read_only_boxed_clone(),
|
||||
spot_price.clone(),
|
||||
|_, price, spot| price_ratio(spot, price),
|
||||
);
|
||||
let ratio = LazyRatioPerBlock::from_height_source(
|
||||
&format!("{name}_ratio"),
|
||||
ratio_version,
|
||||
ppm_source,
|
||||
indexes,
|
||||
);
|
||||
|
||||
Self {
|
||||
usd: price.usd,
|
||||
cents: price.cents,
|
||||
sats: price.sats,
|
||||
ppm: ratio.ppm,
|
||||
ratio: ratio.ratio,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
mod base;
|
||||
mod columnar;
|
||||
mod columnar_price;
|
||||
mod lazy;
|
||||
mod lazy_price;
|
||||
mod price;
|
||||
|
||||
pub use base::*;
|
||||
pub use columnar::*;
|
||||
pub use columnar_price::*;
|
||||
pub use lazy::*;
|
||||
pub use lazy_price::*;
|
||||
pub use price::*;
|
||||
|
||||
@@ -15,7 +15,8 @@ use crate::{
|
||||
blocks::lookback::LazyWindowStartVec,
|
||||
indexes,
|
||||
internal::{
|
||||
ComputedVecValue, NumericValue, PerBlock, RollingWindow24h, Windows, WindowsFrom1w,
|
||||
ColumnarPerBlock, ComputedVecValue, LazyColumnPerBlock, NumericValue, PerBlock,
|
||||
RollingWindow24h, WindowFrom1wId, WindowId, Windows, WindowsFrom1w,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -48,6 +49,44 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Four named rolling-window views backed by one columnar source.
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
#[traversable(transparent)]
|
||||
pub struct ColumnarRollingWindows<T, M: StorageMode = Rw>(
|
||||
pub ColumnarPerBlock<T, WindowId, Windows<LazyColumnPerBlock<T, WindowId>>, M>,
|
||||
)
|
||||
where
|
||||
T: NumericValue + JsonSchema;
|
||||
|
||||
impl<T> ColumnarRollingWindows<T>
|
||||
where
|
||||
T: NumericValue + JsonSchema,
|
||||
{
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(ColumnarPerBlock::forced_import(
|
||||
db,
|
||||
name,
|
||||
version,
|
||||
|source| {
|
||||
WindowId::series(|window| {
|
||||
LazyColumnPerBlock::new(
|
||||
&format!("{name}_{}", window.suffix()),
|
||||
version,
|
||||
source,
|
||||
window,
|
||||
indexes,
|
||||
)
|
||||
})
|
||||
},
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
/// Single 24h rolling window backed by PerBlock (1 stored vec).
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
#[traversable(transparent)]
|
||||
@@ -71,14 +110,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Extended rolling windows: 1w + 1m + 1y (3 stored vecs).
|
||||
/// The 1w, 1m, and 1y views backed by one columnar source.
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
#[traversable(transparent)]
|
||||
pub struct RollingWindowsFrom1w<T, M: StorageMode = Rw>(pub WindowsFrom1w<PerBlock<T, M>>)
|
||||
pub struct ColumnarRollingWindowsFrom1w<T, M: StorageMode = Rw>(
|
||||
pub ColumnarPerBlock<T, WindowFrom1wId, WindowsFrom1w<LazyColumnPerBlock<T, WindowFrom1wId>>, M>,
|
||||
)
|
||||
where
|
||||
T: ComputedVecValue + PartialOrd + JsonSchema;
|
||||
T: NumericValue + JsonSchema;
|
||||
|
||||
impl<T> RollingWindowsFrom1w<T>
|
||||
impl<T> ColumnarRollingWindowsFrom1w<T>
|
||||
where
|
||||
T: NumericValue + JsonSchema,
|
||||
{
|
||||
@@ -88,8 +129,21 @@ where
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(WindowsFrom1w::try_from_fn(|suffix| {
|
||||
PerBlock::forced_import(db, &format!("{name}_{suffix}"), version, indexes)
|
||||
})?))
|
||||
Ok(Self(ColumnarPerBlock::forced_import(
|
||||
db,
|
||||
name,
|
||||
version,
|
||||
|source| {
|
||||
WindowFrom1wId::series(|window| {
|
||||
LazyColumnPerBlock::new(
|
||||
&format!("{name}_{}", window.suffix()),
|
||||
version,
|
||||
source,
|
||||
window,
|
||||
indexes,
|
||||
)
|
||||
})
|
||||
},
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{
|
||||
Bitcoin, Cents, Dollars, Height, PartsPerMillionSigned64, Sats, SatsSigned, Version,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{
|
||||
BinaryTransform, CachedBoxedVec, ColumnId, PcoVec, ReadOnlyColumnarVec, ReadableCloneableVec,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
CachedWindowStartVec, CentsUnsignedToDollars, Identity, LazyColumnPerBlock, LazyIndexedVec,
|
||||
LazyPerBlock, LazyRollingAvgsAmountFromHeight, LazyRollingDeltasAmountFromHeight,
|
||||
LazyRollingSumsAmountFromHeight, LazyValueBlock, SatsToBitcoin, SatsToCents, Windows,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct LazyColumnValuePerBlock<C>
|
||||
where
|
||||
C: ColumnId,
|
||||
{
|
||||
pub btc: LazyPerBlock<Bitcoin, Sats>,
|
||||
pub sats: LazyColumnPerBlock<Sats, C>,
|
||||
pub usd: LazyPerBlock<Dollars, Cents>,
|
||||
pub cents: LazyColumnPerBlock<Cents, C>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct LazyColumnSpotValuePerBlock<C>
|
||||
where
|
||||
C: ColumnId,
|
||||
{
|
||||
pub btc: LazyPerBlock<Bitcoin, Sats>,
|
||||
pub sats: LazyColumnPerBlock<Sats, C>,
|
||||
pub usd: LazyPerBlock<Dollars, Cents>,
|
||||
pub cents: LazyPerBlock<Cents>,
|
||||
}
|
||||
|
||||
impl<C> LazyColumnSpotValuePerBlock<C>
|
||||
where
|
||||
C: ColumnId,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source: &ReadOnlyColumnarVec<PcoVec<Height, Sats>, C>,
|
||||
column: C,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Self {
|
||||
let sats =
|
||||
LazyColumnPerBlock::new(&format!("{name}_sats"), version, source, column, indexes);
|
||||
let btc = LazyPerBlock::from_resolutions::<SatsToBitcoin>(
|
||||
name,
|
||||
version,
|
||||
sats.height.read_only_boxed_clone(),
|
||||
&sats.resolutions,
|
||||
);
|
||||
let cents_source = LazyIndexedVec::new(
|
||||
&format!("{name}_cents_source"),
|
||||
version,
|
||||
sats.height.read_only_boxed_clone(),
|
||||
spot_price.clone(),
|
||||
|_, sats, spot| SatsToCents::apply(sats, spot),
|
||||
);
|
||||
let cents = LazyPerBlock::from_height_source::<Identity<Cents>, _>(
|
||||
&format!("{name}_cents"),
|
||||
version,
|
||||
cents_source,
|
||||
indexes,
|
||||
);
|
||||
let usd = LazyPerBlock::from_lazy::<CentsUnsignedToDollars, Cents>(
|
||||
&format!("{name}_usd"),
|
||||
version,
|
||||
¢s,
|
||||
);
|
||||
|
||||
Self {
|
||||
btc,
|
||||
sats,
|
||||
usd,
|
||||
cents,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deref, DerefMut, Traversable)]
|
||||
pub struct LazyColumnSpotValuePerBlockWithDeltas<C>
|
||||
where
|
||||
C: ColumnId,
|
||||
{
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
#[traversable(flatten)]
|
||||
pub inner: LazyColumnSpotValuePerBlock<C>,
|
||||
pub delta: LazyRollingDeltasAmountFromHeight<Sats, SatsSigned, PartsPerMillionSigned64>,
|
||||
}
|
||||
|
||||
impl<C> LazyColumnSpotValuePerBlockWithDeltas<C>
|
||||
where
|
||||
C: ColumnId,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source: &ReadOnlyColumnarVec<PcoVec<Height, Sats>, C>,
|
||||
column: C,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Self {
|
||||
let inner =
|
||||
LazyColumnSpotValuePerBlock::new(name, version, source, column, indexes, spot_price);
|
||||
let delta = LazyRollingDeltasAmountFromHeight::new(
|
||||
&format!("{name}_delta"),
|
||||
version + Version::TWO,
|
||||
&inner.sats.height,
|
||||
cached_starts,
|
||||
indexes,
|
||||
);
|
||||
|
||||
Self { inner, delta }
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> LazyColumnValuePerBlock<C>
|
||||
where
|
||||
C: ColumnId,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
name: &str,
|
||||
version: Version,
|
||||
sats_source: &ReadOnlyColumnarVec<PcoVec<Height, Sats>, C>,
|
||||
cents_source: &ReadOnlyColumnarVec<PcoVec<Height, Cents>, C>,
|
||||
column: C,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Self {
|
||||
let sats = LazyColumnPerBlock::new(
|
||||
&format!("{name}_sats"),
|
||||
version,
|
||||
sats_source,
|
||||
column,
|
||||
indexes,
|
||||
);
|
||||
let btc = LazyPerBlock::from_resolutions::<SatsToBitcoin>(
|
||||
name,
|
||||
version,
|
||||
sats.height.read_only_boxed_clone(),
|
||||
&sats.resolutions,
|
||||
);
|
||||
let cents = LazyColumnPerBlock::new(
|
||||
&format!("{name}_cents"),
|
||||
version,
|
||||
cents_source,
|
||||
column,
|
||||
indexes,
|
||||
);
|
||||
let usd = LazyPerBlock::from_resolutions::<CentsUnsignedToDollars>(
|
||||
&format!("{name}_usd"),
|
||||
version,
|
||||
cents.height.read_only_boxed_clone(),
|
||||
¢s.resolutions,
|
||||
);
|
||||
|
||||
Self {
|
||||
btc,
|
||||
sats,
|
||||
usd,
|
||||
cents,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct LazyColumnValuePerBlockCumulativeRolling<C>
|
||||
where
|
||||
C: ColumnId,
|
||||
{
|
||||
pub block: LazyValueBlock,
|
||||
pub cumulative: LazyColumnValuePerBlock<C>,
|
||||
pub sum: LazyRollingSumsAmountFromHeight,
|
||||
pub average: LazyRollingAvgsAmountFromHeight,
|
||||
}
|
||||
|
||||
impl<C> LazyColumnValuePerBlockCumulativeRolling<C>
|
||||
where
|
||||
C: ColumnId,
|
||||
{
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new(
|
||||
name: &str,
|
||||
version: Version,
|
||||
cumulative_sats: &ReadOnlyColumnarVec<PcoVec<Height, Sats>, C>,
|
||||
cumulative_cents: &ReadOnlyColumnarVec<PcoVec<Height, Cents>, C>,
|
||||
column: C,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
) -> Self {
|
||||
let cumulative = LazyColumnValuePerBlock::new(
|
||||
&format!("{name}_cumulative"),
|
||||
version,
|
||||
cumulative_sats,
|
||||
cumulative_cents,
|
||||
column,
|
||||
indexes,
|
||||
);
|
||||
let block = LazyValueBlock::from_cumulative_sources(
|
||||
name,
|
||||
version,
|
||||
&cumulative.sats.height,
|
||||
&cumulative.cents.height,
|
||||
);
|
||||
let sum = LazyRollingSumsAmountFromHeight::new(
|
||||
&format!("{name}_sum"),
|
||||
version,
|
||||
&cumulative.sats.height,
|
||||
&cumulative.cents.height,
|
||||
cached_starts,
|
||||
indexes,
|
||||
);
|
||||
let average = LazyRollingAvgsAmountFromHeight::new(
|
||||
&format!("{name}_average"),
|
||||
version,
|
||||
&cumulative.sats.height,
|
||||
&cumulative.cents.height,
|
||||
cached_starts,
|
||||
indexes,
|
||||
);
|
||||
|
||||
Self {
|
||||
block,
|
||||
cumulative,
|
||||
sum,
|
||||
average,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
mod base;
|
||||
mod cached;
|
||||
mod cached_full;
|
||||
mod columnar;
|
||||
mod cumulative;
|
||||
mod cumulative_rolling;
|
||||
mod lazy;
|
||||
@@ -11,11 +12,11 @@ mod lazy_rolling_sum;
|
||||
mod lazy_spot;
|
||||
mod rolling_distribution;
|
||||
mod spot;
|
||||
mod spot_with_deltas;
|
||||
|
||||
pub use base::*;
|
||||
pub use cached::*;
|
||||
pub use cached_full::*;
|
||||
pub use columnar::*;
|
||||
pub use cumulative::*;
|
||||
pub use cumulative_rolling::*;
|
||||
pub use lazy::*;
|
||||
@@ -26,4 +27,3 @@ pub use lazy_rolling_sum::*;
|
||||
pub use lazy_spot::*;
|
||||
pub use rolling_distribution::*;
|
||||
pub use spot::*;
|
||||
pub use spot_with_deltas::*;
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Cents, Height, PartsPerMillionSigned64, Sats, SatsSigned, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{CachedBoxedVec, Database, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
CachedWindowStartVec, LazyRollingDeltasAmountFromHeight, SpotValuePerBlock, Windows,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct SpotValuePerBlockWithDeltas<M: StorageMode = Rw> {
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
#[traversable(flatten)]
|
||||
pub inner: SpotValuePerBlock<M>,
|
||||
pub delta: LazyRollingDeltasAmountFromHeight<Sats, SatsSigned, PartsPerMillionSigned64>,
|
||||
}
|
||||
|
||||
impl SpotValuePerBlockWithDeltas {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let inner = SpotValuePerBlock::forced_import(db, name, version, indexes, spot_price)?;
|
||||
|
||||
let delta = LazyRollingDeltasAmountFromHeight::new(
|
||||
&format!("{name}_delta"),
|
||||
version + Version::TWO,
|
||||
&inner.sats.height,
|
||||
cached_starts,
|
||||
indexes,
|
||||
);
|
||||
|
||||
Ok(Self { inner, delta })
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ pub use derived::{
|
||||
};
|
||||
pub use fixed_ratio::{FixedToPercent, FixedToRatio};
|
||||
pub use ratio::{
|
||||
RatioCents, RatioCentsSignedCents, RatioDiffCents, RatioDiffDollars, RatioDiffF32,
|
||||
RatioBytes, RatioCents, RatioCentsSignedCents, RatioDiffCents, RatioDiffDollars, RatioDiffF32,
|
||||
RatioDollars, RatioSats, RatioU64,
|
||||
};
|
||||
pub use specialized::{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use brk_types::{Cents, CentsSigned, Dollars, Sats, StoredF32, StoredU64};
|
||||
use brk_types::{Bytes, Cents, CentsSigned, Dollars, Sats, StoredF32, StoredU64};
|
||||
use vecdb::{BinaryTransform, unlikely};
|
||||
|
||||
use crate::internal::FixedRatio;
|
||||
use crate::internal::{FixedRatio, NumericValue};
|
||||
|
||||
pub struct RatioU64<P>(PhantomData<P>);
|
||||
|
||||
@@ -18,6 +18,20 @@ impl<P: FixedRatio> BinaryTransform<StoredU64, StoredU64, P> for RatioU64<P> {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RatioBytes<P>(PhantomData<P>);
|
||||
|
||||
impl<P: FixedRatio, D: NumericValue> BinaryTransform<Bytes, D, P> for RatioBytes<P> {
|
||||
#[inline(always)]
|
||||
fn apply(numerator: Bytes, denominator: D) -> P {
|
||||
let denominator: f64 = denominator.into();
|
||||
if denominator > 0.0 {
|
||||
P::from(f64::from(numerator) / denominator)
|
||||
} else {
|
||||
P::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RatioSats<P>(PhantomData<P>);
|
||||
|
||||
impl<P: FixedRatio> BinaryTransform<Sats, Sats, P> for RatioSats<P> {
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
//! Generic `all` + per-`AddrType` container, mirrors the `WithSth` pattern
|
||||
//! along the address-type axis. Used by every metric that tracks one
|
||||
//! aggregate value alongside a per-address-type breakdown.
|
||||
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Lengths;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Cents, Height, Sats, Version};
|
||||
use rayon::prelude::*;
|
||||
use schemars::JsonSchema;
|
||||
use vecdb::{AnyStoredVec, AnyVec, CachedBoxedVec, Database, EagerVec, Exit, PcoVec, WritableVec};
|
||||
|
||||
use crate::indexes;
|
||||
|
||||
use super::{
|
||||
CachedWindowStartVec, NumericValue, PerBlock, PerBlockCumulativeRolling, SpotValuePerBlock,
|
||||
Windows,
|
||||
};
|
||||
|
||||
/// `all` aggregate plus per-`AddrType` breakdown.
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct WithAddrTypes<T> {
|
||||
pub all: T,
|
||||
#[traversable(flatten)]
|
||||
pub by_addr_type: ByAddrType<T>,
|
||||
}
|
||||
|
||||
impl<T> WithAddrTypes<PerBlock<T>>
|
||||
where
|
||||
T: NumericValue + JsonSchema,
|
||||
{
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
let all = PerBlock::forced_import(db, name, version, indexes)?;
|
||||
let by_addr_type = ByAddrType::new_with_name(|type_name| {
|
||||
PerBlock::forced_import(db, &format!("{type_name}_{name}"), version, indexes)
|
||||
})?;
|
||||
Ok(Self { all, by_addr_type })
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.by_addr_type
|
||||
.values()
|
||||
.map(|v| v.height.len())
|
||||
.min()
|
||||
.unwrap()
|
||||
.min(self.all.height.len())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
rayon::iter::once(&mut self.all.height as &mut dyn AnyStoredVec).chain(
|
||||
self.by_addr_type
|
||||
.par_values_mut()
|
||||
.map(|v| &mut v.height as &mut dyn AnyStoredVec),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.all.height.reset()?;
|
||||
for v in self.by_addr_type.values_mut() {
|
||||
v.height.reset()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_height<U>(&mut self, total: U, per_type: impl IntoIterator<Item = U>)
|
||||
where
|
||||
U: Into<T>,
|
||||
{
|
||||
self.all.height.push(total.into());
|
||||
for (v, value) in self.by_addr_type.values_mut().zip(per_type) {
|
||||
v.height.push(value.into());
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute `all.height` as the per-block sum of the per-type vecs.
|
||||
pub(crate) fn compute_rest(&mut self, starting_lengths: &Lengths, exit: &Exit) -> Result<()> {
|
||||
let sources: Vec<&EagerVec<PcoVec<Height, T>>> =
|
||||
self.by_addr_type.values().map(|v| &v.height).collect();
|
||||
self.all
|
||||
.height
|
||||
.compute_sum_of_others(starting_lengths.height, &sources, exit)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> WithAddrTypes<PerBlockCumulativeRolling<T>>
|
||||
where
|
||||
T: NumericValue + JsonSchema,
|
||||
{
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
let all =
|
||||
PerBlockCumulativeRolling::forced_import(db, name, version, indexes, cached_starts)?;
|
||||
let by_addr_type = ByAddrType::new_with_name(|type_name| {
|
||||
PerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
&format!("{type_name}_{name}"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)
|
||||
})?;
|
||||
Ok(Self { all, by_addr_type })
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.by_addr_type
|
||||
.values()
|
||||
.map(|v| v.cumulative.height.len())
|
||||
.min()
|
||||
.unwrap()
|
||||
.min(self.all.cumulative.height.len())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
rayon::iter::once(&mut self.all.cumulative.height as &mut dyn AnyStoredVec).chain(
|
||||
self.by_addr_type
|
||||
.par_values_mut()
|
||||
.map(|v| &mut v.cumulative.height as &mut dyn AnyStoredVec),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.all.cumulative.height.reset()?;
|
||||
for v in self.by_addr_type.values_mut() {
|
||||
v.cumulative.height.reset()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_height<U>(&mut self, total: U, per_type: impl IntoIterator<Item = U>)
|
||||
where
|
||||
U: Into<T>,
|
||||
{
|
||||
self.all.push_block(total.into());
|
||||
for (v, value) in self.by_addr_type.values_mut().zip(per_type) {
|
||||
v.push_block(value.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WithAddrTypes<SpotValuePerBlock> {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let all = SpotValuePerBlock::forced_import(db, name, version, indexes, spot_price)?;
|
||||
let by_addr_type = ByAddrType::new_with_name(|type_name| {
|
||||
SpotValuePerBlock::forced_import(
|
||||
db,
|
||||
&format!("{type_name}_{name}"),
|
||||
version,
|
||||
indexes,
|
||||
spot_price,
|
||||
)
|
||||
})?;
|
||||
Ok(Self { all, by_addr_type })
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.by_addr_type
|
||||
.values()
|
||||
.map(|v| v.sats.height.len())
|
||||
.min()
|
||||
.unwrap()
|
||||
.min(self.all.sats.height.len())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
rayon::iter::once(&mut self.all.sats.height as &mut dyn AnyStoredVec).chain(
|
||||
self.by_addr_type
|
||||
.par_values_mut()
|
||||
.map(|v| &mut v.sats.height as &mut dyn AnyStoredVec),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.all.sats.height.reset()?;
|
||||
for v in self.by_addr_type.values_mut() {
|
||||
v.sats.height.reset()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Push the stateful sats value for `all` and each per-type.
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_height<U>(&mut self, total: U, per_type: impl IntoIterator<Item = U>)
|
||||
where
|
||||
U: Into<Sats>,
|
||||
{
|
||||
self.all.sats.height.push(total.into());
|
||||
for (v, value) in self.by_addr_type.values_mut().zip(per_type) {
|
||||
v.sats.height.push(value.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
use brk_cohort::{ADDR_TYPE_IDS, AddrTypeId};
|
||||
use brk_types::{Cents, Height, Sats, Version};
|
||||
use schemars::JsonSchema;
|
||||
use vecdb::{
|
||||
CachedBoxedVec, CachedVec, PcoVec, PcoVecValue, ReadOnlyColumnarVec, ReadableColumnarVec,
|
||||
UnaryTransform,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
CachedWindowStartVec, Identity, LazyColumnPerBlock, LazyColumnPerBlockCumulativeRolling,
|
||||
LazyColumnSpotValuePerBlock, LazyPerBlock, LazyPerBlockCumulativeAverage,
|
||||
LazyPerBlockCumulativeRolling, LazySpotValuePerBlock, NumericValue, Windows,
|
||||
},
|
||||
};
|
||||
|
||||
use super::WithAddrTypes;
|
||||
|
||||
impl<T> WithAddrTypes<LazyColumnPerBlock<T, AddrTypeId>, LazyPerBlock<T>>
|
||||
where
|
||||
T: NumericValue + JsonSchema + 'static,
|
||||
{
|
||||
pub(crate) fn from_columnar_source(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source: &ReadOnlyColumnarVec<PcoVec<Height, T>, AddrTypeId>,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Self {
|
||||
let all = LazyPerBlock::from_height_source::<Identity<T>, _>(
|
||||
name,
|
||||
version,
|
||||
source.sum_columns(name, version, ADDR_TYPE_IDS),
|
||||
indexes,
|
||||
);
|
||||
let by_addr_type = AddrTypeId::series(|column, type_name| {
|
||||
LazyColumnPerBlock::new(
|
||||
&format!("{type_name}_{name}"),
|
||||
version,
|
||||
source,
|
||||
column,
|
||||
indexes,
|
||||
)
|
||||
});
|
||||
|
||||
Self { all, by_addr_type }
|
||||
}
|
||||
}
|
||||
|
||||
impl WithAddrTypes<LazyColumnSpotValuePerBlock<AddrTypeId>, LazySpotValuePerBlock> {
|
||||
pub(crate) fn from_columnar_spot_value_source(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source: &ReadOnlyColumnarVec<PcoVec<Height, Sats>, AddrTypeId>,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Self {
|
||||
let sats =
|
||||
CachedVec::wrap(source.sum_columns(&format!("{name}_sats"), version, ADDR_TYPE_IDS));
|
||||
let all = LazySpotValuePerBlock::from_sats_source(name, version, sats, indexes, spot_price);
|
||||
let by_addr_type = AddrTypeId::series(|column, type_name| {
|
||||
LazyColumnSpotValuePerBlock::new(
|
||||
&format!("{type_name}_{name}"),
|
||||
version,
|
||||
source,
|
||||
column,
|
||||
indexes,
|
||||
spot_price,
|
||||
)
|
||||
});
|
||||
|
||||
Self { all, by_addr_type }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T>
|
||||
WithAddrTypes<
|
||||
LazyColumnPerBlockCumulativeRolling<T, AddrTypeId>,
|
||||
LazyPerBlockCumulativeRolling<T>,
|
||||
>
|
||||
where
|
||||
T: NumericValue + JsonSchema + PcoVecValue,
|
||||
{
|
||||
pub(crate) fn from_columnar_cumulative_source(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source: &ReadOnlyColumnarVec<PcoVec<Height, T>, AddrTypeId>,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
) -> Self {
|
||||
let cumulative = CachedVec::wrap(source.sum_columns(
|
||||
&format!("{name}_cumulative"),
|
||||
version,
|
||||
ADDR_TYPE_IDS,
|
||||
));
|
||||
let all = LazyPerBlockCumulativeRolling::from_cached_cumulative_source(
|
||||
name,
|
||||
version,
|
||||
cumulative,
|
||||
cached_starts,
|
||||
indexes,
|
||||
);
|
||||
let by_addr_type = AddrTypeId::series(|column, type_name| {
|
||||
LazyColumnPerBlockCumulativeRolling::new(
|
||||
&format!("{type_name}_{name}"),
|
||||
version,
|
||||
source,
|
||||
column,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)
|
||||
});
|
||||
|
||||
Self { all, by_addr_type }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, C, F> WithAddrTypes<LazyPerBlockCumulativeAverage<T, C, F>>
|
||||
where
|
||||
T: NumericValue + JsonSchema,
|
||||
C: NumericValue + JsonSchema + PcoVecValue,
|
||||
F: UnaryTransform<C, T>,
|
||||
{
|
||||
pub(crate) fn from_columnar_cumulative_average_source(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source: &ReadOnlyColumnarVec<PcoVec<Height, C>, AddrTypeId>,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&CachedWindowStartVec>,
|
||||
) -> Self {
|
||||
let cumulative_name = format!("{name}_cumulative");
|
||||
let cumulative = source.sum_columns(&cumulative_name, version, ADDR_TYPE_IDS);
|
||||
let all =
|
||||
LazyPerBlockCumulativeAverage::new(name, version, &cumulative, indexes, cached_starts);
|
||||
let by_addr_type = AddrTypeId::series(|column, type_name| {
|
||||
let name = format!("{type_name}_{name}");
|
||||
let cumulative = source.column(&format!("{name}_cumulative"), version, column);
|
||||
LazyPerBlockCumulativeAverage::new(&name, version, &cumulative, indexes, cached_starts)
|
||||
});
|
||||
|
||||
Self { all, by_addr_type }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
mod columnar;
|
||||
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_traversable::Traversable;
|
||||
|
||||
/// `all` aggregate plus a per-address-type breakdown.
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct WithAddrTypes<T, A = T> {
|
||||
pub all: A,
|
||||
#[traversable(flatten)]
|
||||
pub by_addr_type: ByAddrType<T>,
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
use std::{fs, path::Path, thread, time::Instant};
|
||||
|
||||
|
||||
@@ -16,32 +16,12 @@ impl Vecs {
|
||||
let starting_lengths = indexer.safe_lengths();
|
||||
let close = &prices.spot.cents.height;
|
||||
|
||||
for (ema, period) in [
|
||||
(&mut self.ema._1w, 7),
|
||||
(&mut self.ema._8d, 8),
|
||||
(&mut self.ema._12d, 12),
|
||||
(&mut self.ema._13d, 13),
|
||||
(&mut self.ema._21d, 21),
|
||||
(&mut self.ema._26d, 26),
|
||||
(&mut self.ema._1m, 30),
|
||||
(&mut self.ema._34d, 34),
|
||||
(&mut self.ema._55d, 55),
|
||||
(&mut self.ema._89d, 89),
|
||||
(&mut self.ema._144d, 144),
|
||||
(&mut self.ema._200d, 200),
|
||||
(&mut self.ema._1y, 365),
|
||||
(&mut self.ema._2y, 2 * 365),
|
||||
(&mut self.ema._200w, 200 * 7),
|
||||
(&mut self.ema._4y, 4 * 365),
|
||||
] {
|
||||
let window_starts = blocks.lookback.start_vec(period);
|
||||
ema.cents.height.compute_rolling_ema(
|
||||
starting_lengths.height,
|
||||
window_starts,
|
||||
close,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
self.ema.height.compute_rolling_ema_columns(
|
||||
starting_lengths.height,
|
||||
|period| blocks.lookback.start_vec(period.days()),
|
||||
close,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user