mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-22 12:23:04 -07:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4933ae314 | |||
| 53ffe0e06c | |||
| 0433e3b256 | |||
| 9b409799c8 | |||
| dd96709d18 | |||
| 3818a72045 | |||
| 0437ce1bb4 | |||
| 0d5d7da70f | |||
| 277a0eb6a7 | |||
| c02fc37491 | |||
| 1d440be352 | |||
| 67b2897a8c | |||
| 519e7c4179 | |||
| 36bc1fb491 | |||
| 9e3fe4e557 | |||
| a6d8278730 | |||
| b23d20ea05 | |||
| cf4bc470e4 | |||
| da923e409a | |||
| f7d7c5704a | |||
| f03bbd9a92 | |||
| ff5bb770d7 | |||
| 8dd350264a |
@@ -2,7 +2,7 @@ name: Check outdated dependencies
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 9 * * *'
|
||||
- cron: "0 9 * * 1"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
Generated
+69
-67
@@ -334,7 +334,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"brk_bencher",
|
||||
"brk_bindgen",
|
||||
@@ -358,7 +358,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_alloc"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"libmimalloc-sys",
|
||||
"mimalloc",
|
||||
@@ -366,7 +366,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_bencher"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"brk_error",
|
||||
"brk_logger",
|
||||
@@ -376,18 +376,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_bencher_visualizer"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"plotters",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brk_bindgen"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"brk_cohort",
|
||||
"brk_query",
|
||||
"brk_types",
|
||||
"indexmap",
|
||||
"oas3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -395,7 +396,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_cli"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"brk_alloc",
|
||||
@@ -422,7 +423,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_client"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"brk_cohort",
|
||||
"brk_types",
|
||||
@@ -433,7 +434,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_cohort"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"brk_error",
|
||||
"brk_traversable",
|
||||
@@ -445,7 +446,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_computer"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"brk_alloc",
|
||||
@@ -475,7 +476,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_error"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"bitcoincore-rpc",
|
||||
@@ -490,7 +491,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_fetcher"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"brk_error",
|
||||
"brk_logger",
|
||||
@@ -502,7 +503,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_indexer"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"brk_alloc",
|
||||
@@ -527,7 +528,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_iterator"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"brk_error",
|
||||
"brk_reader",
|
||||
@@ -537,7 +538,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_logger"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"jiff",
|
||||
"owo-colors",
|
||||
@@ -548,7 +549,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_mempool"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"brk_error",
|
||||
"brk_logger",
|
||||
@@ -563,7 +564,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_query"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"brk_computer",
|
||||
@@ -583,7 +584,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_reader"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"brk_error",
|
||||
@@ -598,7 +599,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_rpc"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"bitcoincore-rpc",
|
||||
@@ -611,7 +612,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_server"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"aide",
|
||||
"axum",
|
||||
@@ -643,7 +644,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_store"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"brk_error",
|
||||
"brk_types",
|
||||
@@ -654,10 +655,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_traversable"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"brk_traversable_derive",
|
||||
"brk_types",
|
||||
"indexmap",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -666,7 +668,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_traversable_derive"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -675,12 +677,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_types"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"brk_error",
|
||||
"byteview",
|
||||
"derive_more",
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"jiff",
|
||||
"rapidhash",
|
||||
@@ -695,7 +698,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_website"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"brk_logger",
|
||||
@@ -737,9 +740,9 @@ checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.24.0"
|
||||
version = "1.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
|
||||
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
@@ -755,9 +758,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.0"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "byteview"
|
||||
@@ -767,9 +770,9 @@ checksum = "dda4398f387cc6395a3e93b3867cd9abda914c97a0b344d1eefb2e5c51785fca"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.54"
|
||||
version = "1.2.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
|
||||
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@@ -1215,9 +1218,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.8"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "fjall"
|
||||
@@ -1238,9 +1241,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.8"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369"
|
||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
@@ -1579,12 +1582,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.19"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
@@ -2306,15 +2308,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.13.0"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950"
|
||||
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.4"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
||||
checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
@@ -2432,9 +2434,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rawdb"
|
||||
version = "0.6.3"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bf9c16af7a93d15280ceb0d502657f9ec524cce61c946a1c98390740270820d"
|
||||
checksum = "a66c17743b9a7e6a3bb8edb10fef25c62516e281b723ea38d7c1feea2035c75d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
@@ -2507,9 +2509,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.2"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -2519,9 +2521,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.13"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -2530,9 +2532,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
@@ -2550,9 +2552,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rlimit"
|
||||
version = "0.10.2"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7043b63bd0cd1aaa628e476b80e6d4023a3b50eb32789f2728908107bd0c793a"
|
||||
checksum = "f35ee2729c56bb610f6dba436bf78135f728b7373bdffae2ec815b2d3eb98cc3"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -2636,9 +2638,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2"
|
||||
checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"indexmap",
|
||||
@@ -2650,9 +2652,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4908ad288c5035a8eb12cfdf0d49270def0a268ee162b75eeee0f85d155a7c45"
|
||||
checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2853,9 +2855,9 @@ checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.11"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
@@ -3252,9 +3254,9 @@ checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23"
|
||||
|
||||
[[package]]
|
||||
name = "vecdb"
|
||||
version = "0.6.3"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66b361b0614c0d367441dcbc7c28a885b7b088a8d379e23ad845d81324f2c5f6"
|
||||
checksum = "16459a73939ec1c7ddb5c2f4264916f7bb96c88287b15dcce29cd95c16d2f6c0"
|
||||
dependencies = [
|
||||
"ctrlc",
|
||||
"log",
|
||||
@@ -3273,9 +3275,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vecdb_derive"
|
||||
version = "0.6.3"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "962dfcd7fc848c27f6ee051cf1a4dfd55d72a37d443fb525268eff41da9cacf8"
|
||||
checksum = "e1845265e89f36a22175ebef07dc1340050ef3ec54aa9f9c84859d4dda0a3a03"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
@@ -3694,18 +3696,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.35"
|
||||
version = "0.8.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdea86ddd5568519879b8187e1cf04e24fce28f7fe046ceecbce472ff19a2572"
|
||||
checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.35"
|
||||
version = "0.8.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c15e1b46eff7c6c91195752e0eeed8ef040e391cdece7c25376957d5f15df22"
|
||||
checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3768,9 +3770,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.17"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
|
||||
checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
|
||||
+26
-25
@@ -4,7 +4,7 @@ members = ["crates/*"]
|
||||
package.description = "The Bitcoin Research Kit is a suite of tools designed to extract, compute and display data stored on a Bitcoin Core node"
|
||||
package.license = "MIT"
|
||||
package.edition = "2024"
|
||||
package.version = "0.1.2"
|
||||
package.version = "0.1.4"
|
||||
package.homepage = "https://bitcoinresearchkit.org"
|
||||
package.repository = "https://github.com/bitcoinresearchkit/brk"
|
||||
package.readme = "README.md"
|
||||
@@ -40,39 +40,40 @@ aide = { version = "0.16.0-alpha.2", features = ["axum-json", "axum-query"] }
|
||||
axum = { version = "0.8.8", default-features = false, features = ["http1", "json", "query", "tokio", "tracing"] }
|
||||
bitcoin = { version = "0.32.8", features = ["serde"] }
|
||||
bitcoincore-rpc = "0.19.0"
|
||||
brk_alloc = { version = "0.1.2", path = "crates/brk_alloc" }
|
||||
brk_bencher = { version = "0.1.2", path = "crates/brk_bencher" }
|
||||
brk_bindgen = { version = "0.1.2", path = "crates/brk_bindgen" }
|
||||
brk_cli = { version = "0.1.2", path = "crates/brk_cli" }
|
||||
brk_client = { version = "0.1.2", path = "crates/brk_client" }
|
||||
brk_cohort = { version = "0.1.2", path = "crates/brk_cohort" }
|
||||
brk_computer = { version = "0.1.2", path = "crates/brk_computer" }
|
||||
brk_error = { version = "0.1.2", path = "crates/brk_error" }
|
||||
brk_fetcher = { version = "0.1.2", path = "crates/brk_fetcher" }
|
||||
brk_indexer = { version = "0.1.2", path = "crates/brk_indexer" }
|
||||
brk_iterator = { version = "0.1.2", path = "crates/brk_iterator" }
|
||||
brk_logger = { version = "0.1.2", path = "crates/brk_logger" }
|
||||
brk_mempool = { version = "0.1.2", path = "crates/brk_mempool" }
|
||||
brk_query = { version = "0.1.2", path = "crates/brk_query", features = ["tokio"] }
|
||||
brk_reader = { version = "0.1.2", path = "crates/brk_reader" }
|
||||
brk_rpc = { version = "0.1.2", path = "crates/brk_rpc" }
|
||||
brk_server = { version = "0.1.2", path = "crates/brk_server" }
|
||||
brk_store = { version = "0.1.2", path = "crates/brk_store" }
|
||||
brk_traversable = { version = "0.1.2", path = "crates/brk_traversable", features = ["pco", "derive"] }
|
||||
brk_traversable_derive = { version = "0.1.2", path = "crates/brk_traversable_derive" }
|
||||
brk_types = { version = "0.1.2", path = "crates/brk_types" }
|
||||
brk_website = { version = "0.1.2", path = "crates/brk_website" }
|
||||
brk_alloc = { version = "0.1.4", path = "crates/brk_alloc" }
|
||||
brk_bencher = { version = "0.1.4", path = "crates/brk_bencher" }
|
||||
brk_bindgen = { version = "0.1.4", path = "crates/brk_bindgen" }
|
||||
brk_cli = { version = "0.1.4", path = "crates/brk_cli" }
|
||||
brk_client = { version = "0.1.4", path = "crates/brk_client" }
|
||||
brk_cohort = { version = "0.1.4", path = "crates/brk_cohort" }
|
||||
brk_computer = { version = "0.1.4", path = "crates/brk_computer" }
|
||||
brk_error = { version = "0.1.4", path = "crates/brk_error" }
|
||||
brk_fetcher = { version = "0.1.4", path = "crates/brk_fetcher" }
|
||||
brk_indexer = { version = "0.1.4", path = "crates/brk_indexer" }
|
||||
brk_iterator = { version = "0.1.4", path = "crates/brk_iterator" }
|
||||
brk_logger = { version = "0.1.4", path = "crates/brk_logger" }
|
||||
brk_mempool = { version = "0.1.4", path = "crates/brk_mempool" }
|
||||
brk_query = { version = "0.1.4", path = "crates/brk_query", features = ["tokio"] }
|
||||
brk_reader = { version = "0.1.4", path = "crates/brk_reader" }
|
||||
brk_rpc = { version = "0.1.4", path = "crates/brk_rpc" }
|
||||
brk_server = { version = "0.1.4", path = "crates/brk_server" }
|
||||
brk_store = { version = "0.1.4", path = "crates/brk_store" }
|
||||
brk_traversable = { version = "0.1.4", path = "crates/brk_traversable", features = ["pco", "derive"] }
|
||||
brk_traversable_derive = { version = "0.1.4", path = "crates/brk_traversable_derive" }
|
||||
brk_types = { version = "0.1.4", path = "crates/brk_types" }
|
||||
brk_website = { version = "0.1.4", path = "crates/brk_website" }
|
||||
byteview = "0.10.0"
|
||||
color-eyre = "0.6.5"
|
||||
derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] }
|
||||
fjall = "3.0.1"
|
||||
indexmap = { version = "2.13.0", features = ["serde"] }
|
||||
jiff = { version = "0.2.18", features = ["perf-inline", "tz-system"], default-features = false }
|
||||
minreq = { version = "2.14.1", features = ["https", "json-using-serde"] }
|
||||
owo-colors = "4.2.3"
|
||||
parking_lot = "0.12.5"
|
||||
rayon = "1.11.0"
|
||||
rustc-hash = "2.1.1"
|
||||
schemars = "1.2.0"
|
||||
schemars = { version = "1.2.1", features = ["indexmap2"] }
|
||||
serde = "1.0.228"
|
||||
serde_bytes = "0.11.19"
|
||||
serde_derive = "1.0.228"
|
||||
@@ -82,7 +83,7 @@ tokio = { version = "1.49.0", features = ["rt-multi-thread"] }
|
||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||
tower-http = { version = "0.6.8", features = ["catch-panic", "compression-br", "compression-gzip", "compression-zstd", "cors", "normalize-path", "timeout", "trace"] }
|
||||
tower-layer = "0.3"
|
||||
vecdb = { version = "0.6.3", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
vecdb = { version = "0.6.8", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
# vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
|
||||
[workspace.metadata.release]
|
||||
|
||||
@@ -11,6 +11,7 @@ repository.workspace = true
|
||||
brk_cohort = { workspace = true }
|
||||
brk_query = { workspace = true }
|
||||
brk_types = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
oas3 = "0.20"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -50,7 +50,7 @@ pub fn detect_structural_patterns(
|
||||
BTreeMap<String, PatternBaseResult>,
|
||||
) {
|
||||
let mut ctx = PatternContext::new();
|
||||
resolve_branch_patterns(tree, "root", &mut ctx);
|
||||
resolve_branch_patterns(tree, &mut ctx);
|
||||
|
||||
let (generic_patterns, generic_mappings, type_mappings) =
|
||||
detect_generic_patterns(&ctx.signature_to_pattern);
|
||||
@@ -87,6 +87,13 @@ pub fn detect_structural_patterns(
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Deduplicate patterns by name - different signatures can map to the same name
|
||||
// when their normalized forms match but they can't be unified as generics
|
||||
{
|
||||
let mut seen_names: BTreeSet<String> = BTreeSet::new();
|
||||
patterns.retain(|p| seen_names.insert(p.name.clone()));
|
||||
}
|
||||
|
||||
patterns.extend(generic_patterns);
|
||||
|
||||
// Build pattern lookup for mode analysis (patterns appearing 2+ times)
|
||||
@@ -249,17 +256,19 @@ fn replace_inner_type(type_str: &str, replacement: &str) -> String {
|
||||
/// Recursively resolve branch patterns bottom-up.
|
||||
fn resolve_branch_patterns(
|
||||
node: &TreeNode,
|
||||
field_name: &str,
|
||||
ctx: &mut PatternContext,
|
||||
) -> Option<(String, Vec<PatternField>)> {
|
||||
let TreeNode::Branch(children) = node else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// Convert to sorted BTreeMap for consistent pattern detection
|
||||
let sorted_children: BTreeMap<_, _> = children.iter().collect();
|
||||
|
||||
let mut fields: Vec<PatternField> = Vec::new();
|
||||
let mut child_fields_vec: Vec<Vec<PatternField>> = Vec::new();
|
||||
|
||||
for (child_name, child_node) in children {
|
||||
for (child_name, child_node) in sorted_children {
|
||||
let (rust_type, json_type, indexes, child_fields) = match child_node {
|
||||
TreeNode::Leaf(leaf) => (
|
||||
leaf.kind().to_string(),
|
||||
@@ -268,9 +277,8 @@ fn resolve_branch_patterns(
|
||||
Vec::new(),
|
||||
),
|
||||
TreeNode::Branch(_) => {
|
||||
let (pattern_name, child_pattern_fields) =
|
||||
resolve_branch_patterns(child_node, child_name, ctx)
|
||||
.unwrap_or_else(|| ("Unknown".to_string(), Vec::new()));
|
||||
let (pattern_name, child_pattern_fields) = resolve_branch_patterns(child_node, ctx)
|
||||
.unwrap_or_else(|| ("Unknown".to_string(), Vec::new()));
|
||||
(
|
||||
pattern_name.clone(),
|
||||
pattern_name,
|
||||
@@ -289,7 +297,7 @@ fn resolve_branch_patterns(
|
||||
child_fields_vec.push(child_fields);
|
||||
}
|
||||
|
||||
fields.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
// Fields are already sorted since we iterated over BTreeMap
|
||||
*ctx.signature_counts.entry(fields.clone()).or_insert(0) += 1;
|
||||
|
||||
ctx.signature_to_child_fields
|
||||
@@ -300,10 +308,17 @@ fn resolve_branch_patterns(
|
||||
existing.clone()
|
||||
} else {
|
||||
let normalized = normalize_fields_for_naming(&fields);
|
||||
// Generate stable name from first word of each field (deduped, sorted)
|
||||
let first_words: BTreeSet<String> = fields
|
||||
.iter()
|
||||
.filter_map(|f| f.name.split('_').next())
|
||||
.map(to_pascal_case)
|
||||
.collect();
|
||||
let combined: String = first_words.into_iter().collect();
|
||||
let name = ctx
|
||||
.normalized_to_name
|
||||
.entry(normalized)
|
||||
.or_insert_with(|| generate_pattern_name(field_name, &mut ctx.name_counts))
|
||||
.or_insert_with(|| generate_pattern_name(&combined, &mut ctx.name_counts))
|
||||
.clone();
|
||||
ctx.signature_to_pattern
|
||||
.insert(fields.clone(), name.clone());
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use brk_types::{Index, TreeNode, extract_json_type};
|
||||
use indexmap::IndexMap;
|
||||
|
||||
use crate::{IndexSetPattern, PatternField, child_type_name};
|
||||
|
||||
@@ -26,8 +27,9 @@ fn get_shortest_leaf_name(node: &TreeNode) -> Option<String> {
|
||||
}
|
||||
|
||||
/// Get the field signature for a branch node's children.
|
||||
/// Fields are sorted alphabetically for consistent pattern matching.
|
||||
pub fn get_node_fields(
|
||||
children: &BTreeMap<String, TreeNode>,
|
||||
children: &IndexMap<String, TreeNode>,
|
||||
pattern_lookup: &BTreeMap<Vec<PatternField>, String>,
|
||||
) -> Vec<PatternField> {
|
||||
let mut fields: Vec<PatternField> = children
|
||||
@@ -57,6 +59,7 @@ pub fn get_node_fields(
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
// Sort for consistent pattern matching (display order preserved in IndexMap)
|
||||
fields.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
fields
|
||||
}
|
||||
@@ -298,7 +301,7 @@ pub fn infer_accumulated_name(parent_acc: &str, field_name: &str, descendant_lea
|
||||
|
||||
/// Get fields with child field information for generic pattern lookup.
|
||||
pub fn get_fields_with_child_info(
|
||||
children: &BTreeMap<String, TreeNode>,
|
||||
children: &IndexMap<String, TreeNode>,
|
||||
parent_name: &str,
|
||||
pattern_lookup: &BTreeMap<Vec<PatternField>, String>,
|
||||
) -> Vec<(PatternField, Option<Vec<PatternField>>)> {
|
||||
@@ -344,7 +347,6 @@ pub fn get_fields_with_child_info(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use brk_types::{MetricLeaf, MetricLeafWithSchema, TreeNode};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn make_leaf(name: &str) -> TreeNode {
|
||||
let leaf = MetricLeaf {
|
||||
@@ -356,7 +358,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn make_branch(children: Vec<(&str, TreeNode)>) -> TreeNode {
|
||||
let map: BTreeMap<String, TreeNode> = children
|
||||
let map: IndexMap<String, TreeNode> = children
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_string(), v))
|
||||
.collect();
|
||||
|
||||
@@ -310,6 +310,9 @@ class BrkClientBase {{
|
||||
this.timeout = isString ? 5000 : (options.timeout ?? 5000);
|
||||
/** @type {{Promise<Cache | null>}} */
|
||||
this._cachePromise = _openCache(isString ? undefined : options.cache);
|
||||
/** @type {{Cache | null}} */
|
||||
this._cache = null;
|
||||
this._cachePromise.then(c => this._cache = c);
|
||||
}}
|
||||
|
||||
/**
|
||||
@@ -325,35 +328,57 @@ class BrkClientBase {{
|
||||
}}
|
||||
|
||||
/**
|
||||
* Make a GET request with stale-while-revalidate caching
|
||||
* Make a GET request - races cache vs network, first to resolve calls onUpdate
|
||||
* @template T
|
||||
* @param {{string}} path
|
||||
* @param {{(value: T) => void}} [onUpdate] - Called when data is available
|
||||
* @param {{(value: T) => void}} [onUpdate] - Called when data is available (may be called twice: cache then network)
|
||||
* @returns {{Promise<T>}}
|
||||
*/
|
||||
async getJson(path, onUpdate) {{
|
||||
const base = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
||||
const url = `${{base}}${{path}}`;
|
||||
const cache = await this._cachePromise;
|
||||
const cachedRes = await cache?.match(url);
|
||||
const cachedJson = cachedRes ? await cachedRes.json() : null;
|
||||
const cache = this._cache ?? await this._cachePromise;
|
||||
|
||||
if (cachedJson) onUpdate?.(cachedJson);
|
||||
if (globalThis.navigator?.onLine === false) {{
|
||||
if (cachedJson) return cachedJson;
|
||||
throw new BrkError('Offline and no cached data available');
|
||||
}}
|
||||
let resolved = false;
|
||||
/** @type {{Response | null}} */
|
||||
let cachedRes = null;
|
||||
|
||||
try {{
|
||||
const res = await this.get(path);
|
||||
if (cachedRes?.headers.get('ETag') === res.headers.get('ETag')) return cachedJson;
|
||||
// Race cache vs network - first to resolve calls onUpdate
|
||||
const cachePromise = cache?.match(url).then(async (res) => {{
|
||||
cachedRes = res ?? null;
|
||||
if (!res) return null;
|
||||
const json = await res.json();
|
||||
if (!resolved && onUpdate) {{
|
||||
resolved = true;
|
||||
onUpdate(json);
|
||||
}}
|
||||
return json;
|
||||
}});
|
||||
|
||||
const networkPromise = this.get(path).then(async (res) => {{
|
||||
const cloned = res.clone();
|
||||
const json = await res.json();
|
||||
onUpdate?.(json);
|
||||
// Skip update if ETag matches and cache already delivered
|
||||
if (cachedRes?.headers.get('ETag') === res.headers.get('ETag')) {{
|
||||
if (!resolved && onUpdate) {{
|
||||
resolved = true;
|
||||
onUpdate(json);
|
||||
}}
|
||||
return json;
|
||||
}}
|
||||
resolved = true;
|
||||
if (onUpdate) {{
|
||||
onUpdate(json);
|
||||
}}
|
||||
if (cache) _runIdle(() => cache.put(url, cloned));
|
||||
return json;
|
||||
}});
|
||||
|
||||
try {{
|
||||
return await networkPromise;
|
||||
}} catch (e) {{
|
||||
// Network failed - wait for cache
|
||||
const cachedJson = await cachePromise?.catch(() => null);
|
||||
if (cachedJson) return cachedJson;
|
||||
throw e;
|
||||
}}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ Command-line interface for running a Bitcoin Research Kit instance.
|
||||
|
||||
```bash
|
||||
rustup update
|
||||
RUSTFLAGS="-C target-cpu=native" cargo install --locked brk_cli"
|
||||
RUSTFLAGS="-C target-cpu=native" cargo install --locked brk_cli
|
||||
```
|
||||
|
||||
Portable build (without native CPU optimizations):
|
||||
|
||||
@@ -34,7 +34,7 @@ fn main() -> Result<()> {
|
||||
.fetch()?;
|
||||
|
||||
for ohlc in ohlcs.data {
|
||||
let avg = (*ohlc.open + *ohlc.close) / 2;
|
||||
let avg = (u64::from(*ohlc.open) + u64::from(*ohlc.close)) / 2;
|
||||
let avg = Dollars::from(avg);
|
||||
writeln!(writer, "{avg}").map_err(|e| brk_client::BrkError {
|
||||
message: e.to_string(),
|
||||
|
||||
+3539
-3139
File diff suppressed because it is too large
Load Diff
@@ -2,13 +2,13 @@ use brk_traversable::Traversable;
|
||||
|
||||
#[derive(Debug, Default, Traversable)]
|
||||
pub struct ByAnyAddress<T> {
|
||||
pub loaded: T,
|
||||
pub funded: T,
|
||||
pub empty: T,
|
||||
}
|
||||
|
||||
impl<T> ByAnyAddress<Option<T>> {
|
||||
pub fn take(&mut self) {
|
||||
self.loaded.take();
|
||||
self.funded.take();
|
||||
self.empty.take();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,4 +102,14 @@ impl Filter {
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to compute relative metrics (invested capital %, NUPL ratios, etc.)
|
||||
/// Returns false for edge-case output types (Empty, P2MS, Unknown) which have
|
||||
/// too little volume for meaningful ratio/percentage analysis.
|
||||
pub fn compute_relative(&self) -> bool {
|
||||
!matches!(
|
||||
self,
|
||||
Filter::Type(OutputType::Empty | OutputType::P2MS | OutputType::Unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,27 +36,47 @@ fn run() -> Result<()> {
|
||||
let start = Instant::now();
|
||||
let mut buf = Vec::new();
|
||||
empty_data.write_json(Some(empty_data.len() - 1), Some(empty_data.len()), &mut buf)?;
|
||||
println!("emptyaddressdata last item JSON: {}", String::from_utf8_lossy(&buf));
|
||||
println!(
|
||||
"emptyaddressdata last item JSON: {}",
|
||||
String::from_utf8_lossy(&buf)
|
||||
);
|
||||
println!("Time for BytesVec write_json: {:?}", start.elapsed());
|
||||
|
||||
// Test emptyaddressindex (LazyVecFrom1 wrapper) - computed access
|
||||
let empty_index = &computer.distribution.emptyaddressindex;
|
||||
println!("\nemptyaddressindex (LazyVecFrom1) len: {}", empty_index.len());
|
||||
println!(
|
||||
"\nemptyaddressindex (LazyVecFrom1) len: {}",
|
||||
empty_index.len()
|
||||
);
|
||||
|
||||
let start = Instant::now();
|
||||
let mut buf = Vec::new();
|
||||
empty_index.write_json(Some(empty_index.len() - 1), Some(empty_index.len()), &mut buf)?;
|
||||
println!("emptyaddressindex last item JSON: {}", String::from_utf8_lossy(&buf));
|
||||
empty_index.write_json(
|
||||
Some(empty_index.len() - 1),
|
||||
Some(empty_index.len()),
|
||||
&mut buf,
|
||||
)?;
|
||||
println!(
|
||||
"emptyaddressindex last item JSON: {}",
|
||||
String::from_utf8_lossy(&buf)
|
||||
);
|
||||
println!("Time for LazyVecFrom1 write_json: {:?}", start.elapsed());
|
||||
|
||||
// Compare with loaded versions
|
||||
let loaded_data = &computer.distribution.addresses_data.loaded;
|
||||
println!("\nloadedaddressdata (BytesVec) len: {}", loaded_data.len());
|
||||
// Compare with funded versions
|
||||
let funded_data = &computer.distribution.addresses_data.funded;
|
||||
println!("\nfundedaddressdata (BytesVec) len: {}", funded_data.len());
|
||||
|
||||
let start = Instant::now();
|
||||
let mut buf = Vec::new();
|
||||
loaded_data.write_json(Some(loaded_data.len() - 1), Some(loaded_data.len()), &mut buf)?;
|
||||
println!("loadedaddressdata last item JSON: {}", String::from_utf8_lossy(&buf));
|
||||
funded_data.write_json(
|
||||
Some(funded_data.len() - 1),
|
||||
Some(funded_data.len()),
|
||||
&mut buf,
|
||||
)?;
|
||||
println!(
|
||||
"fundedaddressdata last item JSON: {}",
|
||||
String::from_utf8_lossy(&buf)
|
||||
);
|
||||
println!("Time for BytesVec write_json: {:?}", start.elapsed());
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -4,7 +4,7 @@ use vecdb::Exit;
|
||||
|
||||
use super::super::{ONE_TERA_HASH, TARGET_BLOCKS_PER_DAY_F64, count, difficulty, rewards};
|
||||
use super::Vecs;
|
||||
use crate::{ComputeIndexes, indexes};
|
||||
use crate::{ComputeIndexes, indexes, traits::ComputeDrawdown};
|
||||
|
||||
impl Vecs {
|
||||
pub fn compute(
|
||||
@@ -80,6 +80,27 @@ impl Vecs {
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
self.hash_rate_ath
|
||||
.compute_all(indexes, starting_indexes, exit, |v| {
|
||||
v.compute_all_time_high(
|
||||
starting_indexes.height,
|
||||
&self.hash_rate.height,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
self.hash_rate_drawdown
|
||||
.compute_all(indexes, starting_indexes, exit, |v| {
|
||||
v.compute_drawdown(
|
||||
starting_indexes.height,
|
||||
&self.hash_rate.height,
|
||||
&self.hash_rate_ath.height,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
self.hash_price_ths
|
||||
.compute_all(indexes, starting_indexes, exit, |v| {
|
||||
v.compute_transform2(
|
||||
|
||||
@@ -43,6 +43,18 @@ impl Vecs {
|
||||
version,
|
||||
indexes,
|
||||
)?,
|
||||
hash_rate_ath: ComputedFromHeightLast::forced_import(
|
||||
db,
|
||||
"hash_rate_ath",
|
||||
version,
|
||||
indexes,
|
||||
)?,
|
||||
hash_rate_drawdown: ComputedFromHeightLast::forced_import(
|
||||
db,
|
||||
"hash_rate_drawdown",
|
||||
version,
|
||||
indexes,
|
||||
)?,
|
||||
hash_price_ths: ComputedFromHeightLast::forced_import(
|
||||
db,
|
||||
"hash_price_ths",
|
||||
|
||||
@@ -11,6 +11,8 @@ pub struct Vecs {
|
||||
pub hash_rate_1m_sma: ComputedFromDateLast<StoredF32>,
|
||||
pub hash_rate_2m_sma: ComputedFromDateLast<StoredF32>,
|
||||
pub hash_rate_1y_sma: ComputedFromDateLast<StoredF32>,
|
||||
pub hash_rate_ath: ComputedFromHeightLast<StoredF64>,
|
||||
pub hash_rate_drawdown: ComputedFromHeightLast<StoredF32>,
|
||||
pub hash_price_ths: ComputedFromHeightLast<StoredF32>,
|
||||
pub hash_price_ths_min: ComputedFromHeightLast<StoredF32>,
|
||||
pub hash_price_phs: ComputedFromHeightLast<StoredF32>,
|
||||
|
||||
@@ -29,7 +29,7 @@ impl Vecs {
|
||||
let supply = SupplyVecs::forced_import(&db, v1, indexes, price)?;
|
||||
let value = ValueVecs::forced_import(&db, v1, indexes)?;
|
||||
let cap = CapVecs::forced_import(&db, v1, indexes)?;
|
||||
let pricing = PricingVecs::forced_import(&db, version, indexes, price)?;
|
||||
let pricing = PricingVecs::forced_import(&db, version, indexes)?;
|
||||
let adjusted = AdjustedVecs::forced_import(&db, version, indexes)?;
|
||||
let reserve_risk = ReserveRiskVecs::forced_import(&db, v1, indexes, compute_dollars)?;
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ use super::Vecs;
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{ComputedFromDateRatio, PriceFromHeight},
|
||||
price,
|
||||
};
|
||||
|
||||
impl Vecs {
|
||||
@@ -14,7 +13,6 @@ impl Vecs {
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
price: Option<&price::Vecs>,
|
||||
) -> Result<Self> {
|
||||
let vaulted_price = PriceFromHeight::forced_import(db, "vaulted_price", version, indexes)?;
|
||||
let vaulted_price_ratio = ComputedFromDateRatio::forced_import(
|
||||
@@ -24,7 +22,6 @@ impl Vecs {
|
||||
version,
|
||||
indexes,
|
||||
true,
|
||||
price,
|
||||
)?;
|
||||
|
||||
let active_price = PriceFromHeight::forced_import(db, "active_price", version, indexes)?;
|
||||
@@ -35,7 +32,6 @@ impl Vecs {
|
||||
version,
|
||||
indexes,
|
||||
true,
|
||||
price,
|
||||
)?;
|
||||
|
||||
let true_market_mean =
|
||||
@@ -47,7 +43,6 @@ impl Vecs {
|
||||
version,
|
||||
indexes,
|
||||
true,
|
||||
price,
|
||||
)?;
|
||||
|
||||
let cointime_price =
|
||||
@@ -59,7 +54,6 @@ impl Vecs {
|
||||
version,
|
||||
indexes,
|
||||
true,
|
||||
price,
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
|
||||
@@ -15,7 +15,7 @@ impl Vecs {
|
||||
) -> Result<()> {
|
||||
let vocdd_dateindex_sum = &value.vocdd.dateindex.sum.0;
|
||||
|
||||
self.vocdd_365d_sma.compute_sma(
|
||||
self.vocdd_365d_median.compute_rolling_median(
|
||||
starting_indexes.dateindex,
|
||||
vocdd_dateindex_sum,
|
||||
365,
|
||||
@@ -27,8 +27,8 @@ impl Vecs {
|
||||
self.hodl_bank.compute_cumulative_transformed_binary(
|
||||
starting_indexes.dateindex,
|
||||
price_close,
|
||||
&self.vocdd_365d_sma,
|
||||
|price: Close<Dollars>, sma: StoredF64| StoredF64::from(f64::from(price) - f64::from(sma)),
|
||||
&self.vocdd_365d_median,
|
||||
|price: Close<Dollars>, median: StoredF64| StoredF64::from(f64::from(price) - f64::from(median)),
|
||||
exit,
|
||||
)?;
|
||||
|
||||
@@ -47,54 +47,3 @@ impl Vecs {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_hodl_bank_formula() {
|
||||
let prices = [100.0, 110.0, 105.0, 120.0, 115.0];
|
||||
let vocdd_sma = [50.0, 55.0, 52.0, 60.0, 58.0];
|
||||
|
||||
let mut hodl_bank = 0.0_f64;
|
||||
let mut expected = Vec::new();
|
||||
|
||||
for i in 0..prices.len() {
|
||||
hodl_bank += prices[i] - vocdd_sma[i];
|
||||
expected.push(hodl_bank);
|
||||
}
|
||||
|
||||
assert!((expected[0] - 50.0).abs() < 0.001);
|
||||
assert!((expected[1] - 105.0).abs() < 0.001);
|
||||
assert!((expected[2] - 158.0).abs() < 0.001);
|
||||
assert!((expected[3] - 218.0).abs() < 0.001);
|
||||
assert!((expected[4] - 275.0).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reserve_risk_formula() {
|
||||
let price = 100.0_f64;
|
||||
let hodl_bank = 1000.0_f64;
|
||||
let reserve_risk = price / hodl_bank;
|
||||
assert!((reserve_risk - 0.1).abs() < 0.0001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reserve_risk_interpretation() {
|
||||
let high_confidence = 100.0 / 10000.0;
|
||||
let low_confidence = 100.0 / 100.0;
|
||||
assert!(high_confidence < low_confidence);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hodl_bank_negative_contribution() {
|
||||
let prices = [100.0, 80.0, 90.0];
|
||||
let vocdd_sma = [50.0, 100.0, 85.0];
|
||||
|
||||
let mut hodl_bank = 0.0_f64;
|
||||
for i in 0..prices.len() {
|
||||
hodl_bank += prices[i] - vocdd_sma[i];
|
||||
}
|
||||
|
||||
assert!((hodl_bank - 35.0).abs() < 0.001);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,12 @@ impl Vecs {
|
||||
indexes: &indexes::Vecs,
|
||||
compute_dollars: bool,
|
||||
) -> Result<Self> {
|
||||
let v1 = version + Version::ONE;
|
||||
Ok(Self {
|
||||
vocdd_365d_sma: EagerVec::forced_import(db, "vocdd_365d_sma", version)?,
|
||||
hodl_bank: EagerVec::forced_import(db, "hodl_bank", version)?,
|
||||
vocdd_365d_median: EagerVec::forced_import(db, "vocdd_365d_median", v1)?,
|
||||
hodl_bank: EagerVec::forced_import(db, "hodl_bank", v1)?,
|
||||
reserve_risk: compute_dollars
|
||||
.then(|| ComputedFromDateLast::forced_import(db, "reserve_risk", version, indexes))
|
||||
.then(|| ComputedFromDateLast::forced_import(db, "reserve_risk", v1, indexes))
|
||||
.transpose()?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::internal::ComputedFromDateLast;
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct Vecs {
|
||||
pub vocdd_365d_sma: EagerVec<PcoVec<DateIndex, StoredF64>>,
|
||||
pub vocdd_365d_median: EagerVec<PcoVec<DateIndex, StoredF64>>,
|
||||
pub hodl_bank: EagerVec<PcoVec<DateIndex, StoredF64>>,
|
||||
pub reserve_risk: Option<ComputedFromDateLast<StoredF64>>,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use brk_error::Result;
|
||||
use vecdb::Exit;
|
||||
use brk_types::{Bitcoin, Close, Dollars, StoredF64};
|
||||
use vecdb::{Exit, TypedVecIterator};
|
||||
|
||||
use super::super::activity;
|
||||
use super::Vecs;
|
||||
@@ -29,6 +30,15 @@ impl Vecs {
|
||||
.activity
|
||||
.coindays_destroyed;
|
||||
|
||||
let circulating_supply = &distribution
|
||||
.utxo_cohorts
|
||||
.all
|
||||
.metrics
|
||||
.supply
|
||||
.total
|
||||
.bitcoin
|
||||
.height;
|
||||
|
||||
self.cointime_value_destroyed
|
||||
.compute_all(indexes, starting_indexes, exit, |vec| {
|
||||
vec.compute_multiply(
|
||||
@@ -62,14 +72,27 @@ impl Vecs {
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// VOCDD: Value-weighted Coin Days Destroyed = CDD × price
|
||||
// This is a key input for Reserve Risk calculation
|
||||
// VOCDD: Value of Coin Days Destroyed = price × (CDD / circulating_supply)
|
||||
// Supply-adjusted to account for growing supply over time
|
||||
// This is a key input for Reserve Risk / HODL Bank calculation
|
||||
self.vocdd
|
||||
.compute_all(indexes, starting_indexes, exit, |vec| {
|
||||
vec.compute_multiply(
|
||||
let mut supply_iter = circulating_supply.into_iter();
|
||||
vec.compute_transform2(
|
||||
starting_indexes.height,
|
||||
&price.usd.split.close.height,
|
||||
&coindays_destroyed.height,
|
||||
|(i, price, cdd, _): (_, Close<Dollars>, StoredF64, _)| {
|
||||
let supply: Bitcoin = supply_iter.get_unwrap(i);
|
||||
let supply_f64 = f64::from(supply);
|
||||
if supply_f64 == 0.0 {
|
||||
(i, StoredF64::from(0.0))
|
||||
} else {
|
||||
// VOCDD = price × (CDD / supply)
|
||||
let vocdd = f64::from(price) * f64::from(cdd) / supply_f64;
|
||||
(i, StoredF64::from(vocdd))
|
||||
}
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
|
||||
@@ -29,7 +29,7 @@ impl Vecs {
|
||||
vocdd: ComputedFromHeightSumCum::forced_import(
|
||||
db,
|
||||
"vocdd",
|
||||
version,
|
||||
version + Version::ONE,
|
||||
indexes,
|
||||
)?,
|
||||
})
|
||||
|
||||
@@ -1,14 +1,63 @@
|
||||
use brk_cohort::ByAddressType;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, StoredU64, Version};
|
||||
use brk_types::{Height, StoredF64, StoredU64, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, Database, EagerVec, Exit, GenericStoredVec, PcoVec, TypedVecIterator,
|
||||
};
|
||||
|
||||
use crate::{ComputeIndexes, indexes, internal::ComputedFromHeightLast};
|
||||
use crate::{ComputeIndexes, indexes, internal::{ComputedFromDateLast, ComputedFromHeightLast}};
|
||||
|
||||
/// Address count with 30d change metric for a single type.
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct AddrCountVecs {
|
||||
#[traversable(flatten)]
|
||||
pub count: ComputedFromHeightLast<StoredU64>,
|
||||
pub _30d_change: ComputedFromDateLast<StoredF64>,
|
||||
}
|
||||
|
||||
impl AddrCountVecs {
|
||||
pub fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
count: ComputedFromHeightLast::forced_import(db, name, version, indexes)?,
|
||||
_30d_change: ComputedFromDateLast::forced_import(
|
||||
db,
|
||||
&format!("{name}_30d_change"),
|
||||
version,
|
||||
indexes,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn compute_rest(
|
||||
&mut self,
|
||||
indexes: &indexes::Vecs,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.count.compute_rest(indexes, starting_indexes, exit)?;
|
||||
|
||||
self._30d_change
|
||||
.compute_all(starting_indexes, exit, |v| {
|
||||
v.compute_change(
|
||||
starting_indexes.dateindex,
|
||||
&*self.count.dateindex,
|
||||
30,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Address count per address type (runtime state).
|
||||
#[derive(Debug, Default, Deref, DerefMut)]
|
||||
@@ -28,47 +77,54 @@ impl From<(&AddressTypeToAddrCountVecs, Height)> for AddressTypeToAddressCount {
|
||||
Self(ByAddressType {
|
||||
p2pk65: groups
|
||||
.p2pk65
|
||||
.count
|
||||
.height
|
||||
.into_iter()
|
||||
.get_unwrap(prev_height)
|
||||
.into(),
|
||||
p2pk33: groups
|
||||
.p2pk33
|
||||
.count
|
||||
.height
|
||||
.into_iter()
|
||||
.get_unwrap(prev_height)
|
||||
.into(),
|
||||
p2pkh: groups
|
||||
.p2pkh
|
||||
.count
|
||||
.height
|
||||
.into_iter()
|
||||
.get_unwrap(prev_height)
|
||||
.into(),
|
||||
p2sh: groups
|
||||
.p2sh
|
||||
.count
|
||||
.height
|
||||
.into_iter()
|
||||
.get_unwrap(prev_height)
|
||||
.into(),
|
||||
p2wpkh: groups
|
||||
.p2wpkh
|
||||
.count
|
||||
.height
|
||||
.into_iter()
|
||||
.get_unwrap(prev_height)
|
||||
.into(),
|
||||
p2wsh: groups
|
||||
.p2wsh
|
||||
.count
|
||||
.height
|
||||
.into_iter()
|
||||
.get_unwrap(prev_height)
|
||||
.into(),
|
||||
p2tr: groups
|
||||
.p2tr
|
||||
.count
|
||||
.height
|
||||
.into_iter()
|
||||
.get_unwrap(prev_height)
|
||||
.into(),
|
||||
p2a: groups.p2a.height.into_iter().get_unwrap(prev_height).into(),
|
||||
p2a: groups.p2a.count.height.into_iter().get_unwrap(prev_height).into(),
|
||||
})
|
||||
} else {
|
||||
Default::default()
|
||||
@@ -76,13 +132,13 @@ impl From<(&AddressTypeToAddrCountVecs, Height)> for AddressTypeToAddressCount {
|
||||
}
|
||||
}
|
||||
|
||||
/// Address count per address type, with height + derived indexes.
|
||||
/// Address count per address type, with height + derived indexes + 30d change.
|
||||
#[derive(Clone, Deref, DerefMut, Traversable)]
|
||||
pub struct AddressTypeToAddrCountVecs(ByAddressType<ComputedFromHeightLast<StoredU64>>);
|
||||
pub struct AddressTypeToAddrCountVecs(ByAddressType<AddrCountVecs>);
|
||||
|
||||
impl From<ByAddressType<ComputedFromHeightLast<StoredU64>>> for AddressTypeToAddrCountVecs {
|
||||
impl From<ByAddressType<AddrCountVecs>> for AddressTypeToAddrCountVecs {
|
||||
#[inline]
|
||||
fn from(value: ByAddressType<ComputedFromHeightLast<StoredU64>>) -> Self {
|
||||
fn from(value: ByAddressType<AddrCountVecs>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
@@ -95,8 +151,8 @@ impl AddressTypeToAddrCountVecs {
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self::from(
|
||||
ByAddressType::<ComputedFromHeightLast<StoredU64>>::new_with_name(|type_name| {
|
||||
ComputedFromHeightLast::forced_import(
|
||||
ByAddressType::<AddrCountVecs>::new_with_name(|type_name| {
|
||||
AddrCountVecs::forced_import(
|
||||
db,
|
||||
&format!("{type_name}_{name}"),
|
||||
version,
|
||||
@@ -108,71 +164,68 @@ impl AddressTypeToAddrCountVecs {
|
||||
|
||||
pub fn min_stateful_height(&self) -> usize {
|
||||
self.p2pk65
|
||||
.count
|
||||
.height
|
||||
.len()
|
||||
.min(self.p2pk33.height.len())
|
||||
.min(self.p2pkh.height.len())
|
||||
.min(self.p2sh.height.len())
|
||||
.min(self.p2wpkh.height.len())
|
||||
.min(self.p2wsh.height.len())
|
||||
.min(self.p2tr.height.len())
|
||||
.min(self.p2a.height.len())
|
||||
.min(self.p2pk33.count.height.len())
|
||||
.min(self.p2pkh.count.height.len())
|
||||
.min(self.p2sh.count.height.len())
|
||||
.min(self.p2wpkh.count.height.len())
|
||||
.min(self.p2wsh.count.height.len())
|
||||
.min(self.p2tr.count.height.len())
|
||||
.min(self.p2a.count.height.len())
|
||||
}
|
||||
|
||||
pub fn par_iter_height_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
let inner = &mut self.0;
|
||||
[
|
||||
&mut inner.p2pk65.height as &mut dyn AnyStoredVec,
|
||||
&mut inner.p2pk33.height as &mut dyn AnyStoredVec,
|
||||
&mut inner.p2pkh.height as &mut dyn AnyStoredVec,
|
||||
&mut inner.p2sh.height as &mut dyn AnyStoredVec,
|
||||
&mut inner.p2wpkh.height as &mut dyn AnyStoredVec,
|
||||
&mut inner.p2wsh.height as &mut dyn AnyStoredVec,
|
||||
&mut inner.p2tr.height as &mut dyn AnyStoredVec,
|
||||
&mut inner.p2a.height as &mut dyn AnyStoredVec,
|
||||
&mut inner.p2pk65.count.height as &mut dyn AnyStoredVec,
|
||||
&mut inner.p2pk33.count.height as &mut dyn AnyStoredVec,
|
||||
&mut inner.p2pkh.count.height as &mut dyn AnyStoredVec,
|
||||
&mut inner.p2sh.count.height as &mut dyn AnyStoredVec,
|
||||
&mut inner.p2wpkh.count.height as &mut dyn AnyStoredVec,
|
||||
&mut inner.p2wsh.count.height as &mut dyn AnyStoredVec,
|
||||
&mut inner.p2tr.count.height as &mut dyn AnyStoredVec,
|
||||
&mut inner.p2a.count.height as &mut dyn AnyStoredVec,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
|
||||
pub fn write_height(&mut self) -> Result<()> {
|
||||
self.p2pk65.height.write()?;
|
||||
self.p2pk33.height.write()?;
|
||||
self.p2pkh.height.write()?;
|
||||
self.p2sh.height.write()?;
|
||||
self.p2wpkh.height.write()?;
|
||||
self.p2wsh.height.write()?;
|
||||
self.p2tr.height.write()?;
|
||||
self.p2a.height.write()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn truncate_push_height(
|
||||
&mut self,
|
||||
height: Height,
|
||||
addr_counts: &AddressTypeToAddressCount,
|
||||
) -> Result<()> {
|
||||
self.p2pk65
|
||||
.count
|
||||
.height
|
||||
.truncate_push(height, addr_counts.p2pk65.into())?;
|
||||
self.p2pk33
|
||||
.count
|
||||
.height
|
||||
.truncate_push(height, addr_counts.p2pk33.into())?;
|
||||
self.p2pkh
|
||||
.count
|
||||
.height
|
||||
.truncate_push(height, addr_counts.p2pkh.into())?;
|
||||
self.p2sh
|
||||
.count
|
||||
.height
|
||||
.truncate_push(height, addr_counts.p2sh.into())?;
|
||||
self.p2wpkh
|
||||
.count
|
||||
.height
|
||||
.truncate_push(height, addr_counts.p2wpkh.into())?;
|
||||
self.p2wsh
|
||||
.count
|
||||
.height
|
||||
.truncate_push(height, addr_counts.p2wsh.into())?;
|
||||
self.p2tr
|
||||
.count
|
||||
.height
|
||||
.truncate_push(height, addr_counts.p2tr.into())?;
|
||||
self.p2a
|
||||
.count
|
||||
.height
|
||||
.truncate_push(height, addr_counts.p2a.into())?;
|
||||
Ok(())
|
||||
@@ -180,14 +233,14 @@ impl AddressTypeToAddrCountVecs {
|
||||
|
||||
pub fn reset_height(&mut self) -> Result<()> {
|
||||
use vecdb::GenericStoredVec;
|
||||
self.p2pk65.height.reset()?;
|
||||
self.p2pk33.height.reset()?;
|
||||
self.p2pkh.height.reset()?;
|
||||
self.p2sh.height.reset()?;
|
||||
self.p2wpkh.height.reset()?;
|
||||
self.p2wsh.height.reset()?;
|
||||
self.p2tr.height.reset()?;
|
||||
self.p2a.height.reset()?;
|
||||
self.p2pk65.count.height.reset()?;
|
||||
self.p2pk33.count.height.reset()?;
|
||||
self.p2pkh.count.height.reset()?;
|
||||
self.p2sh.count.height.reset()?;
|
||||
self.p2wpkh.count.height.reset()?;
|
||||
self.p2wsh.count.height.reset()?;
|
||||
self.p2tr.count.height.reset()?;
|
||||
self.p2a.count.height.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -210,26 +263,26 @@ impl AddressTypeToAddrCountVecs {
|
||||
|
||||
pub fn by_height(&self) -> Vec<&EagerVec<PcoVec<Height, StoredU64>>> {
|
||||
vec![
|
||||
&self.p2pk65.height,
|
||||
&self.p2pk33.height,
|
||||
&self.p2pkh.height,
|
||||
&self.p2sh.height,
|
||||
&self.p2wpkh.height,
|
||||
&self.p2wsh.height,
|
||||
&self.p2tr.height,
|
||||
&self.p2a.height,
|
||||
&self.p2pk65.count.height,
|
||||
&self.p2pk33.count.height,
|
||||
&self.p2pkh.count.height,
|
||||
&self.p2sh.count.height,
|
||||
&self.p2wpkh.count.height,
|
||||
&self.p2wsh.count.height,
|
||||
&self.p2tr.count.height,
|
||||
&self.p2a.count.height,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct AddrCountVecs {
|
||||
pub all: ComputedFromHeightLast<StoredU64>,
|
||||
pub struct AddrCountsVecs {
|
||||
pub all: AddrCountVecs,
|
||||
#[traversable(flatten)]
|
||||
pub by_addresstype: AddressTypeToAddrCountVecs,
|
||||
}
|
||||
|
||||
impl AddrCountVecs {
|
||||
impl AddrCountsVecs {
|
||||
pub fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
@@ -237,22 +290,22 @@ impl AddrCountVecs {
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
all: ComputedFromHeightLast::forced_import(db, name, version, indexes)?,
|
||||
all: AddrCountVecs::forced_import(db, name, version, indexes)?,
|
||||
by_addresstype: AddressTypeToAddrCountVecs::forced_import(db, name, version, indexes)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn min_stateful_height(&self) -> usize {
|
||||
self.all.height.len().min(self.by_addresstype.min_stateful_height())
|
||||
self.all.count.height.len().min(self.by_addresstype.min_stateful_height())
|
||||
}
|
||||
|
||||
pub fn par_iter_height_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
rayon::iter::once(&mut self.all.height as &mut dyn AnyStoredVec)
|
||||
rayon::iter::once(&mut self.all.count.height as &mut dyn AnyStoredVec)
|
||||
.chain(self.by_addresstype.par_iter_height_mut())
|
||||
}
|
||||
|
||||
pub fn reset_height(&mut self) -> Result<()> {
|
||||
self.all.height.reset()?;
|
||||
self.all.count.height.reset()?;
|
||||
self.by_addresstype.reset_height()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -263,7 +316,7 @@ impl AddrCountVecs {
|
||||
total: u64,
|
||||
addr_counts: &AddressTypeToAddressCount,
|
||||
) -> Result<()> {
|
||||
self.all.height.truncate_push(height, total.into())?;
|
||||
self.all.count.height.truncate_push(height, total.into())?;
|
||||
self.by_addresstype
|
||||
.truncate_push_height(height, addr_counts)?;
|
||||
Ok(())
|
||||
@@ -280,10 +333,22 @@ impl AddrCountVecs {
|
||||
|
||||
let sources = self.by_addresstype.by_height();
|
||||
self.all
|
||||
.count
|
||||
.compute_all(indexes, starting_indexes, exit, |height_vec| {
|
||||
Ok(height_vec.compute_sum_of_others(starting_indexes.height, &sources, exit)?)
|
||||
})?;
|
||||
|
||||
self.all._30d_change
|
||||
.compute_all(starting_indexes, exit, |v| {
|
||||
v.compute_change(
|
||||
starting_indexes.dateindex,
|
||||
&*self.all.count.dateindex,
|
||||
30,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{
|
||||
EmptyAddressData, EmptyAddressIndex, Height, LoadedAddressData, LoadedAddressIndex, Version,
|
||||
EmptyAddressData, EmptyAddressIndex, FundedAddressData, FundedAddressIndex, Height, Version,
|
||||
};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{
|
||||
@@ -10,10 +10,10 @@ use vecdb::{
|
||||
|
||||
const SAVED_STAMPED_CHANGES: u16 = 10;
|
||||
|
||||
/// Storage for both loaded and empty address data.
|
||||
/// Storage for both funded and empty address data.
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct AddressesDataVecs {
|
||||
pub loaded: BytesVec<LoadedAddressIndex, LoadedAddressData>,
|
||||
pub funded: BytesVec<FundedAddressIndex, FundedAddressData>,
|
||||
pub empty: BytesVec<EmptyAddressIndex, EmptyAddressData>,
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ impl AddressesDataVecs {
|
||||
/// Import from database.
|
||||
pub fn forced_import(db: &Database, version: Version) -> Result<Self> {
|
||||
Ok(Self {
|
||||
loaded: BytesVec::forced_import_with(
|
||||
ImportOptions::new(db, "loadedaddressdata", version)
|
||||
funded: BytesVec::forced_import_with(
|
||||
ImportOptions::new(db, "fundedaddressdata", version)
|
||||
.with_saved_stamped_changes(SAVED_STAMPED_CHANGES),
|
||||
)?,
|
||||
empty: BytesVec::forced_import_with(
|
||||
@@ -32,31 +32,31 @@ impl AddressesDataVecs {
|
||||
})
|
||||
}
|
||||
|
||||
/// Get minimum stamped height across loaded and empty data.
|
||||
/// Get minimum stamped height across funded and empty data.
|
||||
pub fn min_stamped_height(&self) -> Height {
|
||||
Height::from(self.loaded.stamp())
|
||||
Height::from(self.funded.stamp())
|
||||
.incremented()
|
||||
.min(Height::from(self.empty.stamp()).incremented())
|
||||
}
|
||||
|
||||
/// Rollback both loaded and empty data to before the given stamp.
|
||||
/// Rollback both funded and empty data to before the given stamp.
|
||||
pub fn rollback_before(&mut self, stamp: Stamp) -> Result<[Stamp; 2]> {
|
||||
Ok([
|
||||
self.loaded.rollback_before(stamp)?,
|
||||
self.funded.rollback_before(stamp)?,
|
||||
self.empty.rollback_before(stamp)?,
|
||||
])
|
||||
}
|
||||
|
||||
/// Reset both loaded and empty data.
|
||||
/// Reset both funded and empty data.
|
||||
pub fn reset(&mut self) -> Result<()> {
|
||||
self.loaded.reset()?;
|
||||
self.funded.reset()?;
|
||||
self.empty.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Flush both loaded and empty data with stamp.
|
||||
/// Flush both funded and empty data with stamp.
|
||||
pub fn write(&mut self, stamp: Stamp, with_changes: bool) -> Result<()> {
|
||||
self.loaded
|
||||
self.funded
|
||||
.stamped_write_maybe_with_changes(stamp, with_changes)?;
|
||||
self.empty
|
||||
.stamped_write_maybe_with_changes(stamp, with_changes)?;
|
||||
@@ -66,7 +66,7 @@ impl AddressesDataVecs {
|
||||
/// Returns a parallel iterator over all vecs for parallel writing.
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
vec![
|
||||
&mut self.loaded as &mut dyn AnyStoredVec,
|
||||
&mut self.funded as &mut dyn AnyStoredVec,
|
||||
&mut self.empty as &mut dyn AnyStoredVec,
|
||||
]
|
||||
.into_par_iter()
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
internal::{LazyBinaryComputedFromHeightDistribution, RatioU64F32},
|
||||
};
|
||||
|
||||
use super::{AddrCountVecs, NewAddrCountVecs};
|
||||
use super::{AddrCountsVecs, NewAddrCountVecs};
|
||||
|
||||
/// Growth rate by type - lazy ratio with distribution stats
|
||||
pub type GrowthRateByType =
|
||||
@@ -31,7 +31,7 @@ impl GrowthRateVecs {
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
new_addr_count: &NewAddrCountVecs,
|
||||
addr_count: &AddrCountVecs,
|
||||
addr_count: &AddrCountsVecs,
|
||||
) -> Result<Self> {
|
||||
let all = make_growth_rate(
|
||||
db,
|
||||
@@ -39,7 +39,7 @@ impl GrowthRateVecs {
|
||||
version,
|
||||
indexes,
|
||||
&new_addr_count.all.height,
|
||||
&addr_count.all.height,
|
||||
&addr_count.all.count.height,
|
||||
)?;
|
||||
|
||||
let by_addresstype: GrowthRateByType = zip2_by_addresstype(
|
||||
@@ -52,7 +52,7 @@ impl GrowthRateVecs {
|
||||
version,
|
||||
indexes,
|
||||
&new.height,
|
||||
&addr.height,
|
||||
&addr.count.height,
|
||||
)
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -136,7 +136,7 @@ define_any_address_indexes_vecs!(
|
||||
|
||||
impl AnyAddressIndexesVecs {
|
||||
/// Process index updates in parallel by address type.
|
||||
/// Accepts two maps (e.g. from empty and loaded processing) and merges per-thread.
|
||||
/// Accepts two maps (e.g. from empty and funded processing) and merges per-thread.
|
||||
/// Updates existing entries and pushes new ones (sorted).
|
||||
/// Returns (update_count, push_count).
|
||||
pub fn par_batch_update(
|
||||
|
||||
@@ -8,7 +8,7 @@ mod total_addr_count;
|
||||
mod type_map;
|
||||
|
||||
pub use activity::{AddressActivityVecs, AddressTypeToActivityCounts};
|
||||
pub use address_count::{AddrCountVecs, AddressTypeToAddressCount};
|
||||
pub use address_count::{AddrCountsVecs, AddressTypeToAddressCount};
|
||||
pub use data::AddressesDataVecs;
|
||||
pub use growth_rate::GrowthRateVecs;
|
||||
pub use indexes::AnyAddressIndexesVecs;
|
||||
|
||||
@@ -8,7 +8,7 @@ use vecdb::{Database, Exit, IterableCloneableVec};
|
||||
|
||||
use crate::{ComputeIndexes, indexes, internal::{LazyBinaryComputedFromHeightLast, U64Plus}};
|
||||
|
||||
use super::AddrCountVecs;
|
||||
use super::AddrCountsVecs;
|
||||
|
||||
/// Total addresses by type - lazy sum with all derived indexes
|
||||
pub type TotalAddrCountByType =
|
||||
@@ -27,15 +27,15 @@ impl TotalAddrCountVecs {
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
addr_count: &AddrCountVecs,
|
||||
empty_addr_count: &AddrCountVecs,
|
||||
addr_count: &AddrCountsVecs,
|
||||
empty_addr_count: &AddrCountsVecs,
|
||||
) -> Result<Self> {
|
||||
let all = LazyBinaryComputedFromHeightLast::forced_import::<U64Plus>(
|
||||
db,
|
||||
"total_addr_count",
|
||||
version,
|
||||
addr_count.all.height.boxed_clone(),
|
||||
empty_addr_count.all.height.boxed_clone(),
|
||||
addr_count.all.count.height.boxed_clone(),
|
||||
empty_addr_count.all.count.height.boxed_clone(),
|
||||
indexes,
|
||||
)?;
|
||||
|
||||
@@ -47,8 +47,8 @@ impl TotalAddrCountVecs {
|
||||
db,
|
||||
&format!("{name}_total_addr_count"),
|
||||
version,
|
||||
addr.height.boxed_clone(),
|
||||
empty.height.boxed_clone(),
|
||||
addr.count.height.boxed_clone(),
|
||||
empty.count.height.boxed_clone(),
|
||||
indexes,
|
||||
)
|
||||
},
|
||||
|
||||
+22
-23
@@ -1,6 +1,5 @@
|
||||
use brk_cohort::ByAddressType;
|
||||
use brk_types::{AnyAddressDataIndexEnum, LoadedAddressData, OutputType, TypeIndex};
|
||||
use vecdb::GenericStoredVec;
|
||||
use brk_types::{AnyAddressDataIndexEnum, FundedAddressData, OutputType, TypeIndex};
|
||||
|
||||
use crate::distribution::{
|
||||
address::{AddressTypeToTypeIndexMap, AddressesDataVecs, AnyAddressIndexesVecs},
|
||||
@@ -8,7 +7,7 @@ use crate::distribution::{
|
||||
};
|
||||
|
||||
use super::super::cohort::{
|
||||
EmptyAddressDataWithSource, LoadedAddressDataWithSource, TxIndexVec, WithAddressDataSource,
|
||||
EmptyAddressDataWithSource, FundedAddressDataWithSource, TxIndexVec, WithAddressDataSource,
|
||||
update_tx_counts,
|
||||
};
|
||||
use super::lookup::AddressLookup;
|
||||
@@ -16,7 +15,7 @@ use super::lookup::AddressLookup;
|
||||
/// Cache for address data within a flush interval.
|
||||
pub struct AddressCache {
|
||||
/// Addresses with non-zero balance
|
||||
loaded: AddressTypeToTypeIndexMap<LoadedAddressDataWithSource>,
|
||||
funded: AddressTypeToTypeIndexMap<FundedAddressDataWithSource>,
|
||||
/// Addresses that became empty (zero balance)
|
||||
empty: AddressTypeToTypeIndexMap<EmptyAddressDataWithSource>,
|
||||
}
|
||||
@@ -30,15 +29,15 @@ impl Default for AddressCache {
|
||||
impl AddressCache {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
loaded: AddressTypeToTypeIndexMap::default(),
|
||||
funded: AddressTypeToTypeIndexMap::default(),
|
||||
empty: AddressTypeToTypeIndexMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if address is in cache (either loaded or empty).
|
||||
/// Check if address is in cache (either funded or empty).
|
||||
#[inline]
|
||||
pub fn contains(&self, address_type: OutputType, typeindex: TypeIndex) -> bool {
|
||||
self.loaded
|
||||
self.funded
|
||||
.get(address_type)
|
||||
.is_some_and(|m| m.contains_key(&typeindex))
|
||||
|| self
|
||||
@@ -47,24 +46,24 @@ impl AddressCache {
|
||||
.is_some_and(|m| m.contains_key(&typeindex))
|
||||
}
|
||||
|
||||
/// Merge address data into loaded cache.
|
||||
/// Merge address data into funded cache.
|
||||
#[inline]
|
||||
pub fn merge_loaded(&mut self, data: AddressTypeToTypeIndexMap<LoadedAddressDataWithSource>) {
|
||||
self.loaded.merge_mut(data);
|
||||
pub fn merge_funded(&mut self, data: AddressTypeToTypeIndexMap<FundedAddressDataWithSource>) {
|
||||
self.funded.merge_mut(data);
|
||||
}
|
||||
|
||||
/// Create an AddressLookup view into this cache.
|
||||
#[inline]
|
||||
pub fn as_lookup(&mut self) -> AddressLookup<'_> {
|
||||
AddressLookup {
|
||||
loaded: &mut self.loaded,
|
||||
funded: &mut self.funded,
|
||||
empty: &mut self.empty,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update transaction counts for addresses.
|
||||
pub fn update_tx_counts(&mut self, txindex_vecs: AddressTypeToTypeIndexMap<TxIndexVec>) {
|
||||
update_tx_counts(&mut self.loaded, &mut self.empty, txindex_vecs);
|
||||
update_tx_counts(&mut self.funded, &mut self.empty, txindex_vecs);
|
||||
}
|
||||
|
||||
/// Take the cache contents for flushing, leaving empty caches.
|
||||
@@ -72,18 +71,18 @@ impl AddressCache {
|
||||
&mut self,
|
||||
) -> (
|
||||
AddressTypeToTypeIndexMap<EmptyAddressDataWithSource>,
|
||||
AddressTypeToTypeIndexMap<LoadedAddressDataWithSource>,
|
||||
AddressTypeToTypeIndexMap<FundedAddressDataWithSource>,
|
||||
) {
|
||||
(
|
||||
std::mem::take(&mut self.empty),
|
||||
std::mem::take(&mut self.loaded),
|
||||
std::mem::take(&mut self.funded),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Load address data from storage or create new.
|
||||
///
|
||||
/// Returns None if address is already in cache (loaded or empty).
|
||||
/// Returns None if address is already in cache (funded or empty).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn load_uncached_address_data(
|
||||
address_type: OutputType,
|
||||
@@ -93,11 +92,11 @@ pub fn load_uncached_address_data(
|
||||
vr: &VecsReaders,
|
||||
any_address_indexes: &AnyAddressIndexesVecs,
|
||||
addresses_data: &AddressesDataVecs,
|
||||
) -> Option<LoadedAddressDataWithSource> {
|
||||
) -> Option<FundedAddressDataWithSource> {
|
||||
// Check if this is a new address (typeindex >= first for this height)
|
||||
let first = *first_addressindexes.get(address_type).unwrap();
|
||||
if first <= typeindex {
|
||||
return Some(WithAddressDataSource::New(LoadedAddressData::default()));
|
||||
return Some(WithAddressDataSource::New(FundedAddressData::default()));
|
||||
}
|
||||
|
||||
// Skip if already in cache
|
||||
@@ -110,13 +109,13 @@ pub fn load_uncached_address_data(
|
||||
let anyaddressindex = any_address_indexes.get(address_type, typeindex, reader);
|
||||
|
||||
Some(match anyaddressindex.to_enum() {
|
||||
AnyAddressDataIndexEnum::Loaded(loaded_index) => {
|
||||
let reader = &vr.anyaddressindex_to_anyaddressdata.loaded;
|
||||
AnyAddressDataIndexEnum::Funded(funded_index) => {
|
||||
let reader = &vr.anyaddressindex_to_anyaddressdata.funded;
|
||||
// Use get_any_or_read_unwrap to check updated layer (needed after rollback)
|
||||
let loaded_data = addresses_data
|
||||
.loaded
|
||||
.get_any_or_read_unwrap(loaded_index, reader);
|
||||
WithAddressDataSource::FromLoaded(loaded_index, loaded_data)
|
||||
let funded_data = addresses_data
|
||||
.funded
|
||||
.get_any_or_read_unwrap(funded_index, reader);
|
||||
WithAddressDataSource::FromFunded(funded_index, funded_data)
|
||||
}
|
||||
AnyAddressDataIndexEnum::Empty(empty_index) => {
|
||||
let reader = &vr.anyaddressindex_to_anyaddressdata.empty;
|
||||
|
||||
+13
-13
@@ -1,9 +1,9 @@
|
||||
use brk_types::{LoadedAddressData, OutputType, TypeIndex};
|
||||
use brk_types::{FundedAddressData, OutputType, TypeIndex};
|
||||
|
||||
use crate::distribution::address::AddressTypeToTypeIndexMap;
|
||||
|
||||
use super::super::cohort::{
|
||||
EmptyAddressDataWithSource, LoadedAddressDataWithSource, WithAddressDataSource,
|
||||
EmptyAddressDataWithSource, FundedAddressDataWithSource, WithAddressDataSource,
|
||||
};
|
||||
|
||||
/// Tracking status of an address - determines cohort update strategy.
|
||||
@@ -19,7 +19,7 @@ pub enum TrackingStatus {
|
||||
|
||||
/// Context for looking up and storing address data during block processing.
|
||||
pub struct AddressLookup<'a> {
|
||||
pub loaded: &'a mut AddressTypeToTypeIndexMap<LoadedAddressDataWithSource>,
|
||||
pub funded: &'a mut AddressTypeToTypeIndexMap<FundedAddressDataWithSource>,
|
||||
pub empty: &'a mut AddressTypeToTypeIndexMap<EmptyAddressDataWithSource>,
|
||||
}
|
||||
|
||||
@@ -28,21 +28,21 @@ impl<'a> AddressLookup<'a> {
|
||||
&mut self,
|
||||
output_type: OutputType,
|
||||
type_index: TypeIndex,
|
||||
) -> (&mut LoadedAddressDataWithSource, TrackingStatus) {
|
||||
) -> (&mut FundedAddressDataWithSource, TrackingStatus) {
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
let map = self.loaded.get_mut(output_type).unwrap();
|
||||
let map = self.funded.get_mut(output_type).unwrap();
|
||||
|
||||
match map.entry(type_index) {
|
||||
Entry::Occupied(entry) => {
|
||||
// Address is in cache. Need to determine if it's been processed
|
||||
// by process_received (added to a cohort) or just loaded this block.
|
||||
// by process_received (added to a cohort) or just funded this block.
|
||||
//
|
||||
// - If wrapper is New AND funded_txo_count == 0: hasn't received yet,
|
||||
// was just created in process_outputs this block → New
|
||||
// - If wrapper is New AND funded_txo_count > 0: received in previous
|
||||
// block but still in cache (no flush) → Tracked
|
||||
// - If wrapper is FromLoaded: loaded from storage → Tracked
|
||||
// - If wrapper is FromFunded: funded from storage → Tracked
|
||||
// - If wrapper is FromEmpty AND utxo_count == 0: still empty → WasEmpty
|
||||
// - If wrapper is FromEmpty AND utxo_count > 0: already received → Tracked
|
||||
let status = match entry.get() {
|
||||
@@ -53,7 +53,7 @@ impl<'a> AddressLookup<'a> {
|
||||
TrackingStatus::Tracked
|
||||
}
|
||||
}
|
||||
WithAddressDataSource::FromLoaded(..) => TrackingStatus::Tracked,
|
||||
WithAddressDataSource::FromFunded(..) => TrackingStatus::Tracked,
|
||||
WithAddressDataSource::FromEmpty(_, data) => {
|
||||
if data.utxo_count() == 0 {
|
||||
TrackingStatus::WasEmpty
|
||||
@@ -71,7 +71,7 @@ impl<'a> AddressLookup<'a> {
|
||||
return (entry.insert(empty_data.into()), TrackingStatus::WasEmpty);
|
||||
}
|
||||
(
|
||||
entry.insert(WithAddressDataSource::New(LoadedAddressData::default())),
|
||||
entry.insert(WithAddressDataSource::New(FundedAddressData::default())),
|
||||
TrackingStatus::New,
|
||||
)
|
||||
}
|
||||
@@ -83,18 +83,18 @@ impl<'a> AddressLookup<'a> {
|
||||
&mut self,
|
||||
output_type: OutputType,
|
||||
type_index: TypeIndex,
|
||||
) -> &mut LoadedAddressDataWithSource {
|
||||
self.loaded
|
||||
) -> &mut FundedAddressDataWithSource {
|
||||
self.funded
|
||||
.get_mut(output_type)
|
||||
.unwrap()
|
||||
.get_mut(&type_index)
|
||||
.expect("Address must exist for send")
|
||||
}
|
||||
|
||||
/// Move address from loaded to empty set.
|
||||
/// Move address from funded to empty set.
|
||||
pub fn move_to_empty(&mut self, output_type: OutputType, type_index: TypeIndex) {
|
||||
let data = self
|
||||
.loaded
|
||||
.funded
|
||||
.get_mut(output_type)
|
||||
.unwrap()
|
||||
.remove(&type_index)
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::{
|
||||
AnyAddressIndex, EmptyAddressData, EmptyAddressIndex, LoadedAddressData, LoadedAddressIndex,
|
||||
AnyAddressIndex, EmptyAddressData, EmptyAddressIndex, FundedAddressData, FundedAddressIndex,
|
||||
OutputType, TypeIndex,
|
||||
};
|
||||
use vecdb::AnyVec;
|
||||
|
||||
use crate::distribution::{AddressTypeToTypeIndexMap, AddressesDataVecs};
|
||||
|
||||
use super::with_source::{EmptyAddressDataWithSource, LoadedAddressDataWithSource};
|
||||
use super::with_source::{EmptyAddressDataWithSource, FundedAddressDataWithSource};
|
||||
|
||||
/// Process loaded address data updates.
|
||||
/// Process funded address data updates.
|
||||
///
|
||||
/// Handles:
|
||||
/// - New loaded address: push to loaded storage
|
||||
/// - Updated loaded address (was loaded): update in place
|
||||
/// - Transition empty -> loaded: delete from empty, push to loaded
|
||||
pub fn process_loaded_addresses(
|
||||
/// - New funded address: push to funded storage
|
||||
/// - Updated funded address (was funded): update in place
|
||||
/// - Transition empty -> funded: delete from empty, push to funded
|
||||
pub fn process_funded_addresses(
|
||||
addresses_data: &mut AddressesDataVecs,
|
||||
loaded_updates: AddressTypeToTypeIndexMap<LoadedAddressDataWithSource>,
|
||||
funded_updates: AddressTypeToTypeIndexMap<FundedAddressDataWithSource>,
|
||||
) -> Result<AddressTypeToTypeIndexMap<AnyAddressIndex>> {
|
||||
let total: usize = loaded_updates.iter().map(|(_, m)| m.len()).sum();
|
||||
let total: usize = funded_updates.iter().map(|(_, m)| m.len()).sum();
|
||||
|
||||
let mut updates: Vec<(LoadedAddressIndex, LoadedAddressData)> = Vec::with_capacity(total);
|
||||
let mut updates: Vec<(FundedAddressIndex, FundedAddressData)> = Vec::with_capacity(total);
|
||||
let mut deletes: Vec<EmptyAddressIndex> = Vec::with_capacity(total);
|
||||
let mut pushes: Vec<(OutputType, TypeIndex, LoadedAddressData)> = Vec::with_capacity(total);
|
||||
let mut pushes: Vec<(OutputType, TypeIndex, FundedAddressData)> = Vec::with_capacity(total);
|
||||
|
||||
for (address_type, items) in loaded_updates.into_iter() {
|
||||
for (address_type, items) in funded_updates.into_iter() {
|
||||
for (typeindex, source) in items {
|
||||
match source {
|
||||
LoadedAddressDataWithSource::New(data) => {
|
||||
FundedAddressDataWithSource::New(data) => {
|
||||
pushes.push((address_type, typeindex, data));
|
||||
}
|
||||
LoadedAddressDataWithSource::FromLoaded(index, data) => {
|
||||
FundedAddressDataWithSource::FromFunded(index, data) => {
|
||||
updates.push((index, data));
|
||||
}
|
||||
LoadedAddressDataWithSource::FromEmpty(empty_index, data) => {
|
||||
FundedAddressDataWithSource::FromEmpty(empty_index, data) => {
|
||||
deletes.push(empty_index);
|
||||
pushes.push((address_type, typeindex, data));
|
||||
}
|
||||
@@ -49,16 +49,16 @@ pub fn process_loaded_addresses(
|
||||
|
||||
// Phase 2: Updates (in-place)
|
||||
for (index, data) in updates {
|
||||
addresses_data.loaded.update(index, data)?;
|
||||
addresses_data.funded.update(index, data)?;
|
||||
}
|
||||
|
||||
// Phase 3: Pushes (fill holes first, then pure pushes)
|
||||
let mut result = AddressTypeToTypeIndexMap::with_capacity(pushes.len() / 4);
|
||||
let holes_count = addresses_data.loaded.holes().len();
|
||||
let holes_count = addresses_data.funded.holes().len();
|
||||
let mut pushes_iter = pushes.into_iter();
|
||||
|
||||
for (address_type, typeindex, data) in pushes_iter.by_ref().take(holes_count) {
|
||||
let index = addresses_data.loaded.fill_first_hole_or_push(data)?;
|
||||
let index = addresses_data.funded.fill_first_hole_or_push(data)?;
|
||||
result
|
||||
.get_mut(address_type)
|
||||
.unwrap()
|
||||
@@ -66,14 +66,14 @@ pub fn process_loaded_addresses(
|
||||
}
|
||||
|
||||
// Pure pushes - no holes remain
|
||||
addresses_data.loaded.reserve_pushed(pushes_iter.len());
|
||||
let mut next_index = addresses_data.loaded.len();
|
||||
addresses_data.funded.reserve_pushed(pushes_iter.len());
|
||||
let mut next_index = addresses_data.funded.len();
|
||||
for (address_type, typeindex, data) in pushes_iter {
|
||||
addresses_data.loaded.push(data);
|
||||
result
|
||||
.get_mut(address_type)
|
||||
.unwrap()
|
||||
.insert(typeindex, AnyAddressIndex::from(LoadedAddressIndex::from(next_index)));
|
||||
addresses_data.funded.push(data);
|
||||
result.get_mut(address_type).unwrap().insert(
|
||||
typeindex,
|
||||
AnyAddressIndex::from(FundedAddressIndex::from(next_index)),
|
||||
);
|
||||
next_index += 1;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ pub fn process_loaded_addresses(
|
||||
/// Handles:
|
||||
/// - New empty address: push to empty storage
|
||||
/// - Updated empty address (was empty): update in place
|
||||
/// - Transition loaded -> empty: delete from loaded, push to empty
|
||||
/// - Transition funded -> empty: delete from funded, push to empty
|
||||
pub fn process_empty_addresses(
|
||||
addresses_data: &mut AddressesDataVecs,
|
||||
empty_updates: AddressTypeToTypeIndexMap<EmptyAddressDataWithSource>,
|
||||
@@ -93,7 +93,7 @@ pub fn process_empty_addresses(
|
||||
let total: usize = empty_updates.iter().map(|(_, m)| m.len()).sum();
|
||||
|
||||
let mut updates: Vec<(EmptyAddressIndex, EmptyAddressData)> = Vec::with_capacity(total);
|
||||
let mut deletes: Vec<LoadedAddressIndex> = Vec::with_capacity(total);
|
||||
let mut deletes: Vec<FundedAddressIndex> = Vec::with_capacity(total);
|
||||
let mut pushes: Vec<(OutputType, TypeIndex, EmptyAddressData)> = Vec::with_capacity(total);
|
||||
|
||||
for (address_type, items) in empty_updates.into_iter() {
|
||||
@@ -105,8 +105,8 @@ pub fn process_empty_addresses(
|
||||
EmptyAddressDataWithSource::FromEmpty(index, data) => {
|
||||
updates.push((index, data));
|
||||
}
|
||||
EmptyAddressDataWithSource::FromLoaded(loaded_index, data) => {
|
||||
deletes.push(loaded_index);
|
||||
EmptyAddressDataWithSource::FromFunded(funded_index, data) => {
|
||||
deletes.push(funded_index);
|
||||
pushes.push((address_type, typeindex, data));
|
||||
}
|
||||
}
|
||||
@@ -114,8 +114,8 @@ pub fn process_empty_addresses(
|
||||
}
|
||||
|
||||
// Phase 1: Deletes (creates holes)
|
||||
for loaded_index in deletes {
|
||||
addresses_data.loaded.delete(loaded_index);
|
||||
for funded_index in deletes {
|
||||
addresses_data.funded.delete(funded_index);
|
||||
}
|
||||
|
||||
// Phase 2: Updates (in-place)
|
||||
@@ -141,10 +141,10 @@ pub fn process_empty_addresses(
|
||||
let mut next_index = addresses_data.empty.len();
|
||||
for (address_type, typeindex, data) in pushes_iter {
|
||||
addresses_data.empty.push(data);
|
||||
result
|
||||
.get_mut(address_type)
|
||||
.unwrap()
|
||||
.insert(typeindex, AnyAddressIndex::from(EmptyAddressIndex::from(next_index)));
|
||||
result.get_mut(address_type).unwrap().insert(
|
||||
typeindex,
|
||||
AnyAddressIndex::from(EmptyAddressIndex::from(next_index)),
|
||||
);
|
||||
next_index += 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use brk_cohort::{AmountBucket, ByAddressType};
|
||||
use brk_types::{Dollars, Sats, TypeIndex};
|
||||
use brk_types::{CentsUnsigned, Sats, TypeIndex};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::distribution::{
|
||||
@@ -14,7 +14,7 @@ pub fn process_received(
|
||||
received_data: AddressTypeToVec<(TypeIndex, Sats)>,
|
||||
cohorts: &mut AddressCohorts,
|
||||
lookup: &mut AddressLookup<'_>,
|
||||
price: Option<Dollars>,
|
||||
price: Option<CentsUnsigned>,
|
||||
addr_count: &mut ByAddressType<u64>,
|
||||
empty_addr_count: &mut ByAddressType<u64>,
|
||||
activity_counts: &mut AddressTypeToActivityCounts,
|
||||
@@ -118,7 +118,7 @@ pub fn process_received(
|
||||
.state
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.receive_outputs(addr_data, total_value, price, output_count);
|
||||
.receive_outputs(addr_data, total_value, price.unwrap(), output_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use brk_cohort::{AmountBucket, ByAddressType};
|
||||
use brk_error::Result;
|
||||
use brk_types::{Age, CheckedSub, Dollars, Height, Sats, Timestamp, TypeIndex};
|
||||
use brk_types::{Age, CentsUnsigned, CheckedSub, Height, Sats, Timestamp, TypeIndex};
|
||||
use rustc_hash::FxHashSet;
|
||||
use vecdb::{unlikely, VecIndex};
|
||||
use vecdb::{VecIndex, unlikely};
|
||||
|
||||
use crate::distribution::{
|
||||
address::{AddressTypeToActivityCounts, HeightToAddressTypeToVec},
|
||||
cohorts::AddressCohorts,
|
||||
compute::PriceRangeMax,
|
||||
};
|
||||
|
||||
use super::super::cache::AddressLookup;
|
||||
@@ -21,17 +22,21 @@ use super::super::cache::AddressLookup;
|
||||
///
|
||||
/// Note: Takes separate price/timestamp slices instead of chain_state to allow
|
||||
/// parallel execution with UTXO cohort processing (which mutates chain_state).
|
||||
///
|
||||
/// `price_range_max` is used to compute the peak price during each UTXO's holding period
|
||||
/// for accurate peak regret calculation.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn process_sent(
|
||||
sent_data: HeightToAddressTypeToVec<(TypeIndex, Sats)>,
|
||||
cohorts: &mut AddressCohorts,
|
||||
lookup: &mut AddressLookup<'_>,
|
||||
current_price: Option<Dollars>,
|
||||
current_price: Option<CentsUnsigned>,
|
||||
price_range_max: Option<&PriceRangeMax>,
|
||||
addr_count: &mut ByAddressType<u64>,
|
||||
empty_addr_count: &mut ByAddressType<u64>,
|
||||
activity_counts: &mut AddressTypeToActivityCounts,
|
||||
received_addresses: &ByAddressType<FxHashSet<TypeIndex>>,
|
||||
height_to_price: Option<&[Dollars]>,
|
||||
height_to_price: Option<&[CentsUnsigned]>,
|
||||
height_to_timestamp: &[Timestamp],
|
||||
current_height: Height,
|
||||
current_timestamp: Timestamp,
|
||||
@@ -39,12 +44,17 @@ pub fn process_sent(
|
||||
// Track unique senders per address type (simple set, no extra data needed)
|
||||
let mut seen_senders: ByAddressType<FxHashSet<TypeIndex>> = ByAddressType::default();
|
||||
|
||||
for (prev_height, by_type) in sent_data.into_iter() {
|
||||
let prev_price = height_to_price.map(|v| v[prev_height.to_usize()]);
|
||||
let prev_timestamp = height_to_timestamp[prev_height.to_usize()];
|
||||
let blocks_old = current_height.to_usize() - prev_height.to_usize();
|
||||
for (receive_height, by_type) in sent_data.into_iter() {
|
||||
let prev_price = height_to_price.map(|v| v[receive_height.to_usize()]);
|
||||
let prev_timestamp = height_to_timestamp[receive_height.to_usize()];
|
||||
let blocks_old = current_height.to_usize() - receive_height.to_usize();
|
||||
let age = Age::new(current_timestamp, prev_timestamp, blocks_old);
|
||||
|
||||
// Compute peak price during holding period for peak regret
|
||||
// This is the max HIGH price between receive and send heights
|
||||
let peak_price: Option<CentsUnsigned> =
|
||||
price_range_max.map(|t| t.max_between(receive_height, current_height));
|
||||
|
||||
for (output_type, vec) in by_type.unwrap().into_iter() {
|
||||
// Cache mutable refs for this address type
|
||||
let type_addr_count = addr_count.get_mut(output_type).unwrap();
|
||||
@@ -91,11 +101,11 @@ pub fn process_sent(
|
||||
) {
|
||||
panic!(
|
||||
"process_sent: cohort underflow detected!\n\
|
||||
Block context: prev_height={:?}, output_type={:?}, type_index={:?}\n\
|
||||
Block context: receive_height={:?}, output_type={:?}, type_index={:?}\n\
|
||||
prev_balance={}, new_balance={}, value={}\n\
|
||||
will_be_empty={}, crossing_boundary={}\n\
|
||||
Address: {:?}",
|
||||
prev_height,
|
||||
receive_height,
|
||||
output_type,
|
||||
type_index,
|
||||
prev_balance,
|
||||
@@ -121,7 +131,7 @@ pub fn process_sent(
|
||||
*type_addr_count -= 1;
|
||||
*type_empty_count += 1;
|
||||
|
||||
// Move from loaded to empty
|
||||
// Move from funded to empty
|
||||
lookup.move_to_empty(output_type, type_index);
|
||||
} else {
|
||||
// Add to new cohort
|
||||
@@ -141,7 +151,14 @@ pub fn process_sent(
|
||||
.state
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.send(addr_data, value, current_price, prev_price, age)?;
|
||||
.send(
|
||||
addr_data,
|
||||
value,
|
||||
current_price.unwrap(),
|
||||
prev_price.unwrap(),
|
||||
peak_price.unwrap(),
|
||||
age,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::distribution::address::AddressTypeToTypeIndexMap;
|
||||
|
||||
use super::with_source::{EmptyAddressDataWithSource, LoadedAddressDataWithSource, TxIndexVec};
|
||||
use super::with_source::{EmptyAddressDataWithSource, FundedAddressDataWithSource, TxIndexVec};
|
||||
|
||||
/// Update tx_count for addresses based on unique transactions they participated in.
|
||||
///
|
||||
@@ -8,10 +8,10 @@ use super::with_source::{EmptyAddressDataWithSource, LoadedAddressDataWithSource
|
||||
/// 1. Deduplicate transaction indexes (an address may appear in multiple inputs/outputs of same tx)
|
||||
/// 2. Add the unique count to the address's tx_count field
|
||||
///
|
||||
/// Addresses are looked up in loaded_cache first, then empty_cache.
|
||||
/// NOTE: This should be called AFTER merging parallel-fetched address data into loaded_cache.
|
||||
/// Addresses are looked up in funded_cache first, then empty_cache.
|
||||
/// NOTE: This should be called AFTER merging parallel-fetched address data into funded_cache.
|
||||
pub fn update_tx_counts(
|
||||
loaded_cache: &mut AddressTypeToTypeIndexMap<LoadedAddressDataWithSource>,
|
||||
funded_cache: &mut AddressTypeToTypeIndexMap<FundedAddressDataWithSource>,
|
||||
empty_cache: &mut AddressTypeToTypeIndexMap<EmptyAddressDataWithSource>,
|
||||
mut txindex_vecs: AddressTypeToTypeIndexMap<TxIndexVec>,
|
||||
) {
|
||||
@@ -32,7 +32,7 @@ pub fn update_tx_counts(
|
||||
{
|
||||
let tx_count = txindex_vec.len() as u32;
|
||||
|
||||
if let Some(addr_data) = loaded_cache
|
||||
if let Some(addr_data) = funded_cache
|
||||
.get_mut(address_type)
|
||||
.unwrap()
|
||||
.get_mut(&typeindex)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use brk_types::{EmptyAddressData, EmptyAddressIndex, LoadedAddressData, LoadedAddressIndex, TxIndex};
|
||||
use brk_types::{
|
||||
EmptyAddressData, EmptyAddressIndex, FundedAddressData, FundedAddressIndex, TxIndex,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// Loaded address data with source tracking for flush operations.
|
||||
pub type LoadedAddressDataWithSource = WithAddressDataSource<LoadedAddressData>;
|
||||
/// Funded address data with source tracking for flush operations.
|
||||
pub type FundedAddressDataWithSource = WithAddressDataSource<FundedAddressData>;
|
||||
|
||||
/// Empty address data with source tracking for flush operations.
|
||||
pub type EmptyAddressDataWithSource = WithAddressDataSource<EmptyAddressData>;
|
||||
@@ -18,9 +20,9 @@ pub type TxIndexVec = SmallVec<[TxIndex; 4]>;
|
||||
pub enum WithAddressDataSource<T> {
|
||||
/// Brand new address (never seen before)
|
||||
New(T),
|
||||
/// Loaded from loaded address storage (with original index)
|
||||
FromLoaded(LoadedAddressIndex, T),
|
||||
/// Loaded from empty address storage (with original index)
|
||||
/// Funded from funded address storage (with original index)
|
||||
FromFunded(FundedAddressIndex, T),
|
||||
/// Funded from empty address storage (with original index)
|
||||
FromEmpty(EmptyAddressIndex, T),
|
||||
}
|
||||
|
||||
@@ -29,7 +31,7 @@ impl<T> std::ops::Deref for WithAddressDataSource<T> {
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Self::New(v) | Self::FromLoaded(_, v) | Self::FromEmpty(_, v) => v,
|
||||
Self::New(v) | Self::FromFunded(_, v) | Self::FromEmpty(_, v) => v,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,28 +39,28 @@ impl<T> std::ops::Deref for WithAddressDataSource<T> {
|
||||
impl<T> std::ops::DerefMut for WithAddressDataSource<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
match self {
|
||||
Self::New(v) | Self::FromLoaded(_, v) | Self::FromEmpty(_, v) => v,
|
||||
Self::New(v) | Self::FromFunded(_, v) | Self::FromEmpty(_, v) => v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WithAddressDataSource<EmptyAddressData>> for WithAddressDataSource<LoadedAddressData> {
|
||||
impl From<WithAddressDataSource<EmptyAddressData>> for WithAddressDataSource<FundedAddressData> {
|
||||
#[inline]
|
||||
fn from(value: WithAddressDataSource<EmptyAddressData>) -> Self {
|
||||
match value {
|
||||
WithAddressDataSource::New(v) => Self::New(v.into()),
|
||||
WithAddressDataSource::FromLoaded(i, v) => Self::FromLoaded(i, v.into()),
|
||||
WithAddressDataSource::FromFunded(i, v) => Self::FromFunded(i, v.into()),
|
||||
WithAddressDataSource::FromEmpty(i, v) => Self::FromEmpty(i, v.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WithAddressDataSource<LoadedAddressData>> for WithAddressDataSource<EmptyAddressData> {
|
||||
impl From<WithAddressDataSource<FundedAddressData>> for WithAddressDataSource<EmptyAddressData> {
|
||||
#[inline]
|
||||
fn from(value: WithAddressDataSource<LoadedAddressData>) -> Self {
|
||||
fn from(value: WithAddressDataSource<FundedAddressData>) -> Self {
|
||||
match value {
|
||||
WithAddressDataSource::New(v) => Self::New(v.into()),
|
||||
WithAddressDataSource::FromLoaded(i, v) => Self::FromLoaded(i, v.into()),
|
||||
WithAddressDataSource::FromFunded(i, v) => Self::FromFunded(i, v.into()),
|
||||
WithAddressDataSource::FromEmpty(i, v) => Self::FromEmpty(i, v.into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::distribution::address::HeightToAddressTypeToVec;
|
||||
|
||||
use super::super::{
|
||||
cache::{AddressCache, load_uncached_address_data},
|
||||
cohort::{LoadedAddressDataWithSource, TxIndexVec},
|
||||
cohort::{FundedAddressDataWithSource, TxIndexVec},
|
||||
};
|
||||
|
||||
/// Result of processing inputs for a block.
|
||||
@@ -23,7 +23,7 @@ pub struct InputsResult {
|
||||
/// Per-height, per-address-type sent data: (typeindex, value) for each address.
|
||||
pub sent_data: HeightToAddressTypeToVec<(TypeIndex, Sats)>,
|
||||
/// Address data looked up during processing, keyed by (address_type, typeindex).
|
||||
pub address_data: AddressTypeToTypeIndexMap<LoadedAddressDataWithSource>,
|
||||
pub address_data: AddressTypeToTypeIndexMap<FundedAddressDataWithSource>,
|
||||
/// Transaction indexes per address for tx_count tracking.
|
||||
pub txindex_vecs: AddressTypeToTypeIndexMap<TxIndexVec>,
|
||||
}
|
||||
@@ -100,7 +100,7 @@ pub fn process_inputs(
|
||||
);
|
||||
let mut sent_data = HeightToAddressTypeToVec::with_capacity(estimated_unique_heights);
|
||||
let mut address_data =
|
||||
AddressTypeToTypeIndexMap::<LoadedAddressDataWithSource>::with_capacity(estimated_per_type);
|
||||
AddressTypeToTypeIndexMap::<FundedAddressDataWithSource>::with_capacity(estimated_per_type);
|
||||
let mut txindex_vecs =
|
||||
AddressTypeToTypeIndexMap::<TxIndexVec>::with_capacity(estimated_per_type);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::distribution::{
|
||||
|
||||
use super::super::{
|
||||
cache::{AddressCache, load_uncached_address_data},
|
||||
cohort::{LoadedAddressDataWithSource, TxIndexVec},
|
||||
cohort::{FundedAddressDataWithSource, TxIndexVec},
|
||||
};
|
||||
|
||||
/// Result of processing outputs for a block.
|
||||
@@ -21,7 +21,7 @@ pub struct OutputsResult {
|
||||
/// Per-address-type received data: (typeindex, value) for each address.
|
||||
pub received_data: AddressTypeToVec<(TypeIndex, Sats)>,
|
||||
/// Address data looked up during processing, keyed by (address_type, typeindex).
|
||||
pub address_data: AddressTypeToTypeIndexMap<LoadedAddressDataWithSource>,
|
||||
pub address_data: AddressTypeToTypeIndexMap<FundedAddressDataWithSource>,
|
||||
/// Transaction indexes per address for tx_count tracking.
|
||||
pub txindex_vecs: AddressTypeToTypeIndexMap<TxIndexVec>,
|
||||
}
|
||||
@@ -50,7 +50,7 @@ pub fn process_outputs(
|
||||
let mut transacted = Transacted::default();
|
||||
let mut received_data = AddressTypeToVec::with_capacity(estimated_per_type);
|
||||
let mut address_data =
|
||||
AddressTypeToTypeIndexMap::<LoadedAddressDataWithSource>::with_capacity(estimated_per_type);
|
||||
AddressTypeToTypeIndexMap::<FundedAddressDataWithSource>::with_capacity(estimated_per_type);
|
||||
let mut txindex_vecs =
|
||||
AddressTypeToTypeIndexMap::<TxIndexVec>::with_capacity(estimated_per_type);
|
||||
|
||||
|
||||
@@ -56,52 +56,43 @@ impl AddressCohorts {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Apply a function to each aggregate cohort with its source cohorts (in parallel).
|
||||
fn for_each_aggregate<F>(&mut self, f: F) -> Result<()>
|
||||
where
|
||||
F: Fn(&mut AddressCohortVecs, Vec<&AddressCohortVecs>) -> Result<()> + Sync,
|
||||
{
|
||||
let by_amount_range = &self.0.amount_range;
|
||||
|
||||
let pairs: Vec<_> = self
|
||||
.0
|
||||
.ge_amount
|
||||
.iter_mut()
|
||||
.chain(self.0.lt_amount.iter_mut())
|
||||
.map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_amount_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
pairs
|
||||
.into_par_iter()
|
||||
.try_for_each(|(vecs, sources)| f(vecs, sources))
|
||||
}
|
||||
|
||||
/// Compute overlapping cohorts from component amount_range cohorts.
|
||||
///
|
||||
/// For example, ">=1 BTC" cohort is computed from sum of amount_range cohorts that match.
|
||||
pub fn compute_overlapping_vecs(
|
||||
&mut self,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let by_amount_range = &self.0.amount_range;
|
||||
|
||||
// ge_amount cohorts computed from matching amount_range cohorts
|
||||
[
|
||||
self.0
|
||||
.ge_amount
|
||||
.par_iter_mut()
|
||||
.map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_amount_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
// lt_amount cohorts computed from matching amount_range cohorts
|
||||
self.0
|
||||
.lt_amount
|
||||
.par_iter_mut()
|
||||
.map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_amount_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.try_for_each(|(vecs, components)| {
|
||||
vecs.compute_from_stateful(starting_indexes, &components, exit)
|
||||
self.for_each_aggregate(|vecs, sources| {
|
||||
vecs.compute_from_stateful(starting_indexes, &sources, exit)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -113,8 +104,24 @@ impl AddressCohorts {
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
// 1. Compute all metrics except net_sentiment
|
||||
self.par_iter_mut()
|
||||
.try_for_each(|v| v.compute_rest_part1(indexes, price, starting_indexes, exit))
|
||||
.try_for_each(|v| v.compute_rest_part1(indexes, price, starting_indexes, exit))?;
|
||||
|
||||
// 2. Compute net_sentiment.height for separate cohorts (greed - pain)
|
||||
self.par_iter_separate_mut()
|
||||
.try_for_each(|v| v.metrics.compute_net_sentiment_height(starting_indexes, exit))?;
|
||||
|
||||
// 3. Compute net_sentiment.height for aggregate cohorts (weighted average)
|
||||
self.for_each_aggregate(|vecs, sources| {
|
||||
let metrics: Vec<_> = sources.iter().map(|v| &v.metrics).collect();
|
||||
vecs.metrics
|
||||
.compute_net_sentiment_from_others(starting_indexes, &metrics, exit)
|
||||
})?;
|
||||
|
||||
// 4. Compute net_sentiment dateindex for ALL cohorts
|
||||
self.par_iter_mut()
|
||||
.try_for_each(|v| v.metrics.compute_net_sentiment_rest(indexes, starting_indexes, exit))
|
||||
}
|
||||
|
||||
/// Second phase of post-processing: compute relative metrics.
|
||||
@@ -191,11 +198,11 @@ impl AddressCohorts {
|
||||
});
|
||||
}
|
||||
|
||||
/// Reset price_to_amount for all separate cohorts (called during fresh start).
|
||||
pub fn reset_separate_price_to_amount(&mut self) -> Result<()> {
|
||||
/// Reset cost_basis_data for all separate cohorts (called during fresh start).
|
||||
pub fn reset_separate_cost_basis_data(&mut self) -> Result<()> {
|
||||
self.par_iter_separate_mut().try_for_each(|v| {
|
||||
if let Some(state) = v.state.as_mut() {
|
||||
state.reset_price_to_amount_if_needed()?;
|
||||
state.reset_cost_basis_data_if_needed()?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
|
||||
@@ -3,13 +3,15 @@ use std::path::Path;
|
||||
use brk_cohort::{CohortContext, Filter, Filtered};
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{DateIndex, Dollars, Height, StoredU64, Version};
|
||||
use brk_types::{CentsUnsigned, DateIndex, Dollars, Height, StoredF64, StoredU64, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, GenericStoredVec, IterableVec};
|
||||
|
||||
use crate::{
|
||||
ComputeIndexes, distribution::state::AddressCohortState, indexes, internal::ComputedFromHeightLast,
|
||||
price,
|
||||
distribution::state::AddressCohortState,
|
||||
indexes,
|
||||
internal::{ComputedFromDateLast, ComputedFromHeightLast},
|
||||
price, ComputeIndexes,
|
||||
};
|
||||
|
||||
use crate::distribution::metrics::{CohortMetrics, ImportConfig, SupplyMetrics};
|
||||
@@ -33,6 +35,7 @@ pub struct AddressCohortVecs {
|
||||
pub metrics: CohortMetrics,
|
||||
|
||||
pub addr_count: ComputedFromHeightLast<StoredU64>,
|
||||
pub addr_count_30d_change: ComputedFromDateLast<StoredF64>,
|
||||
}
|
||||
|
||||
impl AddressCohortVecs {
|
||||
@@ -79,6 +82,12 @@ impl AddressCohortVecs {
|
||||
version + VERSION,
|
||||
indexes,
|
||||
)?,
|
||||
addr_count_30d_change: ComputedFromDateLast::forced_import(
|
||||
db,
|
||||
&cfg.name("addr_count_30d_change"),
|
||||
version + VERSION,
|
||||
indexes,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -145,7 +154,7 @@ impl DynCohortVecs for AddressCohortVecs {
|
||||
// State files are saved AT height H, so to resume at H+1 we need to import at H
|
||||
// Decrement first, then increment result to match expected starting_height
|
||||
if let Some(mut prev_height) = starting_height.decremented() {
|
||||
// Import price_to_amount state file (may adjust prev_height to actual file found)
|
||||
// Import cost_basis_data state file (may adjust prev_height to actual file found)
|
||||
prev_height = state.inner.import_at_or_before(prev_height)?;
|
||||
|
||||
// Restore supply state from height-indexed vectors
|
||||
@@ -164,15 +173,8 @@ impl DynCohortVecs for AddressCohortVecs {
|
||||
.read_once(prev_height)?;
|
||||
state.addr_count = *self.addr_count.height.read_once(prev_height)?;
|
||||
|
||||
// Restore realized cap if present
|
||||
if let Some(realized_metrics) = self.metrics.realized.as_mut()
|
||||
&& let Some(realized_state) = state.inner.realized.as_mut()
|
||||
{
|
||||
realized_state.cap = realized_metrics
|
||||
.realized_cap
|
||||
.height
|
||||
.read_once(prev_height)?;
|
||||
}
|
||||
// Restore realized cap from persisted exact values
|
||||
state.inner.restore_realized_cap();
|
||||
|
||||
let result = prev_height.incremented();
|
||||
self.starting_height = Some(result);
|
||||
@@ -216,9 +218,9 @@ impl DynCohortVecs for AddressCohortVecs {
|
||||
fn compute_then_truncate_push_unrealized_states(
|
||||
&mut self,
|
||||
height: Height,
|
||||
height_price: Option<Dollars>,
|
||||
height_price: Option<CentsUnsigned>,
|
||||
dateindex: Option<DateIndex>,
|
||||
date_price: Option<Option<Dollars>>,
|
||||
date_price: Option<Option<CentsUnsigned>>,
|
||||
) -> Result<()> {
|
||||
if let Some(state) = self.state.as_mut() {
|
||||
self.metrics.compute_then_truncate_push_unrealized_states(
|
||||
@@ -241,6 +243,18 @@ impl DynCohortVecs for AddressCohortVecs {
|
||||
) -> Result<()> {
|
||||
self.addr_count
|
||||
.compute_rest(indexes, starting_indexes, exit)?;
|
||||
|
||||
self.addr_count_30d_change
|
||||
.compute_all(starting_indexes, exit, |v| {
|
||||
v.compute_change(
|
||||
starting_indexes.dateindex,
|
||||
&*self.addr_count.dateindex,
|
||||
30,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
self.metrics
|
||||
.compute_rest_part1(indexes, price, starting_indexes, exit)?;
|
||||
Ok(())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::{DateIndex, Dollars, Height, Version};
|
||||
use brk_types::{CentsUnsigned, DateIndex, Dollars, Height, Version};
|
||||
use vecdb::{Exit, IterableVec};
|
||||
|
||||
use crate::{ComputeIndexes, indexes, price};
|
||||
@@ -30,9 +30,9 @@ pub trait DynCohortVecs: Send + Sync {
|
||||
fn compute_then_truncate_push_unrealized_states(
|
||||
&mut self,
|
||||
height: Height,
|
||||
height_price: Option<Dollars>,
|
||||
height_price: Option<CentsUnsigned>,
|
||||
dateindex: Option<DateIndex>,
|
||||
date_price: Option<Option<Dollars>>,
|
||||
date_price: Option<Option<CentsUnsigned>>,
|
||||
) -> Result<()>;
|
||||
|
||||
/// First phase of post-processing computations.
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
use std::path::Path;
|
||||
use std::{cmp::Reverse, collections::BinaryHeap, path::Path};
|
||||
|
||||
use brk_cohort::{
|
||||
ByAgeRange, ByAmountRange, ByEpoch, ByGreatEqualAmount, ByLowerThanAmount, ByMaxAge, ByMinAge,
|
||||
BySpendableType, ByTerm, ByYear, Filter, Filtered, StateLevel, UTXOGroups,
|
||||
AGE_BOUNDARIES, ByAgeRange, ByAmountRange, ByEpoch, ByGreatEqualAmount, ByLowerThanAmount,
|
||||
ByMaxAge, ByMinAge, BySpendableType, ByTerm, ByYear, Filter, Filtered, StateLevel, UTXOGroups,
|
||||
};
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{DateIndex, Dollars, Height, Sats, Version};
|
||||
use brk_types::{
|
||||
CentsUnsigned, DateIndex, Dollars, Height, ONE_HOUR_IN_SEC, Sats, StoredF32, Timestamp, Version,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, Database, Exit, IterableVec};
|
||||
use vecdb::{AnyStoredVec, Database, Exit, GenericStoredVec, IterableVec, VecIndex};
|
||||
|
||||
use crate::{
|
||||
ComputeIndexes,
|
||||
distribution::DynCohortVecs,
|
||||
distribution::{DynCohortVecs, compute::PriceRangeMax, state::BlockState},
|
||||
indexes,
|
||||
internal::{PERCENTILES, PERCENTILES_LEN},
|
||||
internal::{PERCENTILES, PERCENTILES_LEN, compute_spot_percentile_rank},
|
||||
price,
|
||||
};
|
||||
|
||||
@@ -150,70 +152,83 @@ impl UTXOCohorts {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Apply a function to each aggregate cohort with its source cohorts (in parallel).
|
||||
fn for_each_aggregate<F>(&mut self, f: F) -> Result<()>
|
||||
where
|
||||
F: Fn(&mut UTXOCohortVecs, Vec<&UTXOCohortVecs>) -> Result<()> + Sync,
|
||||
{
|
||||
let by_age_range = &self.0.age_range;
|
||||
let by_amount_range = &self.0.amount_range;
|
||||
|
||||
// Build (aggregate, sources) pairs
|
||||
let pairs: Vec<_> = [(&mut self.0.all, by_age_range.iter().collect::<Vec<_>>())]
|
||||
.into_iter()
|
||||
.chain(self.0.min_age.iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_age_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect(),
|
||||
)
|
||||
}))
|
||||
.chain(self.0.max_age.iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_age_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect(),
|
||||
)
|
||||
}))
|
||||
.chain(self.0.term.iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_age_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect(),
|
||||
)
|
||||
}))
|
||||
.chain(self.0.ge_amount.iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_amount_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect(),
|
||||
)
|
||||
}))
|
||||
.chain(self.0.lt_amount.iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_amount_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect(),
|
||||
)
|
||||
}))
|
||||
.collect();
|
||||
|
||||
pairs
|
||||
.into_par_iter()
|
||||
.try_for_each(|(vecs, sources)| f(vecs, sources))
|
||||
}
|
||||
|
||||
/// Compute overlapping cohorts from component age/amount range cohorts.
|
||||
pub fn compute_overlapping_vecs(
|
||||
&mut self,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let by_age_range = &self.0.age_range;
|
||||
let by_amount_range = &self.0.amount_range;
|
||||
|
||||
[(&mut self.0.all, by_age_range.iter().collect::<Vec<_>>())]
|
||||
.into_par_iter()
|
||||
.chain(self.0.min_age.par_iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_age_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}))
|
||||
.chain(self.0.max_age.par_iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_age_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}))
|
||||
.chain(self.0.term.par_iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_age_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}))
|
||||
.chain(self.0.ge_amount.par_iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_amount_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}))
|
||||
.chain(self.0.lt_amount.par_iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_amount_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}))
|
||||
.try_for_each(|(vecs, components)| {
|
||||
vecs.compute_from_stateful(starting_indexes, &components, exit)
|
||||
})
|
||||
self.for_each_aggregate(|vecs, sources| {
|
||||
vecs.compute_from_stateful(starting_indexes, &sources, exit)
|
||||
})
|
||||
}
|
||||
|
||||
/// First phase of post-processing: compute index transforms.
|
||||
@@ -224,8 +239,24 @@ impl UTXOCohorts {
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
// 1. Compute all metrics except net_sentiment
|
||||
self.par_iter_mut()
|
||||
.try_for_each(|v| v.compute_rest_part1(indexes, price, starting_indexes, exit))
|
||||
.try_for_each(|v| v.compute_rest_part1(indexes, price, starting_indexes, exit))?;
|
||||
|
||||
// 2. Compute net_sentiment.height for separate cohorts (greed - pain)
|
||||
self.par_iter_separate_mut()
|
||||
.try_for_each(|v| v.metrics.compute_net_sentiment_height(starting_indexes, exit))?;
|
||||
|
||||
// 3. Compute net_sentiment.height for aggregate cohorts (weighted average)
|
||||
self.for_each_aggregate(|vecs, sources| {
|
||||
let metrics: Vec<_> = sources.iter().map(|v| &v.metrics).collect();
|
||||
vecs.metrics
|
||||
.compute_net_sentiment_from_others(starting_indexes, &metrics, exit)
|
||||
})?;
|
||||
|
||||
// 4. Compute net_sentiment dateindex for ALL cohorts
|
||||
self.par_iter_mut()
|
||||
.try_for_each(|v| v.metrics.compute_net_sentiment_rest(indexes, starting_indexes, exit))
|
||||
}
|
||||
|
||||
/// Second phase of post-processing: compute relative metrics.
|
||||
@@ -288,13 +319,12 @@ impl UTXOCohorts {
|
||||
}
|
||||
|
||||
/// Get minimum dateindex from all aggregate cohorts' dateindex-indexed vectors.
|
||||
/// This checks cost_basis percentiles which are only on aggregate cohorts.
|
||||
/// This checks cost_basis metrics which are only on aggregate cohorts.
|
||||
pub fn min_aggregate_stateful_dateindex_len(&self) -> usize {
|
||||
self.0
|
||||
.iter_aggregate()
|
||||
.filter_map(|v| v.metrics.cost_basis.as_ref())
|
||||
.filter_map(|cb| cb.percentiles.as_ref())
|
||||
.map(|cbp| cbp.min_stateful_dateindex_len())
|
||||
.map(|cb| cb.min_stateful_dateindex_len())
|
||||
.min()
|
||||
.unwrap_or(usize::MAX)
|
||||
}
|
||||
@@ -314,108 +344,157 @@ impl UTXOCohorts {
|
||||
});
|
||||
}
|
||||
|
||||
/// Reset price_to_amount for all separate cohorts (called during fresh start).
|
||||
pub fn reset_separate_price_to_amount(&mut self) -> Result<()> {
|
||||
/// Reset cost_basis_data for all separate cohorts (called during fresh start).
|
||||
pub fn reset_separate_cost_basis_data(&mut self) -> Result<()> {
|
||||
self.par_iter_separate_mut().try_for_each(|v| {
|
||||
if let Some(state) = v.state.as_mut() {
|
||||
state.reset_price_to_amount_if_needed()?;
|
||||
state.reset_cost_basis_data_if_needed()?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Compute and push percentiles for aggregate cohorts (all, sth, lth).
|
||||
/// Computes on-demand by merging age_range cohorts' price_to_amount data.
|
||||
/// This avoids maintaining redundant aggregate price_to_amount maps.
|
||||
pub fn truncate_push_aggregate_percentiles(&mut self, dateindex: DateIndex) -> Result<()> {
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::BinaryHeap;
|
||||
|
||||
// Collect (filter, supply, price_to_amount as Vec) from age_range cohorts
|
||||
/// Computes on-demand by merging age_range cohorts' cost_basis_data data.
|
||||
/// This avoids maintaining redundant aggregate cost_basis_data maps.
|
||||
/// Computes both sat-weighted (percentiles) and USD-weighted (invested_capital) percentiles.
|
||||
pub fn truncate_push_aggregate_percentiles(
|
||||
&mut self,
|
||||
dateindex: DateIndex,
|
||||
spot: Dollars,
|
||||
) -> Result<()> {
|
||||
// Collect (filter, entries, total_sats, total_usd) from age_range cohorts.
|
||||
// Keep data in CentsUnsigned to avoid float conversions until output.
|
||||
// Compute totals during collection to avoid a second pass.
|
||||
let age_range_data: Vec<_> = self
|
||||
.0
|
||||
.age_range
|
||||
.iter()
|
||||
.filter_map(|sub| {
|
||||
let state = sub.state.as_ref()?;
|
||||
let entries: Vec<(Dollars, Sats)> = state
|
||||
.price_to_amount_iter()?
|
||||
.map(|(p, &a)| (p, a))
|
||||
let mut total_sats: u64 = 0;
|
||||
let mut total_usd: u128 = 0;
|
||||
let entries: Vec<(CentsUnsigned, Sats)> = state
|
||||
.cost_basis_data_iter()?
|
||||
.map(|(price, &sats)| {
|
||||
let sats_u64 = u64::from(sats);
|
||||
let price_u128 = price.as_u128();
|
||||
total_sats += sats_u64;
|
||||
total_usd += price_u128 * sats_u64 as u128;
|
||||
(price, sats)
|
||||
})
|
||||
.collect();
|
||||
Some((sub.filter().clone(), state.supply.value, entries))
|
||||
Some((sub.filter().clone(), entries, total_sats, total_usd))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Compute percentiles for each aggregate filter
|
||||
for aggregate in self.0.iter_aggregate_mut() {
|
||||
// Compute percentiles for each aggregate filter in parallel
|
||||
self.0.par_iter_aggregate_mut().try_for_each(|aggregate| {
|
||||
let filter = aggregate.filter().clone();
|
||||
|
||||
// Get cost_basis percentiles storage, skip if not configured
|
||||
let Some(percentiles) = aggregate
|
||||
.metrics
|
||||
.cost_basis
|
||||
.as_mut()
|
||||
.and_then(|cb| cb.percentiles.as_mut())
|
||||
else {
|
||||
continue;
|
||||
// Get cost_basis, skip if not configured
|
||||
let Some(cost_basis) = aggregate.metrics.cost_basis.as_mut() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Collect relevant cohort data for this aggregate
|
||||
// Collect relevant cohort data for this aggregate and sum totals
|
||||
let mut total_sats: u64 = 0;
|
||||
let mut total_usd: u128 = 0;
|
||||
let relevant: Vec<_> = age_range_data
|
||||
.iter()
|
||||
.filter(|(sub_filter, _, _)| filter.includes(sub_filter))
|
||||
.filter(|(sub_filter, _, _, _)| filter.includes(sub_filter))
|
||||
.map(|(_, entries, cohort_sats, cohort_usd)| {
|
||||
total_sats += cohort_sats;
|
||||
total_usd += cohort_usd;
|
||||
entries
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Calculate total supply
|
||||
let total_supply: u64 = relevant.iter().map(|(_, s, _)| u64::from(*s)).sum();
|
||||
|
||||
if total_supply == 0 {
|
||||
percentiles.truncate_push(dateindex, &[Dollars::NAN; PERCENTILES_LEN])?;
|
||||
continue;
|
||||
if total_sats == 0 {
|
||||
let nan_prices = [Dollars::NAN; PERCENTILES_LEN];
|
||||
if let Some(percentiles) = cost_basis.percentiles.as_mut() {
|
||||
percentiles.truncate_push(dateindex, &nan_prices)?;
|
||||
}
|
||||
if let Some(invested_capital) = cost_basis.invested_capital.as_mut() {
|
||||
invested_capital.truncate_push(dateindex, &nan_prices)?;
|
||||
}
|
||||
if let Some(spot_pct) = cost_basis.spot_cost_basis_percentile.as_mut() {
|
||||
spot_pct
|
||||
.dateindex
|
||||
.truncate_push(dateindex, StoredF32::NAN)?;
|
||||
}
|
||||
if let Some(spot_pct) = cost_basis.spot_invested_capital_percentile.as_mut() {
|
||||
spot_pct
|
||||
.dateindex
|
||||
.truncate_push(dateindex, StoredF32::NAN)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// K-way merge using min-heap: O(n log k) where k = number of cohorts
|
||||
// Each heap entry: (price, amount, cohort_idx, entry_idx)
|
||||
let mut heap: BinaryHeap<Reverse<(Dollars, usize, usize)>> = BinaryHeap::new();
|
||||
let mut heap: BinaryHeap<Reverse<(CentsUnsigned, usize, usize)>> = BinaryHeap::new();
|
||||
|
||||
// Initialize heap with first entry from each cohort
|
||||
for (cohort_idx, (_, _, entries)) in relevant.iter().enumerate() {
|
||||
for (cohort_idx, entries) in relevant.iter().enumerate() {
|
||||
if !entries.is_empty() {
|
||||
heap.push(Reverse((entries[0].0, cohort_idx, 0)));
|
||||
}
|
||||
}
|
||||
|
||||
let targets = PERCENTILES.map(|p| total_supply * u64::from(p) / 100);
|
||||
let mut result = [Dollars::NAN; PERCENTILES_LEN];
|
||||
let mut accumulated = 0u64;
|
||||
let mut pct_idx = 0;
|
||||
let mut current_price: Option<Dollars> = None;
|
||||
let mut amount_at_price = 0u64;
|
||||
// Compute both sat-weighted and USD-weighted percentiles in one pass
|
||||
let sat_targets = PERCENTILES.map(|p| total_sats * u64::from(p) / 100);
|
||||
let usd_targets = PERCENTILES.map(|p| total_usd * u128::from(p) / 100);
|
||||
|
||||
let mut sat_result = [Dollars::NAN; PERCENTILES_LEN];
|
||||
let mut usd_result = [Dollars::NAN; PERCENTILES_LEN];
|
||||
|
||||
let mut cumsum_sats: u64 = 0;
|
||||
let mut cumsum_usd: u128 = 0;
|
||||
let mut sat_idx = 0;
|
||||
let mut usd_idx = 0;
|
||||
|
||||
let mut current_price: Option<CentsUnsigned> = None;
|
||||
let mut sats_at_price: u64 = 0;
|
||||
let mut usd_at_price: u128 = 0;
|
||||
|
||||
while let Some(Reverse((price, cohort_idx, entry_idx))) = heap.pop() {
|
||||
let (_, _, entries) = relevant[cohort_idx];
|
||||
let entries = relevant[cohort_idx];
|
||||
let (_, amount) = entries[entry_idx];
|
||||
let amount_u64 = u64::from(amount);
|
||||
let price_u128 = price.as_u128();
|
||||
|
||||
// If price changed, finalize previous price
|
||||
if let Some(current_price) = current_price
|
||||
&& current_price != price
|
||||
if let Some(prev_price) = current_price
|
||||
&& prev_price != price
|
||||
{
|
||||
accumulated += amount_at_price;
|
||||
cumsum_sats += sats_at_price;
|
||||
cumsum_usd += usd_at_price;
|
||||
|
||||
while pct_idx < PERCENTILES_LEN && accumulated >= targets[pct_idx] {
|
||||
result[pct_idx] = current_price;
|
||||
pct_idx += 1;
|
||||
// Only convert to dollars if we still need percentiles
|
||||
if sat_idx < PERCENTILES_LEN || usd_idx < PERCENTILES_LEN {
|
||||
let prev_dollars = prev_price.to_dollars();
|
||||
while sat_idx < PERCENTILES_LEN && cumsum_sats >= sat_targets[sat_idx] {
|
||||
sat_result[sat_idx] = prev_dollars;
|
||||
sat_idx += 1;
|
||||
}
|
||||
while usd_idx < PERCENTILES_LEN && cumsum_usd >= usd_targets[usd_idx] {
|
||||
usd_result[usd_idx] = prev_dollars;
|
||||
usd_idx += 1;
|
||||
}
|
||||
|
||||
// Early exit if all percentiles found
|
||||
if sat_idx >= PERCENTILES_LEN && usd_idx >= PERCENTILES_LEN {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if pct_idx >= PERCENTILES_LEN {
|
||||
break;
|
||||
}
|
||||
|
||||
amount_at_price = 0;
|
||||
sats_at_price = 0;
|
||||
usd_at_price = 0;
|
||||
}
|
||||
|
||||
current_price = Some(price);
|
||||
amount_at_price += u64::from(amount);
|
||||
sats_at_price += amount_u64;
|
||||
usd_at_price += price_u128 * amount_u64 as u128;
|
||||
|
||||
// Push next entry from this cohort
|
||||
let next_idx = entry_idx + 1;
|
||||
@@ -424,19 +503,44 @@ impl UTXOCohorts {
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize last price
|
||||
if let Some(price) = current_price {
|
||||
accumulated += amount_at_price;
|
||||
while pct_idx < PERCENTILES_LEN && accumulated >= targets[pct_idx] {
|
||||
result[pct_idx] = price;
|
||||
pct_idx += 1;
|
||||
// Finalize last price (skip if we already found all percentiles via early exit)
|
||||
if (sat_idx < PERCENTILES_LEN || usd_idx < PERCENTILES_LEN)
|
||||
&& let Some(price) = current_price
|
||||
{
|
||||
cumsum_sats += sats_at_price;
|
||||
cumsum_usd += usd_at_price;
|
||||
|
||||
let price_dollars = price.to_dollars();
|
||||
while sat_idx < PERCENTILES_LEN && cumsum_sats >= sat_targets[sat_idx] {
|
||||
sat_result[sat_idx] = price_dollars;
|
||||
sat_idx += 1;
|
||||
}
|
||||
while usd_idx < PERCENTILES_LEN && cumsum_usd >= usd_targets[usd_idx] {
|
||||
usd_result[usd_idx] = price_dollars;
|
||||
usd_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
percentiles.truncate_push(dateindex, &result)?;
|
||||
}
|
||||
// Push both sat-weighted and USD-weighted results
|
||||
if let Some(percentiles) = cost_basis.percentiles.as_mut() {
|
||||
percentiles.truncate_push(dateindex, &sat_result)?;
|
||||
}
|
||||
if let Some(invested_capital) = cost_basis.invested_capital.as_mut() {
|
||||
invested_capital.truncate_push(dateindex, &usd_result)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
// Compute and push spot percentile ranks
|
||||
if let Some(spot_pct) = cost_basis.spot_cost_basis_percentile.as_mut() {
|
||||
let rank = compute_spot_percentile_rank(&sat_result, spot);
|
||||
spot_pct.dateindex.truncate_push(dateindex, rank)?;
|
||||
}
|
||||
if let Some(spot_pct) = cost_basis.spot_invested_capital_percentile.as_mut() {
|
||||
let rank = compute_spot_percentile_rank(&usd_result, spot);
|
||||
spot_pct.dateindex.truncate_push(dateindex, rank)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Validate computed versions for all cohorts (separate and aggregate).
|
||||
@@ -452,4 +556,112 @@ impl UTXOCohorts {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute and push peak regret for all age_range cohorts.
|
||||
///
|
||||
/// Uses split points to efficiently compute regret per cohort.
|
||||
/// All 21 cohorts are computed in parallel, then pushed sequentially.
|
||||
/// Called once per day when dateindex changes.
|
||||
pub fn compute_and_push_peak_regret(
|
||||
&mut self,
|
||||
chain_state: &[BlockState],
|
||||
current_height: Height,
|
||||
current_timestamp: Timestamp,
|
||||
spot: CentsUnsigned,
|
||||
price_range_max: &PriceRangeMax,
|
||||
dateindex: DateIndex,
|
||||
) -> Result<()> {
|
||||
const FIRST_PRICE_HEIGHT: usize = 68_195;
|
||||
|
||||
let start_height = FIRST_PRICE_HEIGHT;
|
||||
let end_height = current_height.to_usize() + 1;
|
||||
|
||||
// Early return: push zeros if no price data yet
|
||||
if end_height <= start_height {
|
||||
for cohort in self.0.age_range.iter_mut() {
|
||||
if let Some(unrealized) = cohort.metrics.unrealized.as_mut()
|
||||
&& let Some(peak_regret) = unrealized.peak_regret.as_mut()
|
||||
{
|
||||
peak_regret
|
||||
.dateindex
|
||||
.truncate_push(dateindex, Dollars::ZERO)?;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let spot_u128 = spot.as_u128();
|
||||
let current_ts = *current_timestamp;
|
||||
|
||||
// Compute split points: splits[k] = first index where age < AGE_BOUNDARIES[k]
|
||||
let splits: [usize; 20] = std::array::from_fn(|k| {
|
||||
let boundary_seconds = (AGE_BOUNDARIES[k] as u32) * ONE_HOUR_IN_SEC;
|
||||
let threshold_ts = current_ts.saturating_sub(boundary_seconds);
|
||||
chain_state[..end_height].partition_point(|b| *b.timestamp <= threshold_ts)
|
||||
});
|
||||
|
||||
// Build ranges for all 21 cohorts
|
||||
let ranges: [(usize, usize); 21] = std::array::from_fn(|i| {
|
||||
if i == 0 {
|
||||
(splits[0], end_height)
|
||||
} else if i < 20 {
|
||||
(splits[i], splits[i - 1])
|
||||
} else {
|
||||
(start_height, splits[19])
|
||||
}
|
||||
});
|
||||
|
||||
// Compute regret for all cohorts in parallel
|
||||
let regrets: [Dollars; 21] = ranges
|
||||
.into_par_iter()
|
||||
.map(|(range_start, range_end)| {
|
||||
let effective_start = range_start.max(start_height);
|
||||
if effective_start >= range_end {
|
||||
return Dollars::ZERO;
|
||||
}
|
||||
|
||||
let mut regret: u128 = 0;
|
||||
for h in effective_start..range_end {
|
||||
let block = &chain_state[h];
|
||||
let supply = block.supply.value;
|
||||
|
||||
if supply.is_zero() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cost_basis = match block.price {
|
||||
Some(p) => p,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let receive_height = Height::from(h);
|
||||
let peak = price_range_max.max_between(receive_height, current_height);
|
||||
let peak_u128 = peak.as_u128();
|
||||
let cost_u128 = cost_basis.as_u128();
|
||||
let supply_u128 = supply.as_u128();
|
||||
|
||||
regret += if spot_u128 >= cost_u128 {
|
||||
(peak_u128 - spot_u128) * supply_u128
|
||||
} else {
|
||||
(peak_u128 - cost_u128) * supply_u128
|
||||
};
|
||||
}
|
||||
|
||||
CentsUnsigned::new((regret / Sats::ONE_BTC_U128) as u64).to_dollars()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
|
||||
// Push results to cohorts
|
||||
for (cohort, regret) in self.0.age_range.iter_mut().zip(regrets) {
|
||||
if let Some(unrealized) = cohort.metrics.unrealized.as_mut()
|
||||
&& let Some(peak_regret) = unrealized.peak_regret.as_mut()
|
||||
{
|
||||
peak_regret.dateindex.truncate_push(dateindex, regret)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use brk_types::{Dollars, Height, Timestamp};
|
||||
use brk_types::{CentsUnsigned, Height, Timestamp};
|
||||
|
||||
use crate::distribution::state::Transacted;
|
||||
|
||||
@@ -18,7 +18,7 @@ impl UTXOCohorts {
|
||||
received: Transacted,
|
||||
height: Height,
|
||||
timestamp: Timestamp,
|
||||
price: Option<Dollars>,
|
||||
price: Option<CentsUnsigned>,
|
||||
) {
|
||||
let supply_state = received.spendable_supply;
|
||||
|
||||
@@ -30,7 +30,7 @@ impl UTXOCohorts {
|
||||
]
|
||||
.into_iter()
|
||||
.for_each(|v| {
|
||||
v.state.as_mut().unwrap().receive(&supply_state, price);
|
||||
v.state.as_mut().unwrap().receive_utxo(&supply_state, price);
|
||||
});
|
||||
|
||||
// Update output type cohorts
|
||||
@@ -40,7 +40,7 @@ impl UTXOCohorts {
|
||||
vecs.state
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.receive(received.by_type.get(output_type), price)
|
||||
.receive_utxo(received.by_type.get(output_type), price)
|
||||
});
|
||||
|
||||
// Update amount range cohorts
|
||||
@@ -53,7 +53,7 @@ impl UTXOCohorts {
|
||||
.state
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.receive(supply_state, price);
|
||||
.receive_utxo(supply_state, price);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use brk_types::{Age, Height};
|
||||
use brk_types::{Age, CentsUnsigned, Height};
|
||||
use rustc_hash::FxHashMap;
|
||||
use vecdb::VecIndex;
|
||||
|
||||
use crate::{
|
||||
distribution::state::{BlockState, Transacted},
|
||||
distribution::{
|
||||
compute::PriceRangeMax,
|
||||
state::{BlockState, Transacted},
|
||||
},
|
||||
utils::OptionExt,
|
||||
};
|
||||
|
||||
@@ -14,10 +17,14 @@ impl UTXOCohorts {
|
||||
///
|
||||
/// Each input references a UTXO created at some previous height.
|
||||
/// We need to update the cohort states based on when that UTXO was created.
|
||||
///
|
||||
/// `price_range_max` is used to compute the peak price during each UTXO's holding period
|
||||
/// for accurate peak regret calculation.
|
||||
pub fn send(
|
||||
&mut self,
|
||||
height_to_sent: FxHashMap<Height, Transacted>,
|
||||
chain_state: &mut [BlockState],
|
||||
price_range_max: Option<&PriceRangeMax>,
|
||||
) {
|
||||
if chain_state.is_empty() {
|
||||
return;
|
||||
@@ -27,31 +34,44 @@ impl UTXOCohorts {
|
||||
let last_timestamp = last_block.timestamp;
|
||||
let current_price = last_block.price;
|
||||
let chain_len = chain_state.len();
|
||||
let send_height = Height::from(chain_len - 1);
|
||||
|
||||
for (height, sent) in height_to_sent {
|
||||
for (receive_height, sent) in height_to_sent {
|
||||
// Update chain_state to reflect spent supply
|
||||
chain_state[height.to_usize()].supply -= &sent.spendable_supply;
|
||||
chain_state[receive_height.to_usize()].supply -= &sent.spendable_supply;
|
||||
|
||||
let block_state = &chain_state[height.to_usize()];
|
||||
let block_state = &chain_state[receive_height.to_usize()];
|
||||
let prev_price = block_state.price;
|
||||
let blocks_old = chain_len - 1 - height.to_usize();
|
||||
let blocks_old = chain_len - 1 - receive_height.to_usize();
|
||||
let age = Age::new(last_timestamp, block_state.timestamp, blocks_old);
|
||||
|
||||
// Compute peak price during holding period for peak regret
|
||||
// This is the max HIGH price between receive and send heights
|
||||
let peak_price: Option<CentsUnsigned> =
|
||||
price_range_max.map(|t| t.max_between(receive_height, send_height));
|
||||
|
||||
// Update age range cohort (direct index lookup)
|
||||
self.0.age_range.get_mut(age).state.um().send(
|
||||
self.0.age_range.get_mut(age).state.um().send_utxo(
|
||||
&sent.spendable_supply,
|
||||
current_price,
|
||||
prev_price,
|
||||
peak_price,
|
||||
age,
|
||||
);
|
||||
|
||||
// Update epoch cohort (direct lookup by height)
|
||||
self.0.epoch.mut_vec_from_height(height).state.um().send(
|
||||
&sent.spendable_supply,
|
||||
current_price,
|
||||
prev_price,
|
||||
age,
|
||||
);
|
||||
self.0
|
||||
.epoch
|
||||
.mut_vec_from_height(receive_height)
|
||||
.state
|
||||
.um()
|
||||
.send_utxo(
|
||||
&sent.spendable_supply,
|
||||
current_price,
|
||||
prev_price,
|
||||
peak_price,
|
||||
age,
|
||||
);
|
||||
|
||||
// Update year cohort (direct lookup by timestamp)
|
||||
self.0
|
||||
@@ -59,7 +79,13 @@ impl UTXOCohorts {
|
||||
.mut_vec_from_timestamp(block_state.timestamp)
|
||||
.state
|
||||
.um()
|
||||
.send(&sent.spendable_supply, current_price, prev_price, age);
|
||||
.send_utxo(
|
||||
&sent.spendable_supply,
|
||||
current_price,
|
||||
prev_price,
|
||||
peak_price,
|
||||
age,
|
||||
);
|
||||
|
||||
// Update output type cohorts
|
||||
sent.by_type
|
||||
@@ -71,7 +97,7 @@ impl UTXOCohorts {
|
||||
.get_mut(output_type)
|
||||
.state
|
||||
.um()
|
||||
.send(supply_state, current_price, prev_price, age)
|
||||
.send_utxo(supply_state, current_price, prev_price, peak_price, age)
|
||||
});
|
||||
|
||||
// Update amount range cohorts
|
||||
@@ -83,7 +109,7 @@ impl UTXOCohorts {
|
||||
.get_mut(group)
|
||||
.state
|
||||
.um()
|
||||
.send(supply_state, current_price, prev_price, age);
|
||||
.send_utxo(supply_state, current_price, prev_price, peak_price, age);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ impl UTXOCohorts {
|
||||
/// UTXOs age with each block. When they cross hour boundaries,
|
||||
/// they move between age-based cohorts (e.g., from "0-1h" to "1h-1d").
|
||||
///
|
||||
/// Complexity: O(k * (log n + m)) where:
|
||||
/// Complexity: O(k * log n) where:
|
||||
/// - k = 20 boundaries to check
|
||||
/// - n = total blocks in chain_state
|
||||
/// - m = blocks crossing each boundary (typically 0-2 per boundary per block)
|
||||
/// - Linear scan for end_idx is faster than binary search since typically 0-2 blocks cross each boundary
|
||||
pub fn tick_tock_next_block(&mut self, chain_state: &[BlockState], timestamp: Timestamp) {
|
||||
if chain_state.is_empty() {
|
||||
return;
|
||||
@@ -49,9 +49,12 @@ impl UTXOCohorts {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Binary search to find blocks in the timestamp range (lower, upper]
|
||||
// Binary search to find start, then linear scan for end (typically 0-2 blocks)
|
||||
let start_idx = chain_state.partition_point(|b| *b.timestamp <= lower_timestamp);
|
||||
let end_idx = chain_state.partition_point(|b| *b.timestamp <= upper_timestamp);
|
||||
let end_idx = chain_state[start_idx..]
|
||||
.iter()
|
||||
.position(|b| *b.timestamp > upper_timestamp)
|
||||
.map_or(chain_state.len(), |pos| start_idx + pos);
|
||||
|
||||
// Move supply from younger cohort to older cohort
|
||||
for block_state in &chain_state[start_idx..end_idx] {
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::path::Path;
|
||||
use brk_cohort::{CohortContext, Filter, Filtered, StateLevel};
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{DateIndex, Dollars, Height, Version};
|
||||
use brk_types::{CentsUnsigned, DateIndex, Dollars, Height, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, Database, Exit, IterableVec};
|
||||
|
||||
@@ -142,7 +142,7 @@ impl DynCohortVecs for UTXOCohortVecs {
|
||||
// State files are saved AT height H, so to resume at H+1 we need to import at H
|
||||
// Decrement first, then increment result to match expected starting_height
|
||||
if let Some(mut prev_height) = starting_height.decremented() {
|
||||
// Import price_to_amount state file (may adjust prev_height to actual file found)
|
||||
// Import cost_basis_data state file (may adjust prev_height to actual file found)
|
||||
prev_height = state.import_at_or_before(prev_height)?;
|
||||
|
||||
// Restore supply state from height-indexed vectors
|
||||
@@ -160,15 +160,8 @@ impl DynCohortVecs for UTXOCohortVecs {
|
||||
.height
|
||||
.read_once(prev_height)?;
|
||||
|
||||
// Restore realized cap if present
|
||||
if let Some(realized_metrics) = self.metrics.realized.as_mut()
|
||||
&& let Some(realized_state) = state.realized.as_mut()
|
||||
{
|
||||
realized_state.cap = realized_metrics
|
||||
.realized_cap
|
||||
.height
|
||||
.read_once(prev_height)?;
|
||||
}
|
||||
// Restore realized cap from persisted exact values
|
||||
state.restore_realized_cap();
|
||||
|
||||
let result = prev_height.incremented();
|
||||
self.state_starting_height = Some(result);
|
||||
@@ -204,9 +197,9 @@ impl DynCohortVecs for UTXOCohortVecs {
|
||||
fn compute_then_truncate_push_unrealized_states(
|
||||
&mut self,
|
||||
height: Height,
|
||||
height_price: Option<Dollars>,
|
||||
height_price: Option<CentsUnsigned>,
|
||||
dateindex: Option<DateIndex>,
|
||||
date_price: Option<Option<Dollars>>,
|
||||
date_price: Option<Option<CentsUnsigned>>,
|
||||
) -> Result<()> {
|
||||
if let Some(state) = self.state.as_mut() {
|
||||
self.metrics.compute_then_truncate_push_unrealized_states(
|
||||
|
||||
@@ -3,10 +3,10 @@ use std::thread;
|
||||
use brk_cohort::ByAddressType;
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_types::{DateIndex, Height, OutputType, Sats, TxIndex, TypeIndex};
|
||||
use brk_types::{CentsUnsigned, DateIndex, Dollars, Height, OutputType, Sats, TxIndex, TypeIndex};
|
||||
use rayon::prelude::*;
|
||||
use rustc_hash::FxHashSet;
|
||||
use tracing::info;
|
||||
use tracing::{debug, info};
|
||||
use vecdb::{Exit, IterableVec, TypedVecIterator, VecIndex};
|
||||
|
||||
use crate::{
|
||||
@@ -51,7 +51,9 @@ pub fn process_blocks(
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
// Create computation context with pre-computed vectors for thread-safe access
|
||||
debug!("creating ComputeContext");
|
||||
let ctx = ComputeContext::new(starting_height, last_height, blocks, price);
|
||||
debug!("ComputeContext created");
|
||||
|
||||
if ctx.starting_height > ctx.last_height {
|
||||
return Ok(());
|
||||
@@ -75,9 +77,9 @@ pub fn process_blocks(
|
||||
let txindex_to_output_count = &indexes.txindex.output_count;
|
||||
let txindex_to_input_count = &indexes.txindex.input_count;
|
||||
|
||||
// From price (optional):
|
||||
let height_to_price = price.map(|p| &p.usd.split.close.height);
|
||||
let dateindex_to_price = price.map(|p| &p.usd.split.close.dateindex);
|
||||
// From price (optional) - use cents for computation:
|
||||
let height_to_price = price.map(|p| &p.cents.split.height.close);
|
||||
let dateindex_to_price = price.map(|p| &p.cents.split.dateindex.close);
|
||||
|
||||
// Access pre-computed vectors from context for thread-safe access
|
||||
let height_to_price_vec = &ctx.height_to_price;
|
||||
@@ -99,9 +101,12 @@ pub fn process_blocks(
|
||||
let mut height_to_price_iter = height_to_price.map(|v| v.into_iter());
|
||||
let mut dateindex_to_price_iter = dateindex_to_price.map(|v| v.into_iter());
|
||||
|
||||
debug!("creating VecsReaders");
|
||||
let mut vr = VecsReaders::new(&vecs.any_address_indexes, &vecs.addresses_data);
|
||||
debug!("VecsReaders created");
|
||||
|
||||
// Build txindex -> height lookup map for efficient prev_height computation
|
||||
debug!("building txindex_to_height RangeMap");
|
||||
let mut txindex_to_height: RangeMap<TxIndex, Height> = {
|
||||
let mut map = RangeMap::with_capacity(last_height.to_usize() + 1);
|
||||
for first_txindex in indexer.vecs.transactions.first_txindex.into_iter() {
|
||||
@@ -109,6 +114,7 @@ pub fn process_blocks(
|
||||
}
|
||||
map
|
||||
};
|
||||
debug!("txindex_to_height RangeMap built");
|
||||
|
||||
// Create reusable iterators for sequential txout/txin reads (16KB buffered)
|
||||
let mut txout_iters = TxOutIterators::new(indexer);
|
||||
@@ -125,6 +131,7 @@ pub fn process_blocks(
|
||||
let mut first_p2wsh_iter = indexer.vecs.addresses.first_p2wshaddressindex.into_iter();
|
||||
|
||||
// Track running totals - recover from previous height if resuming
|
||||
debug!("recovering addr_counts from height {}", starting_height);
|
||||
let (mut addr_counts, mut empty_addr_counts) = if starting_height > Height::ZERO {
|
||||
let addr_counts =
|
||||
AddressTypeToAddressCount::from((&vecs.addr_count.by_addresstype, starting_height));
|
||||
@@ -139,11 +146,14 @@ pub fn process_blocks(
|
||||
AddressTypeToAddressCount::default(),
|
||||
)
|
||||
};
|
||||
debug!("addr_counts recovered");
|
||||
|
||||
// Track activity counts - reset each block
|
||||
let mut activity_counts = AddressTypeToActivityCounts::default();
|
||||
|
||||
debug!("creating AddressCache");
|
||||
let mut cache = AddressCache::new();
|
||||
debug!("AddressCache created, entering main loop");
|
||||
|
||||
// Main block iteration
|
||||
for height in starting_height.to_usize()..=last_height.to_usize() {
|
||||
@@ -253,8 +263,8 @@ pub fn process_blocks(
|
||||
});
|
||||
|
||||
// Merge new address data into current cache
|
||||
cache.merge_loaded(outputs_result.address_data);
|
||||
cache.merge_loaded(inputs_result.address_data);
|
||||
cache.merge_funded(outputs_result.address_data);
|
||||
cache.merge_funded(inputs_result.address_data);
|
||||
|
||||
// Combine txindex_vecs from outputs and inputs, then update tx_count
|
||||
let combined_txindex_vecs = outputs_result
|
||||
@@ -329,6 +339,7 @@ pub fn process_blocks(
|
||||
&mut vecs.address_cohorts,
|
||||
&mut lookup,
|
||||
block_price,
|
||||
ctx.price_range_max.as_ref(),
|
||||
&mut addr_counts,
|
||||
&mut empty_addr_counts,
|
||||
&mut activity_counts,
|
||||
@@ -344,7 +355,8 @@ pub fn process_blocks(
|
||||
// Main thread: Update UTXO cohorts
|
||||
vecs.utxo_cohorts
|
||||
.receive(transacted, height, timestamp, block_price);
|
||||
vecs.utxo_cohorts.send(height_to_sent, chain_state);
|
||||
vecs.utxo_cohorts
|
||||
.send(height_to_sent, chain_state, ctx.price_range_max.as_ref());
|
||||
});
|
||||
|
||||
// Push to height-indexed vectors
|
||||
@@ -382,8 +394,27 @@ pub fn process_blocks(
|
||||
|
||||
// Compute and push percentiles for aggregate cohorts (all, sth, lth)
|
||||
if let Some(dateindex) = dateindex_opt {
|
||||
let spot = date_price
|
||||
.flatten()
|
||||
.map(|c| c.to_dollars())
|
||||
.unwrap_or(Dollars::NAN);
|
||||
vecs.utxo_cohorts
|
||||
.truncate_push_aggregate_percentiles(dateindex)?;
|
||||
.truncate_push_aggregate_percentiles(dateindex, spot)?;
|
||||
|
||||
// Compute unrealized peak regret by age range (once per day)
|
||||
// Aggregate cohorts (all, term, etc.) get values via compute_from_stateful
|
||||
if let Some(spot_cents) = block_price
|
||||
&& let Some(price_range_max) = ctx.price_range_max.as_ref()
|
||||
{
|
||||
vecs.utxo_cohorts.compute_and_push_peak_regret(
|
||||
chain_state,
|
||||
height,
|
||||
timestamp,
|
||||
spot_cents,
|
||||
price_range_max,
|
||||
dateindex,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic checkpoint flush
|
||||
@@ -394,14 +425,14 @@ pub fn process_blocks(
|
||||
// Drop readers to release mmap handles
|
||||
drop(vr);
|
||||
|
||||
let (empty_updates, loaded_updates) = cache.take();
|
||||
let (empty_updates, funded_updates) = cache.take();
|
||||
|
||||
// Process address updates (mutations)
|
||||
process_address_updates(
|
||||
&mut vecs.addresses_data,
|
||||
&mut vecs.any_address_indexes,
|
||||
empty_updates,
|
||||
loaded_updates,
|
||||
funded_updates,
|
||||
)?;
|
||||
|
||||
let _lock = exit.lock();
|
||||
@@ -420,14 +451,14 @@ pub fn process_blocks(
|
||||
let _lock = exit.lock();
|
||||
drop(vr);
|
||||
|
||||
let (empty_updates, loaded_updates) = cache.take();
|
||||
let (empty_updates, funded_updates) = cache.take();
|
||||
|
||||
// Process address updates (mutations)
|
||||
process_address_updates(
|
||||
&mut vecs.addresses_data,
|
||||
&mut vecs.any_address_indexes,
|
||||
empty_updates,
|
||||
loaded_updates,
|
||||
funded_updates,
|
||||
)?;
|
||||
|
||||
// Write to disk (pure I/O) - save changes for rollback
|
||||
@@ -456,9 +487,9 @@ fn push_cohort_states(
|
||||
utxo_cohorts: &mut UTXOCohorts,
|
||||
address_cohorts: &mut AddressCohorts,
|
||||
height: Height,
|
||||
height_price: Option<brk_types::Dollars>,
|
||||
height_price: Option<CentsUnsigned>,
|
||||
dateindex: Option<DateIndex>,
|
||||
date_price: Option<Option<brk_types::Dollars>>,
|
||||
date_price: Option<Option<CentsUnsigned>>,
|
||||
) -> Result<()> {
|
||||
// utxo_cohorts.iter_separate_mut().try_for_each(|v| {
|
||||
utxo_cohorts.par_iter_separate_mut().try_for_each(|v| {
|
||||
|
||||
@@ -1,8 +1,99 @@
|
||||
use brk_types::{Dollars, Height, Timestamp};
|
||||
use std::time::Instant;
|
||||
|
||||
use brk_types::{CentsUnsigned, Height, Timestamp};
|
||||
use tracing::debug;
|
||||
use vecdb::VecIndex;
|
||||
|
||||
use crate::{blocks, price};
|
||||
|
||||
/// Sparse table for O(1) range maximum queries on prices.
|
||||
/// Uses O(n log n) space (~140MB for 880k blocks).
|
||||
pub struct PriceRangeMax {
|
||||
/// Flattened table: table[k * n + i] = max of 2^k elements starting at index i
|
||||
/// Using flat layout for better cache locality.
|
||||
table: Vec<CentsUnsigned>,
|
||||
/// Number of elements
|
||||
n: usize,
|
||||
}
|
||||
|
||||
impl PriceRangeMax {
|
||||
/// Build sparse table from high prices. O(n log n) time and space.
|
||||
pub fn build(prices: &[CentsUnsigned]) -> Self {
|
||||
let start = Instant::now();
|
||||
|
||||
let n = prices.len();
|
||||
if n == 0 {
|
||||
return Self {
|
||||
table: vec![],
|
||||
n: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// levels = floor(log2(n)) + 1
|
||||
let levels = (usize::BITS - n.leading_zeros()) as usize;
|
||||
|
||||
// Allocate flat table: levels * n elements
|
||||
let mut table = vec![CentsUnsigned::ZERO; levels * n];
|
||||
|
||||
// Base case: level 0 = original prices
|
||||
table[..n].copy_from_slice(prices);
|
||||
|
||||
// Build each level from the previous
|
||||
// table[k][i] = max(table[k-1][i], table[k-1][i + 2^(k-1)])
|
||||
for k in 1..levels {
|
||||
let prev_offset = (k - 1) * n;
|
||||
let curr_offset = k * n;
|
||||
let half = 1 << (k - 1);
|
||||
let end = n.saturating_sub(1 << k) + 1;
|
||||
|
||||
// Use split_at_mut to avoid bounds checks in the loop
|
||||
let (prev_level, rest) = table.split_at_mut(curr_offset);
|
||||
let prev = &prev_level[prev_offset..prev_offset + n];
|
||||
let curr = &mut rest[..n];
|
||||
|
||||
for i in 0..end {
|
||||
curr[i] = prev[i].max(prev[i + half]);
|
||||
}
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
debug!(
|
||||
"PriceRangeMax built: {} heights, {} levels, {:.2}MB, {:.2}ms",
|
||||
n,
|
||||
levels,
|
||||
(levels * n * std::mem::size_of::<CentsUnsigned>()) as f64 / 1_000_000.0,
|
||||
elapsed.as_secs_f64() * 1000.0
|
||||
);
|
||||
|
||||
Self { table, n }
|
||||
}
|
||||
|
||||
/// Query maximum value in range [l, r] (inclusive). O(1) time.
|
||||
#[inline]
|
||||
pub fn range_max(&self, l: usize, r: usize) -> CentsUnsigned {
|
||||
debug_assert!(l <= r && r < self.n);
|
||||
|
||||
let len = r - l + 1;
|
||||
// k = floor(log2(len))
|
||||
let k = (usize::BITS - len.leading_zeros() - 1) as usize;
|
||||
let half = 1 << k;
|
||||
|
||||
// max of [l, l + 2^k) and [r - 2^k + 1, r + 1)
|
||||
let offset = k * self.n;
|
||||
unsafe {
|
||||
let a = *self.table.get_unchecked(offset + l);
|
||||
let b = *self.table.get_unchecked(offset + r + 1 - half);
|
||||
a.max(b)
|
||||
}
|
||||
}
|
||||
|
||||
/// Query maximum value in height range. O(1) time.
|
||||
#[inline]
|
||||
pub fn max_between(&self, from: Height, to: Height) -> CentsUnsigned {
|
||||
self.range_max(from.to_usize(), to.to_usize())
|
||||
}
|
||||
}
|
||||
|
||||
/// Context shared across block processing.
|
||||
pub struct ComputeContext {
|
||||
/// Starting height for this computation run
|
||||
@@ -15,7 +106,11 @@ pub struct ComputeContext {
|
||||
pub height_to_timestamp: Vec<Timestamp>,
|
||||
|
||||
/// Pre-computed height -> price mapping (if available)
|
||||
pub height_to_price: Option<Vec<Dollars>>,
|
||||
pub height_to_price: Option<Vec<CentsUnsigned>>,
|
||||
|
||||
/// Sparse table for O(1) range max queries on high prices.
|
||||
/// Used for computing max price during UTXO holding periods (peak regret).
|
||||
pub price_range_max: Option<PriceRangeMax>,
|
||||
}
|
||||
|
||||
impl ComputeContext {
|
||||
@@ -29,20 +124,28 @@ impl ComputeContext {
|
||||
let height_to_timestamp: Vec<Timestamp> =
|
||||
blocks.time.timestamp_monotonic.into_iter().collect();
|
||||
|
||||
let height_to_price: Option<Vec<Dollars>> = price
|
||||
.map(|p| &p.usd.split.close.height)
|
||||
.map(|v| v.into_iter().map(|d| *d).collect());
|
||||
let height_to_price: Option<Vec<CentsUnsigned>> = price
|
||||
.map(|p| &p.cents.split.height.close)
|
||||
.map(|v| v.into_iter().map(|c| *c).collect());
|
||||
|
||||
// Build sparse table for O(1) range max queries on HIGH prices
|
||||
// Used for computing peak price during UTXO holding periods (peak regret)
|
||||
let price_range_max = price
|
||||
.map(|p| &p.cents.split.height.high)
|
||||
.map(|v| v.into_iter().map(|c| *c).collect::<Vec<_>>())
|
||||
.map(|prices| PriceRangeMax::build(&prices));
|
||||
|
||||
Self {
|
||||
starting_height,
|
||||
last_height,
|
||||
height_to_timestamp,
|
||||
height_to_price,
|
||||
price_range_max,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get price at height (None if no price data or height out of range).
|
||||
pub fn price_at(&self, height: Height) -> Option<Dollars> {
|
||||
pub fn price_at(&self, height: Height) -> Option<CentsUnsigned> {
|
||||
self.height_to_price
|
||||
.as_ref()?
|
||||
.get(height.to_usize())
|
||||
|
||||
@@ -6,7 +6,7 @@ mod recover;
|
||||
mod write;
|
||||
|
||||
pub use block_loop::process_blocks;
|
||||
pub use context::ComputeContext;
|
||||
pub use context::{ComputeContext, PriceRangeMax};
|
||||
pub use readers::{
|
||||
TxInIterators, TxOutData, TxOutIterators, VecsReaders, build_txinindex_to_txindex,
|
||||
build_txoutindex_to_txindex,
|
||||
|
||||
@@ -140,7 +140,7 @@ impl VecsReaders {
|
||||
p2wsh: any_address_indexes.p2wsh.create_reader(),
|
||||
},
|
||||
anyaddressindex_to_anyaddressdata: ByAnyAddress {
|
||||
loaded: addresses_data.loaded.create_reader(),
|
||||
funded: addresses_data.funded.create_reader(),
|
||||
empty: addresses_data.empty.create_reader(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -71,20 +71,24 @@ pub fn recover_state(
|
||||
}
|
||||
|
||||
// Import UTXO cohort states - all must succeed
|
||||
debug!("importing UTXO cohort states at height {}", consistent_height);
|
||||
if !utxo_cohorts.import_separate_states(consistent_height) {
|
||||
warn!("UTXO cohort state import failed at height {}", consistent_height);
|
||||
return Ok(RecoveredState {
|
||||
starting_height: Height::ZERO,
|
||||
});
|
||||
}
|
||||
debug!("UTXO cohort states imported");
|
||||
|
||||
// Import address cohort states - all must succeed
|
||||
debug!("importing address cohort states at height {}", consistent_height);
|
||||
if !address_cohorts.import_separate_states(consistent_height) {
|
||||
warn!("Address cohort state import failed at height {}", consistent_height);
|
||||
return Ok(RecoveredState {
|
||||
starting_height: Height::ZERO,
|
||||
});
|
||||
}
|
||||
debug!("address cohort states imported");
|
||||
|
||||
Ok(RecoveredState {
|
||||
starting_height: consistent_height,
|
||||
@@ -108,9 +112,9 @@ pub fn reset_state(
|
||||
utxo_cohorts.reset_separate_state_heights();
|
||||
address_cohorts.reset_separate_state_heights();
|
||||
|
||||
// Reset price_to_amount for all cohorts
|
||||
utxo_cohorts.reset_separate_price_to_amount()?;
|
||||
address_cohorts.reset_separate_price_to_amount()?;
|
||||
// Reset cost_basis_data for all cohorts
|
||||
utxo_cohorts.reset_separate_cost_basis_data()?;
|
||||
address_cohorts.reset_separate_cost_basis_data()?;
|
||||
|
||||
Ok(RecoveredState {
|
||||
starting_height: Height::ZERO,
|
||||
|
||||
@@ -9,8 +9,8 @@ use vecdb::{AnyStoredVec, GenericStoredVec, Stamp};
|
||||
use crate::distribution::{
|
||||
Vecs,
|
||||
block::{
|
||||
EmptyAddressDataWithSource, LoadedAddressDataWithSource, process_empty_addresses,
|
||||
process_loaded_addresses,
|
||||
EmptyAddressDataWithSource, FundedAddressDataWithSource, process_empty_addresses,
|
||||
process_funded_addresses,
|
||||
},
|
||||
state::BlockState,
|
||||
};
|
||||
@@ -21,7 +21,7 @@ use super::super::address::{AddressTypeToTypeIndexMap, AddressesDataVecs, AnyAdd
|
||||
///
|
||||
/// Applies all accumulated address changes to storage structures:
|
||||
/// - Processes empty address transitions
|
||||
/// - Processes loaded address transitions
|
||||
/// - Processes funded address transitions
|
||||
/// - Updates address indexes
|
||||
///
|
||||
/// Call this before `flush()` to prepare data for writing.
|
||||
@@ -29,14 +29,14 @@ pub fn process_address_updates(
|
||||
addresses_data: &mut AddressesDataVecs,
|
||||
address_indexes: &mut AnyAddressIndexesVecs,
|
||||
empty_updates: AddressTypeToTypeIndexMap<EmptyAddressDataWithSource>,
|
||||
loaded_updates: AddressTypeToTypeIndexMap<LoadedAddressDataWithSource>,
|
||||
funded_updates: AddressTypeToTypeIndexMap<FundedAddressDataWithSource>,
|
||||
) -> Result<()> {
|
||||
info!("Processing address updates...");
|
||||
|
||||
let i = Instant::now();
|
||||
let empty_result = process_empty_addresses(addresses_data, empty_updates)?;
|
||||
let loaded_result = process_loaded_addresses(addresses_data, loaded_updates)?;
|
||||
address_indexes.par_batch_update(empty_result, loaded_result)?;
|
||||
let funded_result = process_funded_addresses(addresses_data, funded_updates)?;
|
||||
address_indexes.par_batch_update(empty_result, funded_result)?;
|
||||
|
||||
info!("Processed address updates in {:?}", i.elapsed());
|
||||
|
||||
@@ -66,9 +66,9 @@ pub fn write(
|
||||
let stamp = Stamp::from(height);
|
||||
|
||||
// Prepare chain_state before parallel write
|
||||
vecs.chain_state.truncate_if_needed(Height::ZERO)?;
|
||||
vecs.supply_state.truncate_if_needed(Height::ZERO)?;
|
||||
for block_state in chain_state {
|
||||
vecs.chain_state.push(block_state.supply.clone());
|
||||
vecs.supply_state.push(block_state.supply.clone());
|
||||
}
|
||||
|
||||
vecs.any_address_indexes
|
||||
@@ -78,7 +78,7 @@ pub fn write(
|
||||
.chain(vecs.empty_addr_count.par_iter_height_mut())
|
||||
.chain(vecs.address_activity.par_iter_height_mut())
|
||||
.chain(rayon::iter::once(
|
||||
&mut vecs.chain_state as &mut dyn AnyStoredVec,
|
||||
&mut vecs.supply_state as &mut dyn AnyStoredVec,
|
||||
))
|
||||
.chain(vecs.utxo_cohorts.par_iter_vecs_mut())
|
||||
.chain(vecs.address_cohorts.par_iter_vecs_mut())
|
||||
|
||||
@@ -6,7 +6,7 @@ use vecdb::{AnyStoredVec, AnyVec, EagerVec, Exit, GenericStoredVec, ImportableVe
|
||||
|
||||
use crate::{
|
||||
ComputeIndexes, indexes,
|
||||
internal::{ComputedFromHeightSumCum, LazyComputedValueFromHeightSumCum},
|
||||
internal::{ComputedFromHeightSumCum, LazyComputedValueFromHeightSumCum, ValueFromDateLast},
|
||||
};
|
||||
|
||||
use super::ImportConfig;
|
||||
@@ -17,6 +17,9 @@ pub struct ActivityMetrics {
|
||||
/// Total satoshis sent at each height + derived indexes
|
||||
pub sent: LazyComputedValueFromHeightSumCum,
|
||||
|
||||
/// 14-day EMA of sent supply (sats, btc, usd)
|
||||
pub sent_14d_ema: ValueFromDateLast,
|
||||
|
||||
/// Satoshi-blocks destroyed (supply * blocks_old when spent)
|
||||
pub satblocks_destroyed: EagerVec<PcoVec<Height, Sats>>,
|
||||
|
||||
@@ -42,6 +45,14 @@ impl ActivityMetrics {
|
||||
cfg.price,
|
||||
)?,
|
||||
|
||||
sent_14d_ema: ValueFromDateLast::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("sent_14d_ema"),
|
||||
cfg.version,
|
||||
cfg.compute_dollars(),
|
||||
cfg.indexes,
|
||||
)?,
|
||||
|
||||
satblocks_destroyed: EagerVec::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("satblocks_destroyed"),
|
||||
@@ -96,14 +107,6 @@ impl ActivityMetrics {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write height-indexed vectors to disk.
|
||||
pub fn write(&mut self) -> Result<()> {
|
||||
self.sent.sats.height.write()?;
|
||||
self.satblocks_destroyed.write()?;
|
||||
self.satdays_destroyed.write()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a parallel iterator over all vecs for parallel writing.
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
vec![
|
||||
@@ -163,6 +166,15 @@ impl ActivityMetrics {
|
||||
) -> Result<()> {
|
||||
self.sent.compute_rest(indexes, starting_indexes, exit)?;
|
||||
|
||||
// 14-day EMA of sent (sats and dollars)
|
||||
self.sent_14d_ema.compute_ema(
|
||||
starting_indexes.dateindex,
|
||||
&self.sent.sats.dateindex.sum.0,
|
||||
self.sent.dollars.as_ref().map(|d| &d.dateindex.sum.0),
|
||||
14,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.coinblocks_destroyed
|
||||
.compute_all(indexes, starting_indexes, exit, |v| {
|
||||
v.compute_transform(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use brk_cohort::{CohortContext, Filter};
|
||||
use brk_cohort::{CohortContext, Filter, TimeFilter};
|
||||
use brk_types::Version;
|
||||
use vecdb::Database;
|
||||
|
||||
@@ -41,6 +41,11 @@ impl<'a> ImportConfig<'a> {
|
||||
self.filter.compute_adjusted(self.context)
|
||||
}
|
||||
|
||||
/// Whether to compute relative metrics (invested capital %, NUPL ratios, etc.).
|
||||
pub fn compute_relative(&self) -> bool {
|
||||
self.filter.compute_relative()
|
||||
}
|
||||
|
||||
/// Get full metric name with filter prefix.
|
||||
pub fn name(&self, suffix: &str) -> String {
|
||||
if self.full_name.is_empty() {
|
||||
@@ -51,4 +56,24 @@ impl<'a> ImportConfig<'a> {
|
||||
format!("{}_{suffix}", self.full_name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this cohort needs peak_regret metric.
|
||||
/// True for UTXO cohorts with age-based filters (all, term, time).
|
||||
/// age_range cohorts compute directly, others aggregate from age_range.
|
||||
pub fn compute_peak_regret(&self) -> bool {
|
||||
matches!(self.context, CohortContext::Utxo)
|
||||
&& matches!(
|
||||
self.filter,
|
||||
Filter::All | Filter::Term(_) | Filter::Time(_)
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether this is an age_range cohort (UTXO context with Time::Range filter).
|
||||
/// These cohorts have peak_regret computed directly from chain_state.
|
||||
pub fn is_age_range(&self) -> bool {
|
||||
matches!(
|
||||
(&self.context, &self.filter),
|
||||
(CohortContext::Utxo, Filter::Time(TimeFilter::Range(_)))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{DateIndex, Dollars, Height, Version};
|
||||
use brk_types::{DateIndex, Dollars, Height, StoredF32, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, AnyVec, Exit, GenericStoredVec};
|
||||
|
||||
@@ -8,7 +8,10 @@ use crate::{
|
||||
ComputeIndexes,
|
||||
distribution::state::CohortState,
|
||||
indexes,
|
||||
internal::{CostBasisPercentiles, PriceFromHeight},
|
||||
internal::{
|
||||
ComputedFromDateLast, PERCENTILES_LEN, PercentilesVecs, PriceFromHeight,
|
||||
compute_spot_percentile_rank,
|
||||
},
|
||||
};
|
||||
|
||||
use super::ImportConfig;
|
||||
@@ -22,8 +25,17 @@ pub struct CostBasisMetrics {
|
||||
/// Maximum cost basis for any UTXO at this height
|
||||
pub max: PriceFromHeight,
|
||||
|
||||
/// Cost basis distribution percentiles (median, quartiles, etc.)
|
||||
pub percentiles: Option<CostBasisPercentiles>,
|
||||
/// Cost basis percentiles (sat-weighted)
|
||||
pub percentiles: Option<PercentilesVecs>,
|
||||
|
||||
/// Invested capital percentiles (USD-weighted)
|
||||
pub invested_capital: Option<PercentilesVecs>,
|
||||
|
||||
/// What percentile of cost basis is below spot (sat-weighted)
|
||||
pub spot_cost_basis_percentile: Option<ComputedFromDateLast<StoredF32>>,
|
||||
|
||||
/// What percentile of invested capital is below spot (USD-weighted)
|
||||
pub spot_invested_capital_percentile: Option<ComputedFromDateLast<StoredF32>>,
|
||||
}
|
||||
|
||||
impl CostBasisMetrics {
|
||||
@@ -46,15 +58,46 @@ impl CostBasisMetrics {
|
||||
)?,
|
||||
percentiles: extended
|
||||
.then(|| {
|
||||
CostBasisPercentiles::forced_import(
|
||||
PercentilesVecs::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name(""),
|
||||
&cfg.name("cost_basis"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
true,
|
||||
)
|
||||
})
|
||||
.transpose()?,
|
||||
invested_capital: extended
|
||||
.then(|| {
|
||||
PercentilesVecs::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("invested_capital"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
true,
|
||||
)
|
||||
})
|
||||
.transpose()?,
|
||||
spot_cost_basis_percentile: extended
|
||||
.then(|| {
|
||||
ComputedFromDateLast::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("spot_cost_basis_percentile"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
)
|
||||
})
|
||||
.transpose()?,
|
||||
spot_invested_capital_percentile: extended
|
||||
.then(|| {
|
||||
ComputedFromDateLast::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("spot_invested_capital_percentile"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
)
|
||||
})
|
||||
.transpose()?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -69,6 +112,24 @@ impl CostBasisMetrics {
|
||||
.as_ref()
|
||||
.map(|p| p.min_stateful_dateindex_len())
|
||||
.unwrap_or(usize::MAX)
|
||||
.min(
|
||||
self.invested_capital
|
||||
.as_ref()
|
||||
.map(|p| p.min_stateful_dateindex_len())
|
||||
.unwrap_or(usize::MAX),
|
||||
)
|
||||
.min(
|
||||
self.spot_cost_basis_percentile
|
||||
.as_ref()
|
||||
.map(|v| v.dateindex.len())
|
||||
.unwrap_or(usize::MAX),
|
||||
)
|
||||
.min(
|
||||
self.spot_invested_capital_percentile
|
||||
.as_ref()
|
||||
.map(|v| v.dateindex.len())
|
||||
.unwrap_or(usize::MAX),
|
||||
)
|
||||
}
|
||||
|
||||
/// Push min/max cost basis from state.
|
||||
@@ -76,15 +137,15 @@ impl CostBasisMetrics {
|
||||
self.min.height.truncate_push(
|
||||
height,
|
||||
state
|
||||
.price_to_amount_first_key_value()
|
||||
.map(|(dollars, _)| dollars)
|
||||
.cost_basis_data_first_key_value()
|
||||
.map(|(cents, _)| cents.into())
|
||||
.unwrap_or(Dollars::NAN),
|
||||
)?;
|
||||
self.max.height.truncate_push(
|
||||
height,
|
||||
state
|
||||
.price_to_amount_last_key_value()
|
||||
.map(|(dollars, _)| dollars)
|
||||
.cost_basis_data_last_key_value()
|
||||
.map(|(cents, _)| cents.into())
|
||||
.unwrap_or(Dollars::NAN),
|
||||
)?;
|
||||
Ok(())
|
||||
@@ -96,21 +157,38 @@ impl CostBasisMetrics {
|
||||
&mut self,
|
||||
dateindex: DateIndex,
|
||||
state: &CohortState,
|
||||
spot: Dollars,
|
||||
) -> Result<()> {
|
||||
if let Some(percentiles) = self.percentiles.as_mut() {
|
||||
let percentile_prices = state.compute_percentile_prices();
|
||||
percentiles.truncate_push(dateindex, &percentile_prices)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
let computed = state.compute_percentiles();
|
||||
|
||||
// Push sat-weighted percentiles and spot rank
|
||||
let sat_prices = computed
|
||||
.as_ref()
|
||||
.map(|p| p.sat_weighted.map(|c| c.to_dollars()))
|
||||
.unwrap_or([Dollars::NAN; PERCENTILES_LEN]);
|
||||
|
||||
/// Write height-indexed vectors to disk.
|
||||
pub fn write(&mut self) -> Result<()> {
|
||||
self.min.height.write()?;
|
||||
self.max.height.write()?;
|
||||
if let Some(percentiles) = self.percentiles.as_mut() {
|
||||
percentiles.write()?;
|
||||
percentiles.truncate_push(dateindex, &sat_prices)?;
|
||||
}
|
||||
if let Some(spot_pct) = self.spot_cost_basis_percentile.as_mut() {
|
||||
let rank = compute_spot_percentile_rank(&sat_prices, spot);
|
||||
spot_pct.dateindex.truncate_push(dateindex, rank)?;
|
||||
}
|
||||
|
||||
// Push USD-weighted percentiles and spot rank
|
||||
let usd_prices = computed
|
||||
.as_ref()
|
||||
.map(|p| p.usd_weighted.map(|c| c.to_dollars()))
|
||||
.unwrap_or([Dollars::NAN; PERCENTILES_LEN]);
|
||||
|
||||
if let Some(invested_capital) = self.invested_capital.as_mut() {
|
||||
invested_capital.truncate_push(dateindex, &usd_prices)?;
|
||||
}
|
||||
if let Some(spot_pct) = self.spot_invested_capital_percentile.as_mut() {
|
||||
let rank = compute_spot_percentile_rank(&usd_prices, spot);
|
||||
spot_pct.dateindex.truncate_push(dateindex, rank)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -126,6 +204,21 @@ impl CostBasisMetrics {
|
||||
.map(|v| &mut v.dateindex as &mut dyn AnyStoredVec),
|
||||
);
|
||||
}
|
||||
if let Some(invested_capital) = self.invested_capital.as_mut() {
|
||||
vecs.extend(
|
||||
invested_capital
|
||||
.vecs
|
||||
.iter_mut()
|
||||
.flatten()
|
||||
.map(|v| &mut v.dateindex as &mut dyn AnyStoredVec),
|
||||
);
|
||||
}
|
||||
if let Some(v) = self.spot_cost_basis_percentile.as_mut() {
|
||||
vecs.push(&mut v.dateindex);
|
||||
}
|
||||
if let Some(v) = self.spot_invested_capital_percentile.as_mut() {
|
||||
vecs.push(&mut v.dateindex);
|
||||
}
|
||||
vecs.into_par_iter()
|
||||
}
|
||||
|
||||
@@ -134,6 +227,15 @@ impl CostBasisMetrics {
|
||||
if let Some(percentiles) = self.percentiles.as_mut() {
|
||||
percentiles.validate_computed_version_or_reset(base_version)?;
|
||||
}
|
||||
if let Some(invested_capital) = self.invested_capital.as_mut() {
|
||||
invested_capital.validate_computed_version_or_reset(base_version)?;
|
||||
}
|
||||
if let Some(v) = self.spot_cost_basis_percentile.as_mut() {
|
||||
v.dateindex.validate_computed_version_or_reset(base_version)?;
|
||||
}
|
||||
if let Some(v) = self.spot_invested_capital_percentile.as_mut() {
|
||||
v.dateindex.validate_computed_version_or_reset(base_version)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ pub use unrealized::*;
|
||||
use brk_cohort::Filter;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{DateIndex, Dollars, Height, Version};
|
||||
use brk_types::{CentsUnsigned, DateIndex, Dollars, Height, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, Exit, IterableVec};
|
||||
|
||||
@@ -69,9 +69,20 @@ impl CohortMetrics {
|
||||
.then(|| UnrealizedMetrics::forced_import(cfg))
|
||||
.transpose()?;
|
||||
|
||||
let relative = unrealized
|
||||
.as_ref()
|
||||
.map(|u| RelativeMetrics::forced_import(cfg, u, &supply, all_supply))
|
||||
let realized = compute_dollars
|
||||
.then(|| RealizedMetrics::forced_import(cfg))
|
||||
.transpose()?;
|
||||
|
||||
let relative = (cfg.compute_relative() && unrealized.is_some())
|
||||
.then(|| {
|
||||
RelativeMetrics::forced_import(
|
||||
cfg,
|
||||
unrealized.as_ref().unwrap(),
|
||||
&supply,
|
||||
all_supply,
|
||||
realized.as_ref(),
|
||||
)
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
Ok(Self {
|
||||
@@ -79,9 +90,7 @@ impl CohortMetrics {
|
||||
supply,
|
||||
outputs,
|
||||
activity: ActivityMetrics::forced_import(cfg)?,
|
||||
realized: compute_dollars
|
||||
.then(|| RealizedMetrics::forced_import(cfg))
|
||||
.transpose()?,
|
||||
realized,
|
||||
cost_basis: compute_dollars
|
||||
.then(|| CostBasisMetrics::forced_import(cfg))
|
||||
.transpose()?,
|
||||
@@ -146,27 +155,6 @@ impl CohortMetrics {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write height-indexed vectors to disk.
|
||||
pub fn write(&mut self) -> Result<()> {
|
||||
self.supply.write()?;
|
||||
self.outputs.write()?;
|
||||
self.activity.write()?;
|
||||
|
||||
if let Some(realized) = self.realized.as_mut() {
|
||||
realized.write()?;
|
||||
}
|
||||
|
||||
if let Some(unrealized) = self.unrealized.as_mut() {
|
||||
unrealized.write()?;
|
||||
}
|
||||
|
||||
if let Some(cost_basis) = self.cost_basis.as_mut() {
|
||||
cost_basis.write()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a parallel iterator over all vecs for parallel writing.
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
|
||||
@@ -211,9 +199,9 @@ impl CohortMetrics {
|
||||
pub fn compute_then_truncate_push_unrealized_states(
|
||||
&mut self,
|
||||
height: Height,
|
||||
height_price: Option<Dollars>,
|
||||
height_price: Option<CentsUnsigned>,
|
||||
dateindex: Option<DateIndex>,
|
||||
date_price: Option<Option<Dollars>>,
|
||||
date_price: Option<Option<CentsUnsigned>>,
|
||||
state: &mut CohortState,
|
||||
) -> Result<()> {
|
||||
// Apply pending updates before reading
|
||||
@@ -238,7 +226,11 @@ impl CohortMetrics {
|
||||
|
||||
// Only compute expensive percentiles at date boundaries (~144x reduction)
|
||||
if let Some(dateindex) = dateindex {
|
||||
cost_basis.truncate_push_percentiles(dateindex, state)?;
|
||||
let spot = date_price
|
||||
.unwrap()
|
||||
.map(|c| c.to_dollars())
|
||||
.unwrap_or(Dollars::NAN);
|
||||
cost_basis.truncate_push_percentiles(dateindex, state, spot)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +296,42 @@ impl CohortMetrics {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute net_sentiment.height as capital-weighted average of component cohorts.
|
||||
///
|
||||
/// For aggregate cohorts, the simple greed-pain formula produces values outside
|
||||
/// the range of components due to asymmetric weighting. This computes net_sentiment
|
||||
/// as a proper weighted average using realized_cap as weight.
|
||||
///
|
||||
/// Only computes height; dateindex derivation is done separately via compute_net_sentiment_rest.
|
||||
pub fn compute_net_sentiment_from_others(
|
||||
&mut self,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
others: &[&Self],
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let Some(unrealized) = self.unrealized.as_mut() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let weights: Vec<_> = others
|
||||
.iter()
|
||||
.filter_map(|o| Some(&o.realized.as_ref()?.realized_cap.height))
|
||||
.collect();
|
||||
let values: Vec<_> = others
|
||||
.iter()
|
||||
.filter_map(|o| Some(&o.unrealized.as_ref()?.net_sentiment.height))
|
||||
.collect();
|
||||
|
||||
if weights.len() != others.len() || values.len() != others.len() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Ok(unrealized
|
||||
.net_sentiment
|
||||
.height
|
||||
.compute_weighted_average_of_others(starting_indexes.height, &weights, &values, exit)?)
|
||||
}
|
||||
|
||||
/// First phase of computed metrics (indexes from height).
|
||||
pub fn compute_rest_part1(
|
||||
&mut self,
|
||||
@@ -323,7 +351,7 @@ impl CohortMetrics {
|
||||
}
|
||||
|
||||
if let Some(unrealized) = self.unrealized.as_mut() {
|
||||
unrealized.compute_rest_part1(price, starting_indexes, exit)?;
|
||||
unrealized.compute_rest(indexes, price, starting_indexes, exit)?;
|
||||
}
|
||||
|
||||
if let Some(cost_basis) = self.cost_basis.as_mut() {
|
||||
@@ -358,4 +386,31 @@ impl CohortMetrics {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute net_sentiment.height for separate cohorts (greed - pain).
|
||||
/// Called only for separate cohorts; aggregates compute via weighted average in compute_from_stateful.
|
||||
pub fn compute_net_sentiment_height(
|
||||
&mut self,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
if let Some(unrealized) = self.unrealized.as_mut() {
|
||||
unrealized.compute_net_sentiment_height(starting_indexes, exit)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute net_sentiment dateindex derivation from height.
|
||||
/// Called for ALL cohorts after height is computed.
|
||||
pub fn compute_net_sentiment_rest(
|
||||
&mut self,
|
||||
indexes: &indexes::Vecs,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
if let Some(unrealized) = self.unrealized.as_mut() {
|
||||
unrealized.compute_net_sentiment_rest(indexes, starting_indexes, exit)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, StoredU64};
|
||||
use brk_types::{Height, StoredF64, StoredU64};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, AnyVec, Exit, GenericStoredVec};
|
||||
|
||||
use crate::{ComputeIndexes, indexes, internal::ComputedFromHeightLast};
|
||||
use crate::{ComputeIndexes, indexes, internal::{ComputedFromDateLast, ComputedFromHeightLast}};
|
||||
|
||||
use super::ImportConfig;
|
||||
|
||||
@@ -12,6 +12,7 @@ use super::ImportConfig;
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct OutputsMetrics {
|
||||
pub utxo_count: ComputedFromHeightLast<StoredU64>,
|
||||
pub utxo_count_30d_change: ComputedFromDateLast<StoredF64>,
|
||||
}
|
||||
|
||||
impl OutputsMetrics {
|
||||
@@ -24,6 +25,12 @@ impl OutputsMetrics {
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
)?,
|
||||
utxo_count_30d_change: ComputedFromDateLast::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("utxo_count_30d_change"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -40,15 +47,13 @@ impl OutputsMetrics {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write height-indexed vectors to disk.
|
||||
pub fn write(&mut self) -> Result<()> {
|
||||
self.utxo_count.height.write()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a parallel iterator over all vecs for parallel writing.
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
vec![&mut self.utxo_count.height as &mut dyn AnyStoredVec].into_par_iter()
|
||||
vec![
|
||||
&mut self.utxo_count.height as &mut dyn AnyStoredVec,
|
||||
&mut self.utxo_count_30d_change.dateindex as &mut dyn AnyStoredVec,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
|
||||
/// Compute aggregate values from separate cohorts.
|
||||
@@ -76,6 +81,20 @@ impl OutputsMetrics {
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.utxo_count.compute_rest(indexes, starting_indexes, exit)
|
||||
self.utxo_count
|
||||
.compute_rest(indexes, starting_indexes, exit)?;
|
||||
|
||||
self.utxo_count_30d_change
|
||||
.compute_all(starting_indexes, exit, |v| {
|
||||
v.compute_change(
|
||||
starting_indexes.dateindex,
|
||||
&*self.utxo_count.dateindex,
|
||||
30,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Bitcoin, DateIndex, Dollars, Height, StoredF32, StoredF64, Version};
|
||||
use brk_types::{
|
||||
Bitcoin, CentsSats, CentsSquaredSats, CentsUnsigned, DateIndex, Dollars, Height, StoredF32,
|
||||
StoredF64, Version,
|
||||
};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, EagerVec, Exit, GenericStoredVec, Ident, ImportableVec,
|
||||
IterableCloneableVec, IterableVec, Negate, PcoVec,
|
||||
AnyStoredVec, AnyVec, BytesVec, EagerVec, Exit, GenericStoredVec, Ident, ImportableVec,
|
||||
IterableCloneableVec, IterableVec, Negate, PcoVec, TypedVecIterator,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -12,10 +15,12 @@ use crate::{
|
||||
distribution::state::RealizedState,
|
||||
indexes,
|
||||
internal::{
|
||||
ComputedFromHeightLast, ComputedFromHeightSum, ComputedFromHeightSumCum, ComputedFromDateLast,
|
||||
ComputedFromDateRatio, DollarsMinus, LazyBinaryFromHeightSum, LazyBinaryFromHeightSumCum,
|
||||
LazyFromHeightSum, LazyFromHeightSumCum, LazyFromDateLast, PercentageDollarsF32,
|
||||
PriceFromHeight, StoredF32Identity,
|
||||
CentsUnsignedToDollars, ComputedFromDateLast, ComputedFromDateRatio,
|
||||
ComputedFromHeightLast, ComputedFromHeightSum, ComputedFromHeightSumCum, DollarsMinus,
|
||||
DollarsPlus, LazyBinaryFromHeightSum, LazyBinaryFromHeightSumCum,
|
||||
LazyComputedValueFromHeightSumCum, LazyFromDateLast, LazyFromHeightLast, LazyFromHeightSum,
|
||||
LazyFromHeightSumCum, LazyPriceFromCents, PercentageDollarsF32, PriceFromHeight,
|
||||
StoredF32Identity, ValueFromDateLast,
|
||||
},
|
||||
price,
|
||||
};
|
||||
@@ -26,35 +31,62 @@ use super::ImportConfig;
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct RealizedMetrics {
|
||||
// === Realized Cap ===
|
||||
pub realized_cap: ComputedFromHeightLast<Dollars>,
|
||||
pub realized_cap_cents: ComputedFromHeightLast<CentsUnsigned>,
|
||||
pub realized_cap: LazyFromHeightLast<Dollars, CentsUnsigned>,
|
||||
pub realized_price: PriceFromHeight,
|
||||
pub realized_price_extra: ComputedFromDateRatio,
|
||||
pub realized_cap_rel_to_own_market_cap: Option<ComputedFromHeightLast<StoredF32>>,
|
||||
pub realized_cap_30d_delta: ComputedFromDateLast<Dollars>,
|
||||
|
||||
// === Investor Price (dollar-weighted average acquisition price) ===
|
||||
pub investor_price_cents: ComputedFromHeightLast<CentsUnsigned>,
|
||||
pub investor_price: LazyPriceFromCents,
|
||||
pub investor_price_extra: ComputedFromDateRatio,
|
||||
|
||||
// === Raw values for aggregation (needed to compute investor_price for aggregated cohorts) ===
|
||||
/// Raw Σ(price × sats) for realized cap aggregation
|
||||
pub cap_raw: BytesVec<Height, CentsSats>,
|
||||
/// Raw Σ(price² × sats) for investor_price aggregation
|
||||
pub investor_cap_raw: BytesVec<Height, CentsSquaredSats>,
|
||||
|
||||
// === MVRV (Market Value to Realized Value) ===
|
||||
// Proxy for realized_price_extra.ratio (close / realized_price = market_cap / realized_cap)
|
||||
pub mvrv: LazyFromDateLast<StoredF32>,
|
||||
|
||||
// === Realized Profit/Loss ===
|
||||
pub realized_profit: ComputedFromHeightSumCum<Dollars>,
|
||||
pub realized_profit_7d_ema: ComputedFromDateLast<Dollars>,
|
||||
pub realized_loss: ComputedFromHeightSumCum<Dollars>,
|
||||
pub realized_loss_7d_ema: ComputedFromDateLast<Dollars>,
|
||||
pub neg_realized_loss: LazyFromHeightSumCum<Dollars>,
|
||||
pub net_realized_pnl: ComputedFromHeightSumCum<Dollars>,
|
||||
pub net_realized_pnl_7d_ema: ComputedFromDateLast<Dollars>,
|
||||
pub realized_value: ComputedFromHeightSum<Dollars>,
|
||||
|
||||
// === Realized vs Realized Cap Ratios (lazy) ===
|
||||
pub realized_profit_rel_to_realized_cap: LazyBinaryFromHeightSumCum<StoredF32, Dollars, Dollars>,
|
||||
pub realized_profit_rel_to_realized_cap:
|
||||
LazyBinaryFromHeightSumCum<StoredF32, Dollars, Dollars>,
|
||||
pub realized_loss_rel_to_realized_cap: LazyBinaryFromHeightSumCum<StoredF32, Dollars, Dollars>,
|
||||
pub net_realized_pnl_rel_to_realized_cap: LazyBinaryFromHeightSumCum<StoredF32, Dollars, Dollars>,
|
||||
pub net_realized_pnl_rel_to_realized_cap:
|
||||
LazyBinaryFromHeightSumCum<StoredF32, Dollars, Dollars>,
|
||||
|
||||
// === Total Realized PnL ===
|
||||
pub total_realized_pnl: LazyFromHeightSum<Dollars>,
|
||||
pub realized_profit_to_loss_ratio: Option<EagerVec<PcoVec<DateIndex, StoredF64>>>,
|
||||
|
||||
// === Value Created/Destroyed ===
|
||||
pub value_created: ComputedFromHeightSum<Dollars>,
|
||||
pub value_destroyed: ComputedFromHeightSum<Dollars>,
|
||||
// === Value Created/Destroyed Splits (stored) ===
|
||||
pub profit_value_created: ComputedFromHeightSum<Dollars>,
|
||||
pub profit_value_destroyed: ComputedFromHeightSum<Dollars>,
|
||||
pub loss_value_created: ComputedFromHeightSum<Dollars>,
|
||||
pub loss_value_destroyed: ComputedFromHeightSum<Dollars>,
|
||||
|
||||
// === Value Created/Destroyed Totals (lazy: profit + loss) ===
|
||||
pub value_created: LazyBinaryFromHeightSum<Dollars, Dollars, Dollars>,
|
||||
pub value_destroyed: LazyBinaryFromHeightSum<Dollars, Dollars, Dollars>,
|
||||
|
||||
// === Capitulation/Profit Flow (lazy aliases) ===
|
||||
pub capitulation_flow: LazyFromHeightSum<Dollars>,
|
||||
pub profit_flow: LazyFromHeightSum<Dollars>,
|
||||
|
||||
// === Adjusted Value (lazy: cohort - up_to_1h) ===
|
||||
pub adjusted_value_created: Option<LazyBinaryFromHeightSum<Dollars, Dollars, Dollars>>,
|
||||
@@ -77,24 +109,50 @@ pub struct RealizedMetrics {
|
||||
pub net_realized_pnl_cumulative_30d_delta: ComputedFromDateLast<Dollars>,
|
||||
pub net_realized_pnl_cumulative_30d_delta_rel_to_realized_cap: ComputedFromDateLast<StoredF32>,
|
||||
pub net_realized_pnl_cumulative_30d_delta_rel_to_market_cap: ComputedFromDateLast<StoredF32>,
|
||||
|
||||
// === Peak Regret ===
|
||||
/// Realized peak regret: Σ((peak - sell_price) × sats)
|
||||
/// where peak = max price during holding period.
|
||||
/// "How much more could have been made by selling at peak instead"
|
||||
pub peak_regret: ComputedFromHeightSumCum<Dollars>,
|
||||
/// Peak regret as % of realized cap
|
||||
pub peak_regret_rel_to_realized_cap: LazyBinaryFromHeightSum<StoredF32, Dollars, Dollars>,
|
||||
|
||||
// === Sent in Profit/Loss ===
|
||||
/// Sats sent in profit (sats/btc/usd)
|
||||
pub sent_in_profit: LazyComputedValueFromHeightSumCum,
|
||||
/// 14-day EMA of sent in profit (sats, btc, usd)
|
||||
pub sent_in_profit_14d_ema: ValueFromDateLast,
|
||||
/// Sats sent in loss (sats/btc/usd)
|
||||
pub sent_in_loss: LazyComputedValueFromHeightSumCum,
|
||||
/// 14-day EMA of sent in loss (sats, btc, usd)
|
||||
pub sent_in_loss_14d_ema: ValueFromDateLast,
|
||||
}
|
||||
|
||||
impl RealizedMetrics {
|
||||
/// Import realized metrics from database.
|
||||
pub fn forced_import(cfg: &ImportConfig) -> Result<Self> {
|
||||
let v1 = Version::ONE;
|
||||
let v2 = Version::new(2);
|
||||
let v3 = Version::new(3);
|
||||
let extended = cfg.extended();
|
||||
let compute_adjusted = cfg.compute_adjusted();
|
||||
|
||||
// Import combined types using forced_import which handles height + derived
|
||||
let realized_cap = ComputedFromHeightLast::forced_import(
|
||||
let realized_cap_cents = ComputedFromHeightLast::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("realized_cap"),
|
||||
&cfg.name("realized_cap_cents"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
let realized_cap = LazyFromHeightLast::from_computed::<CentsUnsignedToDollars>(
|
||||
&cfg.name("realized_cap"),
|
||||
cfg.version,
|
||||
realized_cap_cents.height.boxed_clone(),
|
||||
&realized_cap_cents,
|
||||
);
|
||||
|
||||
let realized_profit = ComputedFromHeightSumCum::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("realized_profit"),
|
||||
@@ -102,6 +160,13 @@ impl RealizedMetrics {
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
let realized_profit_7d_ema = ComputedFromDateLast::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("realized_profit_7d_ema"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
let realized_loss = ComputedFromHeightSumCum::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("realized_loss"),
|
||||
@@ -109,6 +174,13 @@ impl RealizedMetrics {
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
let realized_loss_7d_ema = ComputedFromDateLast::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("realized_loss_7d_ema"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
let neg_realized_loss = LazyFromHeightSumCum::from_computed::<Negate>(
|
||||
&cfg.name("neg_realized_loss"),
|
||||
cfg.version + v1,
|
||||
@@ -123,6 +195,20 @@ impl RealizedMetrics {
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
let net_realized_pnl_7d_ema = ComputedFromDateLast::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("net_realized_pnl_7d_ema"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
let peak_regret = ComputedFromHeightSumCum::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("realized_peak_regret"),
|
||||
cfg.version + v2,
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
// realized_value is the source for total_realized_pnl (they're identical)
|
||||
let realized_value = ComputedFromHeightSum::forced_import(
|
||||
cfg.db,
|
||||
@@ -141,7 +227,7 @@ impl RealizedMetrics {
|
||||
|
||||
// Construct lazy ratio vecs
|
||||
let realized_profit_rel_to_realized_cap =
|
||||
LazyBinaryFromHeightSumCum::from_computed_last::<PercentageDollarsF32>(
|
||||
LazyBinaryFromHeightSumCum::from_computed_lazy_last::<PercentageDollarsF32, _>(
|
||||
&cfg.name("realized_profit_rel_to_realized_cap"),
|
||||
cfg.version + v1,
|
||||
realized_profit.height.boxed_clone(),
|
||||
@@ -151,7 +237,7 @@ impl RealizedMetrics {
|
||||
);
|
||||
|
||||
let realized_loss_rel_to_realized_cap =
|
||||
LazyBinaryFromHeightSumCum::from_computed_last::<PercentageDollarsF32>(
|
||||
LazyBinaryFromHeightSumCum::from_computed_lazy_last::<PercentageDollarsF32, _>(
|
||||
&cfg.name("realized_loss_rel_to_realized_cap"),
|
||||
cfg.version + v1,
|
||||
realized_loss.height.boxed_clone(),
|
||||
@@ -161,7 +247,7 @@ impl RealizedMetrics {
|
||||
);
|
||||
|
||||
let net_realized_pnl_rel_to_realized_cap =
|
||||
LazyBinaryFromHeightSumCum::from_computed_last::<PercentageDollarsF32>(
|
||||
LazyBinaryFromHeightSumCum::from_computed_lazy_last::<PercentageDollarsF32, _>(
|
||||
&cfg.name("net_realized_pnl_rel_to_realized_cap"),
|
||||
cfg.version + v1,
|
||||
net_realized_pnl.height.boxed_clone(),
|
||||
@@ -177,25 +263,104 @@ impl RealizedMetrics {
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
let value_created = ComputedFromHeightSum::forced_import(
|
||||
// Investor price (dollar-weighted average acquisition price)
|
||||
let investor_price_cents = ComputedFromHeightLast::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("value_created"),
|
||||
&cfg.name("investor_price_cents"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
let value_destroyed = ComputedFromHeightSum::forced_import(
|
||||
let investor_price = LazyPriceFromCents::from_computed(
|
||||
&cfg.name("investor_price"),
|
||||
cfg.version,
|
||||
&investor_price_cents,
|
||||
);
|
||||
|
||||
let investor_price_extra = ComputedFromDateRatio::forced_import_from_lazy(
|
||||
cfg.db,
|
||||
&cfg.name("value_destroyed"),
|
||||
&cfg.name("investor_price"),
|
||||
&investor_price.dollars,
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
extended,
|
||||
)?;
|
||||
|
||||
// Raw values for aggregation
|
||||
let cap_raw = BytesVec::forced_import(cfg.db, &cfg.name("cap_raw"), cfg.version)?;
|
||||
let investor_cap_raw =
|
||||
BytesVec::forced_import(cfg.db, &cfg.name("investor_cap_raw"), cfg.version)?;
|
||||
|
||||
// Import the 4 splits (stored)
|
||||
let profit_value_created = ComputedFromHeightSum::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("profit_value_created"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
let profit_value_destroyed = ComputedFromHeightSum::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("profit_value_destroyed"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
let loss_value_created = ComputedFromHeightSum::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("loss_value_created"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
let loss_value_destroyed = ComputedFromHeightSum::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("loss_value_destroyed"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
// Create lazy totals (profit + loss)
|
||||
let value_created = LazyBinaryFromHeightSum::from_computed::<DollarsPlus>(
|
||||
&cfg.name("value_created"),
|
||||
cfg.version,
|
||||
&profit_value_created,
|
||||
&loss_value_created,
|
||||
);
|
||||
|
||||
let value_destroyed = LazyBinaryFromHeightSum::from_computed::<DollarsPlus>(
|
||||
&cfg.name("value_destroyed"),
|
||||
cfg.version,
|
||||
&profit_value_destroyed,
|
||||
&loss_value_destroyed,
|
||||
);
|
||||
|
||||
// Create lazy aliases
|
||||
let capitulation_flow = LazyFromHeightSum::from_computed::<Ident>(
|
||||
&cfg.name("capitulation_flow"),
|
||||
cfg.version,
|
||||
loss_value_destroyed.height.boxed_clone(),
|
||||
&loss_value_destroyed,
|
||||
);
|
||||
|
||||
let profit_flow = LazyFromHeightSum::from_computed::<Ident>(
|
||||
&cfg.name("profit_flow"),
|
||||
cfg.version,
|
||||
profit_value_destroyed.height.boxed_clone(),
|
||||
&profit_value_destroyed,
|
||||
);
|
||||
|
||||
// Create lazy adjusted vecs if compute_adjusted and up_to_1h is available
|
||||
let adjusted_value_created =
|
||||
(compute_adjusted && cfg.up_to_1h_realized.is_some()).then(|| {
|
||||
let up_to_1h = cfg.up_to_1h_realized.unwrap();
|
||||
LazyBinaryFromHeightSum::from_computed::<DollarsMinus>(
|
||||
LazyBinaryFromHeightSum::from_binary::<
|
||||
DollarsMinus,
|
||||
Dollars,
|
||||
Dollars,
|
||||
Dollars,
|
||||
Dollars,
|
||||
>(
|
||||
&cfg.name("adjusted_value_created"),
|
||||
cfg.version,
|
||||
&value_created,
|
||||
@@ -205,7 +370,13 @@ impl RealizedMetrics {
|
||||
let adjusted_value_destroyed =
|
||||
(compute_adjusted && cfg.up_to_1h_realized.is_some()).then(|| {
|
||||
let up_to_1h = cfg.up_to_1h_realized.unwrap();
|
||||
LazyBinaryFromHeightSum::from_computed::<DollarsMinus>(
|
||||
LazyBinaryFromHeightSum::from_binary::<
|
||||
DollarsMinus,
|
||||
Dollars,
|
||||
Dollars,
|
||||
Dollars,
|
||||
Dollars,
|
||||
>(
|
||||
&cfg.name("adjusted_value_destroyed"),
|
||||
cfg.version,
|
||||
&value_destroyed,
|
||||
@@ -221,7 +392,6 @@ impl RealizedMetrics {
|
||||
cfg.version + v1,
|
||||
cfg.indexes,
|
||||
extended,
|
||||
cfg.price,
|
||||
)?;
|
||||
|
||||
// MVRV is a lazy proxy for realized_price_extra.ratio
|
||||
@@ -234,7 +404,8 @@ impl RealizedMetrics {
|
||||
|
||||
Ok(Self {
|
||||
// === Realized Cap ===
|
||||
realized_cap,
|
||||
realized_cap_cents,
|
||||
realized_cap: realized_cap.clone(),
|
||||
realized_price,
|
||||
realized_price_extra,
|
||||
realized_cap_rel_to_own_market_cap: extended
|
||||
@@ -254,14 +425,24 @@ impl RealizedMetrics {
|
||||
cfg.indexes,
|
||||
)?,
|
||||
|
||||
// === Investor Price ===
|
||||
investor_price_cents,
|
||||
investor_price,
|
||||
investor_price_extra,
|
||||
cap_raw,
|
||||
investor_cap_raw,
|
||||
|
||||
// === MVRV ===
|
||||
mvrv,
|
||||
|
||||
// === Realized Profit/Loss ===
|
||||
realized_profit,
|
||||
realized_profit_7d_ema,
|
||||
realized_loss,
|
||||
realized_loss_7d_ema,
|
||||
neg_realized_loss,
|
||||
net_realized_pnl,
|
||||
net_realized_pnl_7d_ema,
|
||||
realized_value,
|
||||
|
||||
// === Realized vs Realized Cap Ratios (lazy) ===
|
||||
@@ -281,17 +462,31 @@ impl RealizedMetrics {
|
||||
})
|
||||
.transpose()?,
|
||||
|
||||
// === Value Created/Destroyed ===
|
||||
// === Value Created/Destroyed Splits (stored) ===
|
||||
profit_value_created,
|
||||
profit_value_destroyed,
|
||||
loss_value_created,
|
||||
loss_value_destroyed,
|
||||
|
||||
// === Value Created/Destroyed Totals (lazy: profit + loss) ===
|
||||
value_created,
|
||||
value_destroyed,
|
||||
|
||||
// === Capitulation/Profit Flow (lazy aliases) ===
|
||||
capitulation_flow,
|
||||
profit_flow,
|
||||
|
||||
// === Adjusted Value (lazy: cohort - up_to_1h) ===
|
||||
adjusted_value_created,
|
||||
adjusted_value_destroyed,
|
||||
|
||||
// === SOPR ===
|
||||
sopr: EagerVec::forced_import(cfg.db, &cfg.name("sopr"), cfg.version + v1)?,
|
||||
sopr_7d_ema: EagerVec::forced_import(cfg.db, &cfg.name("sopr_7d_ema"), cfg.version + v1)?,
|
||||
sopr_7d_ema: EagerVec::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("sopr_7d_ema"),
|
||||
cfg.version + v1,
|
||||
)?,
|
||||
sopr_30d_ema: EagerVec::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("sopr_30d_ema"),
|
||||
@@ -359,6 +554,50 @@ impl RealizedMetrics {
|
||||
cfg.version + v3,
|
||||
cfg.indexes,
|
||||
)?,
|
||||
|
||||
// === ATH Regret ===
|
||||
peak_regret: peak_regret.clone(),
|
||||
peak_regret_rel_to_realized_cap: LazyBinaryFromHeightSum::from_sumcum_lazy_last::<
|
||||
PercentageDollarsF32,
|
||||
_,
|
||||
>(
|
||||
&cfg.name("peak_regret_rel_to_realized_cap"),
|
||||
cfg.version + v1,
|
||||
peak_regret.height.boxed_clone(),
|
||||
realized_cap.height.boxed_clone(),
|
||||
&peak_regret,
|
||||
&realized_cap,
|
||||
),
|
||||
|
||||
// === Sent in Profit/Loss ===
|
||||
sent_in_profit: LazyComputedValueFromHeightSumCum::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("sent_in_profit"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
cfg.price,
|
||||
)?,
|
||||
sent_in_profit_14d_ema: ValueFromDateLast::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("sent_in_profit_14d_ema"),
|
||||
cfg.version,
|
||||
cfg.compute_dollars(),
|
||||
cfg.indexes,
|
||||
)?,
|
||||
sent_in_loss: LazyComputedValueFromHeightSumCum::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("sent_in_loss"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
cfg.price,
|
||||
)?,
|
||||
sent_in_loss_14d_ema: ValueFromDateLast::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("sent_in_loss_14d_ema"),
|
||||
cfg.version,
|
||||
cfg.compute_dollars(),
|
||||
cfg.indexes,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -369,47 +608,88 @@ impl RealizedMetrics {
|
||||
.len()
|
||||
.min(self.realized_profit.height.len())
|
||||
.min(self.realized_loss.height.len())
|
||||
.min(self.value_created.height.len())
|
||||
.min(self.value_destroyed.height.len())
|
||||
.min(self.investor_price_cents.height.len())
|
||||
.min(self.cap_raw.len())
|
||||
.min(self.investor_cap_raw.len())
|
||||
.min(self.profit_value_created.height.len())
|
||||
.min(self.profit_value_destroyed.height.len())
|
||||
.min(self.loss_value_created.height.len())
|
||||
.min(self.loss_value_destroyed.height.len())
|
||||
.min(self.peak_regret.height.len())
|
||||
.min(self.sent_in_profit.sats.height.len())
|
||||
.min(self.sent_in_loss.sats.height.len())
|
||||
}
|
||||
|
||||
/// Push realized state values to height-indexed vectors.
|
||||
/// State values are CentsUnsigned (deterministic), converted to Dollars for storage.
|
||||
pub fn truncate_push(&mut self, height: Height, state: &RealizedState) -> Result<()> {
|
||||
self.realized_cap.height.truncate_push(height, state.cap)?;
|
||||
self.realized_cap_cents
|
||||
.height
|
||||
.truncate_push(height, state.cap())?;
|
||||
self.realized_profit
|
||||
.height
|
||||
.truncate_push(height, state.profit)?;
|
||||
.truncate_push(height, state.profit().to_dollars())?;
|
||||
self.realized_loss
|
||||
.height
|
||||
.truncate_push(height, state.loss)?;
|
||||
self.value_created
|
||||
.truncate_push(height, state.loss().to_dollars())?;
|
||||
self.investor_price_cents
|
||||
.height
|
||||
.truncate_push(height, state.value_created)?;
|
||||
self.value_destroyed
|
||||
.truncate_push(height, state.investor_price())?;
|
||||
// Push raw values for aggregation
|
||||
self.cap_raw.truncate_push(height, state.cap_raw())?;
|
||||
self.investor_cap_raw
|
||||
.truncate_push(height, state.investor_cap_raw())?;
|
||||
// Push the 4 splits (totals are derived lazily)
|
||||
self.profit_value_created
|
||||
.height
|
||||
.truncate_push(height, state.value_destroyed)?;
|
||||
.truncate_push(height, state.profit_value_created().to_dollars())?;
|
||||
self.profit_value_destroyed
|
||||
.height
|
||||
.truncate_push(height, state.profit_value_destroyed().to_dollars())?;
|
||||
self.loss_value_created
|
||||
.height
|
||||
.truncate_push(height, state.loss_value_created().to_dollars())?;
|
||||
self.loss_value_destroyed
|
||||
.height
|
||||
.truncate_push(height, state.loss_value_destroyed().to_dollars())?;
|
||||
// ATH regret
|
||||
self.peak_regret
|
||||
.height
|
||||
.truncate_push(height, state.peak_regret().to_dollars())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// Volume at profit/loss
|
||||
self.sent_in_profit
|
||||
.sats
|
||||
.height
|
||||
.truncate_push(height, state.sent_in_profit())?;
|
||||
self.sent_in_loss
|
||||
.sats
|
||||
.height
|
||||
.truncate_push(height, state.sent_in_loss())?;
|
||||
|
||||
/// Write height-indexed vectors to disk.
|
||||
pub fn write(&mut self) -> Result<()> {
|
||||
self.realized_cap.height.write()?;
|
||||
self.realized_profit.height.write()?;
|
||||
self.realized_loss.height.write()?;
|
||||
self.value_created.height.write()?;
|
||||
self.value_destroyed.height.write()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a parallel iterator over all vecs for parallel writing.
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
[
|
||||
&mut self.realized_cap.height as &mut dyn AnyStoredVec,
|
||||
vec![
|
||||
&mut self.realized_cap_cents.height as &mut dyn AnyStoredVec,
|
||||
&mut self.realized_profit.height,
|
||||
&mut self.realized_loss.height,
|
||||
&mut self.value_created.height,
|
||||
&mut self.value_destroyed.height,
|
||||
&mut self.investor_price_cents.height,
|
||||
// Raw values for aggregation
|
||||
&mut self.cap_raw as &mut dyn AnyStoredVec,
|
||||
&mut self.investor_cap_raw as &mut dyn AnyStoredVec,
|
||||
// The 4 splits (totals are derived lazily)
|
||||
&mut self.profit_value_created.height,
|
||||
&mut self.profit_value_destroyed.height,
|
||||
&mut self.loss_value_created.height,
|
||||
&mut self.loss_value_destroyed.height,
|
||||
// ATH regret
|
||||
&mut self.peak_regret.height,
|
||||
// Sent in profit/loss
|
||||
&mut self.sent_in_profit.sats.height,
|
||||
&mut self.sent_in_loss.sats.height,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
@@ -427,11 +707,11 @@ impl RealizedMetrics {
|
||||
others: &[&Self],
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.realized_cap.height.compute_sum_of_others(
|
||||
self.realized_cap_cents.height.compute_sum_of_others(
|
||||
starting_indexes.height,
|
||||
&others
|
||||
.iter()
|
||||
.map(|v| &v.realized_cap.height)
|
||||
.map(|v| &v.realized_cap_cents.height)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
@@ -451,19 +731,121 @@ impl RealizedMetrics {
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
self.value_created.height.compute_sum_of_others(
|
||||
|
||||
// Aggregate raw values for investor_price computation
|
||||
// (BytesVec doesn't have compute_sum_of_others, so we manually iterate)
|
||||
// Validate version for investor_price_cents (same pattern as compute_sum_of_others)
|
||||
let investor_price_dep_version = others
|
||||
.iter()
|
||||
.map(|o| o.investor_price_cents.height.version())
|
||||
.fold(vecdb::Version::ZERO, |acc, v| acc + v);
|
||||
self.investor_price_cents
|
||||
.height
|
||||
.validate_computed_version_or_reset(investor_price_dep_version)?;
|
||||
|
||||
let mut iters: Vec<_> = others
|
||||
.iter()
|
||||
.filter_map(|o| Some((o.cap_raw.iter().ok()?, o.investor_cap_raw.iter().ok()?)))
|
||||
.collect();
|
||||
|
||||
// Start from where the target vecs left off (handles fresh/reset vecs)
|
||||
let start = self
|
||||
.cap_raw
|
||||
.len()
|
||||
.min(self.investor_cap_raw.len())
|
||||
.min(self.investor_price_cents.height.len());
|
||||
// End at the minimum length across all source vecs
|
||||
let end = others.iter().map(|o| o.cap_raw.len()).min().unwrap_or(0);
|
||||
|
||||
for i in start..end {
|
||||
let height = Height::from(i);
|
||||
|
||||
let mut sum_cap = CentsSats::ZERO;
|
||||
let mut sum_investor_cap = CentsSquaredSats::ZERO;
|
||||
|
||||
for (cap_iter, investor_cap_iter) in &mut iters {
|
||||
sum_cap += cap_iter.get_unwrap(height);
|
||||
sum_investor_cap += investor_cap_iter.get_unwrap(height);
|
||||
}
|
||||
|
||||
self.cap_raw.truncate_push(height, sum_cap)?;
|
||||
self.investor_cap_raw
|
||||
.truncate_push(height, sum_investor_cap)?;
|
||||
|
||||
// Compute investor_price from aggregated raw values
|
||||
let investor_price = if sum_cap.inner() == 0 {
|
||||
CentsUnsigned::ZERO
|
||||
} else {
|
||||
CentsUnsigned::new((sum_investor_cap / sum_cap.inner()) as u64)
|
||||
};
|
||||
self.investor_price_cents
|
||||
.height
|
||||
.truncate_push(height, investor_price)?;
|
||||
}
|
||||
|
||||
// Write to persist computed_version (same pattern as compute_sum_of_others)
|
||||
{
|
||||
let _lock = exit.lock();
|
||||
self.investor_price_cents.height.write()?;
|
||||
}
|
||||
|
||||
// Aggregate the 4 splits (totals are derived lazily)
|
||||
self.profit_value_created.height.compute_sum_of_others(
|
||||
starting_indexes.height,
|
||||
&others
|
||||
.iter()
|
||||
.map(|v| &v.value_created.height)
|
||||
.map(|v| &v.profit_value_created.height)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
self.value_destroyed.height.compute_sum_of_others(
|
||||
self.profit_value_destroyed.height.compute_sum_of_others(
|
||||
starting_indexes.height,
|
||||
&others
|
||||
.iter()
|
||||
.map(|v| &v.value_destroyed.height)
|
||||
.map(|v| &v.profit_value_destroyed.height)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
self.loss_value_created.height.compute_sum_of_others(
|
||||
starting_indexes.height,
|
||||
&others
|
||||
.iter()
|
||||
.map(|v| &v.loss_value_created.height)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
self.loss_value_destroyed.height.compute_sum_of_others(
|
||||
starting_indexes.height,
|
||||
&others
|
||||
.iter()
|
||||
.map(|v| &v.loss_value_destroyed.height)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
// ATH regret
|
||||
self.peak_regret.height.compute_sum_of_others(
|
||||
starting_indexes.height,
|
||||
&others
|
||||
.iter()
|
||||
.map(|v| &v.peak_regret.height)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
|
||||
// Volume at profit/loss
|
||||
self.sent_in_profit.sats.height.compute_sum_of_others(
|
||||
starting_indexes.height,
|
||||
&others
|
||||
.iter()
|
||||
.map(|v| &v.sent_in_profit.sats.height)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
self.sent_in_loss.sats.height.compute_sum_of_others(
|
||||
starting_indexes.height,
|
||||
&others
|
||||
.iter()
|
||||
.map(|v| &v.sent_in_loss.sats.height)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
@@ -478,9 +860,14 @@ impl RealizedMetrics {
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.realized_cap.compute_rest(indexes, starting_indexes, exit)?;
|
||||
self.realized_profit.compute_rest(indexes, starting_indexes, exit)?;
|
||||
self.realized_loss.compute_rest(indexes, starting_indexes, exit)?;
|
||||
self.realized_cap_cents
|
||||
.compute_rest(indexes, starting_indexes, exit)?;
|
||||
self.realized_profit
|
||||
.compute_rest(indexes, starting_indexes, exit)?;
|
||||
self.realized_loss
|
||||
.compute_rest(indexes, starting_indexes, exit)?;
|
||||
self.investor_price_cents
|
||||
.compute_rest(indexes, starting_indexes, exit)?;
|
||||
|
||||
// net_realized_pnl = profit - loss
|
||||
self.net_realized_pnl
|
||||
@@ -508,8 +895,25 @@ impl RealizedMetrics {
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
self.value_created.compute_rest(indexes, starting_indexes, exit)?;
|
||||
self.value_destroyed.compute_rest(indexes, starting_indexes, exit)?;
|
||||
// Compute derived aggregations for the 4 splits
|
||||
// (value_created, value_destroyed, capitulation_flow, profit_flow are derived lazily)
|
||||
self.profit_value_created
|
||||
.compute_rest(indexes, starting_indexes, exit)?;
|
||||
self.profit_value_destroyed
|
||||
.compute_rest(indexes, starting_indexes, exit)?;
|
||||
self.loss_value_created
|
||||
.compute_rest(indexes, starting_indexes, exit)?;
|
||||
self.loss_value_destroyed
|
||||
.compute_rest(indexes, starting_indexes, exit)?;
|
||||
// ATH regret
|
||||
self.peak_regret
|
||||
.compute_rest(indexes, starting_indexes, exit)?;
|
||||
|
||||
// Volume at profit/loss
|
||||
self.sent_in_profit
|
||||
.compute_rest(indexes, starting_indexes, exit)?;
|
||||
self.sent_in_loss
|
||||
.compute_rest(indexes, starting_indexes, exit)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -545,6 +949,13 @@ impl RealizedMetrics {
|
||||
exit,
|
||||
Some(&self.realized_price.dateindex.0),
|
||||
)?;
|
||||
|
||||
self.investor_price_extra.compute_rest(
|
||||
price,
|
||||
starting_indexes,
|
||||
exit,
|
||||
Some(&self.investor_price.dateindex.0),
|
||||
)?;
|
||||
}
|
||||
|
||||
// realized_cap_30d_delta
|
||||
@@ -567,6 +978,52 @@ impl RealizedMetrics {
|
||||
exit,
|
||||
)?;
|
||||
|
||||
// 7d EMA of realized profit/loss
|
||||
self.realized_profit_7d_ema.compute_all(starting_indexes, exit, |v| {
|
||||
Ok(v.compute_ema(
|
||||
starting_indexes.dateindex,
|
||||
&self.realized_profit.dateindex.sum.0,
|
||||
7,
|
||||
exit,
|
||||
)?)
|
||||
})?;
|
||||
|
||||
self.realized_loss_7d_ema.compute_all(starting_indexes, exit, |v| {
|
||||
Ok(v.compute_ema(
|
||||
starting_indexes.dateindex,
|
||||
&self.realized_loss.dateindex.sum.0,
|
||||
7,
|
||||
exit,
|
||||
)?)
|
||||
})?;
|
||||
|
||||
self.net_realized_pnl_7d_ema.compute_all(starting_indexes, exit, |v| {
|
||||
Ok(v.compute_ema(
|
||||
starting_indexes.dateindex,
|
||||
&self.net_realized_pnl.dateindex.sum.0,
|
||||
7,
|
||||
exit,
|
||||
)?)
|
||||
})?;
|
||||
|
||||
// 14-day EMA of sent in profit (sats and dollars)
|
||||
self.sent_in_profit_14d_ema.compute_ema(
|
||||
starting_indexes.dateindex,
|
||||
&self.sent_in_profit.sats.dateindex.sum.0,
|
||||
self.sent_in_profit.dollars.as_ref().map(|d| &d.dateindex.sum.0),
|
||||
14,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
// 14-day EMA of sent in loss (sats and dollars)
|
||||
self.sent_in_loss_14d_ema.compute_ema(
|
||||
starting_indexes.dateindex,
|
||||
&self.sent_in_loss.sats.dateindex.sum.0,
|
||||
self.sent_in_loss.dollars.as_ref().map(|d| &d.dateindex.sum.0),
|
||||
14,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.sopr_7d_ema
|
||||
.compute_ema(starting_indexes.dateindex, &self.sopr, 7, exit)?;
|
||||
|
||||
@@ -613,8 +1070,12 @@ impl RealizedMetrics {
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.sell_side_risk_ratio_7d_ema
|
||||
.compute_ema(starting_indexes.dateindex, &self.sell_side_risk_ratio, 7, exit)?;
|
||||
self.sell_side_risk_ratio_7d_ema.compute_ema(
|
||||
starting_indexes.dateindex,
|
||||
&self.sell_side_risk_ratio,
|
||||
7,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.sell_side_risk_ratio_30d_ema.compute_ema(
|
||||
starting_indexes.dateindex,
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use brk_cohort::Filter;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Dollars, Sats, StoredF32, StoredF64, Version};
|
||||
use vecdb::IterableCloneableVec;
|
||||
|
||||
use crate::internal::{
|
||||
LazyBinaryFromHeightLast, LazyBinaryFromDateLast, NegPercentageDollarsF32, NegRatio32,
|
||||
PercentageDollarsF32, PercentageSatsF64, Ratio32,
|
||||
LazyBinaryFromDateLast, LazyBinaryFromHeightLast, NegPercentageDollarsF32,
|
||||
PercentageDollarsF32, PercentageSatsF64,
|
||||
};
|
||||
|
||||
use super::{ImportConfig, SupplyMetrics, UnrealizedMetrics};
|
||||
use super::{ImportConfig, RealizedMetrics, SupplyMetrics, UnrealizedMetrics};
|
||||
|
||||
/// Relative metrics comparing cohort values to global values.
|
||||
/// All `rel_to_` vecs are lazy - computed on-demand from their sources.
|
||||
@@ -58,6 +59,16 @@ pub struct RelativeMetrics {
|
||||
Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
|
||||
pub net_unrealized_pnl_rel_to_own_total_unrealized_pnl:
|
||||
Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
|
||||
|
||||
// === Invested Capital in Profit/Loss as % of Realized Cap ===
|
||||
pub invested_capital_in_profit_pct:
|
||||
Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
|
||||
pub invested_capital_in_loss_pct:
|
||||
Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
|
||||
|
||||
// === Unrealized Peak Regret Relative to Market Cap (date-only, lazy) ===
|
||||
pub unrealized_peak_regret_rel_to_market_cap:
|
||||
Option<LazyBinaryFromDateLast<StoredF32, Dollars, Dollars>>,
|
||||
}
|
||||
|
||||
impl RelativeMetrics {
|
||||
@@ -65,11 +76,13 @@ impl RelativeMetrics {
|
||||
///
|
||||
/// All `rel_to_` metrics are lazy - computed on-demand from their sources.
|
||||
/// `all_supply` provides global sources for `*_rel_to_market_cap` and `*_rel_to_circulating_supply`.
|
||||
/// `realized` provides realized_cap for invested capital percentage metrics.
|
||||
pub fn forced_import(
|
||||
cfg: &ImportConfig,
|
||||
unrealized: &UnrealizedMetrics,
|
||||
supply: &SupplyMetrics,
|
||||
all_supply: Option<&SupplyMetrics>,
|
||||
realized: Option<&RealizedMetrics>,
|
||||
) -> Result<Self> {
|
||||
let v1 = Version::ONE;
|
||||
let v2 = Version::new(2);
|
||||
@@ -86,6 +99,11 @@ impl RelativeMetrics {
|
||||
// Own market cap source
|
||||
let own_market_cap = supply.total.dollars.as_ref();
|
||||
|
||||
// For "all" cohort, own_market_cap IS the global market cap
|
||||
let market_cap = global_market_cap.or_else(|| {
|
||||
matches!(cfg.filter, Filter::All).then_some(own_market_cap).flatten()
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
// === Supply Relative to Circulating Supply (lazy from global supply) ===
|
||||
supply_rel_to_circulating_supply: (compute_rel_to_all
|
||||
@@ -181,7 +199,7 @@ impl RelativeMetrics {
|
||||
|
||||
// === Unrealized vs Market Cap (lazy from global market cap) ===
|
||||
unrealized_profit_rel_to_market_cap:
|
||||
global_market_cap.map(|mc| {
|
||||
market_cap.map(|mc| {
|
||||
LazyBinaryFromHeightLast::from_computed_height_date_and_lazy_binary_block_last::<
|
||||
PercentageDollarsF32,
|
||||
_,
|
||||
@@ -194,7 +212,7 @@ impl RelativeMetrics {
|
||||
)
|
||||
}),
|
||||
unrealized_loss_rel_to_market_cap:
|
||||
global_market_cap.map(|mc| {
|
||||
market_cap.map(|mc| {
|
||||
LazyBinaryFromHeightLast::from_computed_height_date_and_lazy_binary_block_last::<
|
||||
PercentageDollarsF32,
|
||||
_,
|
||||
@@ -206,7 +224,7 @@ impl RelativeMetrics {
|
||||
mc,
|
||||
)
|
||||
}),
|
||||
neg_unrealized_loss_rel_to_market_cap: global_market_cap.map(|mc| {
|
||||
neg_unrealized_loss_rel_to_market_cap: market_cap.map(|mc| {
|
||||
LazyBinaryFromHeightLast::from_computed_height_date_and_lazy_binary_block_last::<
|
||||
NegPercentageDollarsF32,
|
||||
_,
|
||||
@@ -218,7 +236,7 @@ impl RelativeMetrics {
|
||||
mc,
|
||||
)
|
||||
}),
|
||||
net_unrealized_pnl_rel_to_market_cap: global_market_cap.map(|mc| {
|
||||
net_unrealized_pnl_rel_to_market_cap: market_cap.map(|mc| {
|
||||
LazyBinaryFromHeightLast::from_binary_block_and_lazy_binary_block_last::<
|
||||
PercentageDollarsF32,
|
||||
_,
|
||||
@@ -234,7 +252,7 @@ impl RelativeMetrics {
|
||||
}),
|
||||
|
||||
// NUPL is a proxy for net_unrealized_pnl_rel_to_market_cap
|
||||
nupl: global_market_cap.map(|mc| {
|
||||
nupl: market_cap.map(|mc| {
|
||||
LazyBinaryFromHeightLast::from_binary_block_and_lazy_binary_block_last::<
|
||||
PercentageDollarsF32,
|
||||
_,
|
||||
@@ -319,37 +337,76 @@ impl RelativeMetrics {
|
||||
|
||||
// === Unrealized vs Own Total Unrealized PnL (lazy, optional) ===
|
||||
unrealized_profit_rel_to_own_total_unrealized_pnl: extended.then(|| {
|
||||
LazyBinaryFromHeightLast::from_computed_height_date_and_binary_block::<Ratio32, _, _>(
|
||||
LazyBinaryFromHeightLast::from_computed_height_date_and_binary_block::<PercentageDollarsF32, _, _>(
|
||||
&cfg.name("unrealized_profit_rel_to_own_total_unrealized_pnl"),
|
||||
cfg.version,
|
||||
cfg.version + v1,
|
||||
&unrealized.unrealized_profit,
|
||||
&unrealized.total_unrealized_pnl,
|
||||
)
|
||||
}),
|
||||
unrealized_loss_rel_to_own_total_unrealized_pnl: extended.then(|| {
|
||||
LazyBinaryFromHeightLast::from_computed_height_date_and_binary_block::<Ratio32, _, _>(
|
||||
LazyBinaryFromHeightLast::from_computed_height_date_and_binary_block::<PercentageDollarsF32, _, _>(
|
||||
&cfg.name("unrealized_loss_rel_to_own_total_unrealized_pnl"),
|
||||
cfg.version,
|
||||
cfg.version + v1,
|
||||
&unrealized.unrealized_loss,
|
||||
&unrealized.total_unrealized_pnl,
|
||||
)
|
||||
}),
|
||||
neg_unrealized_loss_rel_to_own_total_unrealized_pnl: extended.then(|| {
|
||||
LazyBinaryFromHeightLast::from_computed_height_date_and_binary_block::<NegRatio32, _, _>(
|
||||
LazyBinaryFromHeightLast::from_computed_height_date_and_binary_block::<NegPercentageDollarsF32, _, _>(
|
||||
&cfg.name("neg_unrealized_loss_rel_to_own_total_unrealized_pnl"),
|
||||
cfg.version,
|
||||
cfg.version + v1,
|
||||
&unrealized.unrealized_loss,
|
||||
&unrealized.total_unrealized_pnl,
|
||||
)
|
||||
}),
|
||||
net_unrealized_pnl_rel_to_own_total_unrealized_pnl: extended.then(|| {
|
||||
LazyBinaryFromHeightLast::from_both_binary_block::<Ratio32, _, _, _, _>(
|
||||
LazyBinaryFromHeightLast::from_both_binary_block::<PercentageDollarsF32, _, _, _, _>(
|
||||
&cfg.name("net_unrealized_pnl_rel_to_own_total_unrealized_pnl"),
|
||||
cfg.version + v1,
|
||||
cfg.version + v2,
|
||||
&unrealized.net_unrealized_pnl,
|
||||
&unrealized.total_unrealized_pnl,
|
||||
)
|
||||
}),
|
||||
|
||||
// === Invested Capital in Profit/Loss as % of Realized Cap ===
|
||||
invested_capital_in_profit_pct: realized.map(|r| {
|
||||
LazyBinaryFromHeightLast::from_computed_height_date_and_lazy_block_last::<
|
||||
PercentageDollarsF32,
|
||||
_,
|
||||
>(
|
||||
&cfg.name("invested_capital_in_profit_pct"),
|
||||
cfg.version,
|
||||
&unrealized.invested_capital_in_profit,
|
||||
&r.realized_cap,
|
||||
)
|
||||
}),
|
||||
invested_capital_in_loss_pct: realized.map(|r| {
|
||||
LazyBinaryFromHeightLast::from_computed_height_date_and_lazy_block_last::<
|
||||
PercentageDollarsF32,
|
||||
_,
|
||||
>(
|
||||
&cfg.name("invested_capital_in_loss_pct"),
|
||||
cfg.version,
|
||||
&unrealized.invested_capital_in_loss,
|
||||
&r.realized_cap,
|
||||
)
|
||||
}),
|
||||
|
||||
// === Peak Regret Relative to Market Cap (date-only, lazy) ===
|
||||
unrealized_peak_regret_rel_to_market_cap: unrealized
|
||||
.peak_regret
|
||||
.as_ref()
|
||||
.zip(market_cap)
|
||||
.map(|(pr, mc)| {
|
||||
LazyBinaryFromDateLast::from_computed_and_derived_last::<PercentageDollarsF32>(
|
||||
&cfg.name("unrealized_peak_regret_rel_to_market_cap"),
|
||||
cfg.version,
|
||||
pr,
|
||||
mc.rest.dateindex.boxed_clone(),
|
||||
&mc.rest.dates,
|
||||
)
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
HalfClosePriceTimesSats, HalveDollars, HalveSats, HalveSatsToBitcoin,
|
||||
LazyBinaryValueFromHeightLast, ValueFromHeightLast,
|
||||
LazyBinaryValueFromHeightLast, ValueChangeFromDate, ValueFromHeightLast,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,8 @@ use super::ImportConfig;
|
||||
pub struct SupplyMetrics {
|
||||
pub total: ValueFromHeightLast,
|
||||
pub halved: LazyBinaryValueFromHeightLast,
|
||||
/// 30-day change in supply (net position change) - sats, btc, usd
|
||||
pub _30d_change: ValueChangeFromDate,
|
||||
}
|
||||
|
||||
impl SupplyMetrics {
|
||||
@@ -41,9 +43,18 @@ impl SupplyMetrics {
|
||||
HalveDollars,
|
||||
>(&cfg.name("supply_halved"), &supply, cfg.price, cfg.version);
|
||||
|
||||
let _30d_change = ValueChangeFromDate::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("_30d_change"),
|
||||
cfg.version,
|
||||
cfg.compute_dollars(),
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
total: supply,
|
||||
halved: supply_halved,
|
||||
_30d_change,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -58,12 +69,6 @@ impl SupplyMetrics {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write height-indexed vectors to disk.
|
||||
pub fn write(&mut self) -> Result<()> {
|
||||
self.total.sats.height.write()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a parallel iterator over all vecs for parallel writing.
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
vec![&mut self.total.sats.height as &mut dyn AnyStoredVec].into_par_iter()
|
||||
@@ -100,6 +105,17 @@ impl SupplyMetrics {
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.total.compute_rest(indexes, starting_indexes, exit)
|
||||
self.total.compute_rest(indexes, starting_indexes, exit)?;
|
||||
|
||||
// 30-day change in supply
|
||||
self._30d_change.compute_change(
|
||||
starting_indexes.dateindex,
|
||||
&self.total.sats.dateindex.0,
|
||||
self.total.dollars.as_ref().map(|d| &d.dateindex.0),
|
||||
30,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{DateIndex, Dollars, Height};
|
||||
use brk_types::{CentsSats, CentsSquaredSats, CentsUnsigned, DateIndex, Dollars, Height, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, AnyVec, Exit, GenericStoredVec, Negate};
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, BytesVec, Exit, GenericStoredVec, ImportableVec, Negate,
|
||||
TypedVecIterator,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
ComputeIndexes,
|
||||
distribution::state::UnrealizedState,
|
||||
indexes,
|
||||
internal::{
|
||||
ComputedFromHeightAndDateLast, DollarsMinus, DollarsPlus, LazyBinaryFromHeightLast, LazyFromHeightLast,
|
||||
ValueFromHeightAndDateLast,
|
||||
ComputedFromDateLast, ComputedFromHeightAndDateLast, ComputedFromHeightLast, DollarsMinus,
|
||||
DollarsPlus, LazyBinaryFromHeightLast, LazyFromHeightLast, ValueFromHeightAndDateLast,
|
||||
},
|
||||
price,
|
||||
};
|
||||
|
||||
use super::ImportConfig;
|
||||
@@ -26,12 +31,40 @@ pub struct UnrealizedMetrics {
|
||||
pub unrealized_profit: ComputedFromHeightAndDateLast<Dollars>,
|
||||
pub unrealized_loss: ComputedFromHeightAndDateLast<Dollars>,
|
||||
|
||||
// === Invested Capital in Profit/Loss ===
|
||||
pub invested_capital_in_profit: ComputedFromHeightAndDateLast<Dollars>,
|
||||
pub invested_capital_in_loss: ComputedFromHeightAndDateLast<Dollars>,
|
||||
|
||||
// === Raw values for precise aggregation (used to compute pain/greed indices) ===
|
||||
/// Σ(price × sats) for UTXOs in profit (raw u128, no indexes)
|
||||
pub invested_capital_in_profit_raw: BytesVec<Height, CentsSats>,
|
||||
/// Σ(price × sats) for UTXOs in loss (raw u128, no indexes)
|
||||
pub invested_capital_in_loss_raw: BytesVec<Height, CentsSats>,
|
||||
/// Σ(price² × sats) for UTXOs in profit (raw u128, no indexes)
|
||||
pub investor_cap_in_profit_raw: BytesVec<Height, CentsSquaredSats>,
|
||||
/// Σ(price² × sats) for UTXOs in loss (raw u128, no indexes)
|
||||
pub investor_cap_in_loss_raw: BytesVec<Height, CentsSquaredSats>,
|
||||
|
||||
// === Pain/Greed Indices (computed in compute_rest from raw values + spot price) ===
|
||||
/// investor_price_of_losers - spot (average distance underwater, weighted by $)
|
||||
pub pain_index: ComputedFromHeightLast<Dollars>,
|
||||
/// spot - investor_price_of_winners (average distance in profit, weighted by $)
|
||||
pub greed_index: ComputedFromHeightLast<Dollars>,
|
||||
/// greed_index - pain_index (positive = greedy market, negative = painful market)
|
||||
pub net_sentiment: ComputedFromHeightLast<Dollars>,
|
||||
|
||||
// === Negated ===
|
||||
pub neg_unrealized_loss: LazyFromHeightLast<Dollars>,
|
||||
|
||||
// === Net and Total ===
|
||||
pub net_unrealized_pnl: LazyBinaryFromHeightLast<Dollars>,
|
||||
pub total_unrealized_pnl: LazyBinaryFromHeightLast<Dollars>,
|
||||
|
||||
// === Peak Regret (age_range cohorts only) ===
|
||||
/// Unrealized peak regret: sum of (peak_price - reference_price) × supply
|
||||
/// where reference_price = max(spot, cost_basis) and peak = max price during holding period.
|
||||
/// Only computed for age_range cohorts, then aggregated for overlapping cohorts.
|
||||
pub peak_regret: Option<ComputedFromDateLast<Dollars>>,
|
||||
}
|
||||
|
||||
impl UnrealizedMetrics {
|
||||
@@ -71,6 +104,56 @@ impl UnrealizedMetrics {
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
// === Invested Capital in Profit/Loss ===
|
||||
let invested_capital_in_profit = ComputedFromHeightAndDateLast::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("invested_capital_in_profit"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
)?;
|
||||
let invested_capital_in_loss = ComputedFromHeightAndDateLast::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("invested_capital_in_loss"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
// === Raw values for precise aggregation ===
|
||||
let invested_capital_in_profit_raw = BytesVec::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("invested_capital_in_profit_raw"),
|
||||
cfg.version,
|
||||
)?;
|
||||
let invested_capital_in_loss_raw = BytesVec::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("invested_capital_in_loss_raw"),
|
||||
cfg.version,
|
||||
)?;
|
||||
let investor_cap_in_profit_raw =
|
||||
BytesVec::forced_import(cfg.db, &cfg.name("investor_cap_in_profit_raw"), cfg.version)?;
|
||||
let investor_cap_in_loss_raw =
|
||||
BytesVec::forced_import(cfg.db, &cfg.name("investor_cap_in_loss_raw"), cfg.version)?;
|
||||
|
||||
// === Pain/Greed Indices ===
|
||||
let pain_index = ComputedFromHeightLast::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("pain_index"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
)?;
|
||||
let greed_index = ComputedFromHeightLast::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("greed_index"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
)?;
|
||||
let net_sentiment = ComputedFromHeightLast::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("net_sentiment"),
|
||||
cfg.version + Version::ONE, // v1: weighted average for aggregate cohorts
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
// === Negated ===
|
||||
let neg_unrealized_loss = LazyFromHeightLast::from_computed_height_date::<Negate>(
|
||||
&cfg.name("neg_unrealized_loss"),
|
||||
@@ -79,27 +162,52 @@ impl UnrealizedMetrics {
|
||||
);
|
||||
|
||||
// === Net and Total ===
|
||||
let net_unrealized_pnl = LazyBinaryFromHeightLast::from_computed_height_date_last::<DollarsMinus>(
|
||||
&cfg.name("net_unrealized_pnl"),
|
||||
cfg.version,
|
||||
&unrealized_profit,
|
||||
&unrealized_loss,
|
||||
);
|
||||
let total_unrealized_pnl = LazyBinaryFromHeightLast::from_computed_height_date_last::<DollarsPlus>(
|
||||
&cfg.name("total_unrealized_pnl"),
|
||||
cfg.version,
|
||||
&unrealized_profit,
|
||||
&unrealized_loss,
|
||||
);
|
||||
let net_unrealized_pnl =
|
||||
LazyBinaryFromHeightLast::from_computed_height_date_last::<DollarsMinus>(
|
||||
&cfg.name("net_unrealized_pnl"),
|
||||
cfg.version,
|
||||
&unrealized_profit,
|
||||
&unrealized_loss,
|
||||
);
|
||||
let total_unrealized_pnl =
|
||||
LazyBinaryFromHeightLast::from_computed_height_date_last::<DollarsPlus>(
|
||||
&cfg.name("total_unrealized_pnl"),
|
||||
cfg.version,
|
||||
&unrealized_profit,
|
||||
&unrealized_loss,
|
||||
);
|
||||
|
||||
// Peak regret: only for age-based UTXO cohorts
|
||||
let peak_regret = cfg
|
||||
.compute_peak_regret()
|
||||
.then(|| {
|
||||
ComputedFromDateLast::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("unrealized_peak_regret"),
|
||||
cfg.version,
|
||||
cfg.indexes,
|
||||
)
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
Ok(Self {
|
||||
supply_in_profit,
|
||||
supply_in_loss,
|
||||
unrealized_profit,
|
||||
unrealized_loss,
|
||||
invested_capital_in_profit,
|
||||
invested_capital_in_loss,
|
||||
invested_capital_in_profit_raw,
|
||||
invested_capital_in_loss_raw,
|
||||
investor_cap_in_profit_raw,
|
||||
investor_cap_in_loss_raw,
|
||||
pain_index,
|
||||
greed_index,
|
||||
net_sentiment,
|
||||
neg_unrealized_loss,
|
||||
net_unrealized_pnl,
|
||||
total_unrealized_pnl,
|
||||
peak_regret,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -111,17 +219,30 @@ impl UnrealizedMetrics {
|
||||
.min(self.supply_in_loss.height.len())
|
||||
.min(self.unrealized_profit.height.len())
|
||||
.min(self.unrealized_loss.height.len())
|
||||
.min(self.invested_capital_in_profit.height.len())
|
||||
.min(self.invested_capital_in_loss.height.len())
|
||||
.min(self.invested_capital_in_profit_raw.len())
|
||||
.min(self.invested_capital_in_loss_raw.len())
|
||||
.min(self.investor_cap_in_profit_raw.len())
|
||||
.min(self.investor_cap_in_loss_raw.len())
|
||||
}
|
||||
|
||||
/// Get minimum length across dateindex-indexed vectors written in block loop.
|
||||
pub fn min_stateful_dateindex_len(&self) -> usize {
|
||||
self.supply_in_profit
|
||||
let mut min = self
|
||||
.supply_in_profit
|
||||
.indexes
|
||||
.sats_dateindex
|
||||
.len()
|
||||
.min(self.supply_in_loss.indexes.sats_dateindex.len())
|
||||
.min(self.unrealized_profit.dateindex.len())
|
||||
.min(self.unrealized_loss.dateindex.len())
|
||||
.min(self.invested_capital_in_profit.dateindex.len())
|
||||
.min(self.invested_capital_in_loss.dateindex.len());
|
||||
if let Some(pr) = &self.peak_regret {
|
||||
min = min.min(pr.dateindex.len());
|
||||
}
|
||||
min
|
||||
}
|
||||
|
||||
/// Push unrealized state values to height-indexed vectors.
|
||||
@@ -140,10 +261,34 @@ impl UnrealizedMetrics {
|
||||
.truncate_push(height, height_state.supply_in_loss)?;
|
||||
self.unrealized_profit
|
||||
.height
|
||||
.truncate_push(height, height_state.unrealized_profit)?;
|
||||
.truncate_push(height, height_state.unrealized_profit.to_dollars())?;
|
||||
self.unrealized_loss
|
||||
.height
|
||||
.truncate_push(height, height_state.unrealized_loss)?;
|
||||
.truncate_push(height, height_state.unrealized_loss.to_dollars())?;
|
||||
self.invested_capital_in_profit
|
||||
.height
|
||||
.truncate_push(height, height_state.invested_capital_in_profit.to_dollars())?;
|
||||
self.invested_capital_in_loss
|
||||
.height
|
||||
.truncate_push(height, height_state.invested_capital_in_loss.to_dollars())?;
|
||||
|
||||
// Raw values for aggregation
|
||||
self.invested_capital_in_profit_raw.truncate_push(
|
||||
height,
|
||||
CentsSats::new(height_state.invested_capital_in_profit_raw),
|
||||
)?;
|
||||
self.invested_capital_in_loss_raw.truncate_push(
|
||||
height,
|
||||
CentsSats::new(height_state.invested_capital_in_loss_raw),
|
||||
)?;
|
||||
self.investor_cap_in_profit_raw.truncate_push(
|
||||
height,
|
||||
CentsSquaredSats::new(height_state.investor_cap_in_profit_raw),
|
||||
)?;
|
||||
self.investor_cap_in_loss_raw.truncate_push(
|
||||
height,
|
||||
CentsSquaredSats::new(height_state.investor_cap_in_loss_raw),
|
||||
)?;
|
||||
|
||||
if let (Some(dateindex), Some(date_state)) = (dateindex, date_state) {
|
||||
self.supply_in_profit
|
||||
@@ -156,41 +301,46 @@ impl UnrealizedMetrics {
|
||||
.truncate_push(dateindex, date_state.supply_in_loss)?;
|
||||
self.unrealized_profit
|
||||
.dateindex
|
||||
.truncate_push(dateindex, date_state.unrealized_profit)?;
|
||||
.truncate_push(dateindex, date_state.unrealized_profit.to_dollars())?;
|
||||
self.unrealized_loss
|
||||
.dateindex
|
||||
.truncate_push(dateindex, date_state.unrealized_loss)?;
|
||||
.truncate_push(dateindex, date_state.unrealized_loss.to_dollars())?;
|
||||
self.invested_capital_in_profit.dateindex.truncate_push(
|
||||
dateindex,
|
||||
date_state.invested_capital_in_profit.to_dollars(),
|
||||
)?;
|
||||
self.invested_capital_in_loss
|
||||
.dateindex
|
||||
.truncate_push(dateindex, date_state.invested_capital_in_loss.to_dollars())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write height-indexed vectors to disk.
|
||||
pub fn write(&mut self) -> Result<()> {
|
||||
self.supply_in_profit.height.write()?;
|
||||
self.supply_in_loss.height.write()?;
|
||||
self.unrealized_profit.height.write()?;
|
||||
self.unrealized_loss.height.write()?;
|
||||
self.supply_in_profit.indexes.sats_dateindex.write()?;
|
||||
self.supply_in_loss.indexes.sats_dateindex.write()?;
|
||||
self.unrealized_profit.dateindex.write()?;
|
||||
self.unrealized_loss.dateindex.write()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a parallel iterator over all vecs for parallel writing.
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
vec![
|
||||
&mut self.supply_in_profit.height as &mut dyn AnyStoredVec,
|
||||
&mut self.supply_in_loss.height as &mut dyn AnyStoredVec,
|
||||
&mut self.unrealized_profit.height as &mut dyn AnyStoredVec,
|
||||
&mut self.unrealized_loss.height as &mut dyn AnyStoredVec,
|
||||
&mut self.supply_in_profit.indexes.sats_dateindex as &mut dyn AnyStoredVec,
|
||||
&mut self.supply_in_loss.indexes.sats_dateindex as &mut dyn AnyStoredVec,
|
||||
&mut self.unrealized_profit.rest.dateindex as &mut dyn AnyStoredVec,
|
||||
&mut self.unrealized_loss.rest.dateindex as &mut dyn AnyStoredVec,
|
||||
]
|
||||
.into_par_iter()
|
||||
let mut vecs: Vec<&mut dyn AnyStoredVec> = vec![
|
||||
&mut self.supply_in_profit.height,
|
||||
&mut self.supply_in_loss.height,
|
||||
&mut self.unrealized_profit.height,
|
||||
&mut self.unrealized_loss.height,
|
||||
&mut self.invested_capital_in_profit.height,
|
||||
&mut self.invested_capital_in_loss.height,
|
||||
&mut self.invested_capital_in_profit_raw,
|
||||
&mut self.invested_capital_in_loss_raw,
|
||||
&mut self.investor_cap_in_profit_raw,
|
||||
&mut self.investor_cap_in_loss_raw,
|
||||
&mut self.supply_in_profit.indexes.sats_dateindex,
|
||||
&mut self.supply_in_loss.indexes.sats_dateindex,
|
||||
&mut self.unrealized_profit.rest.dateindex,
|
||||
&mut self.unrealized_loss.rest.dateindex,
|
||||
&mut self.invested_capital_in_profit.rest.dateindex,
|
||||
&mut self.invested_capital_in_loss.rest.dateindex,
|
||||
];
|
||||
if let Some(pr) = &mut self.peak_regret {
|
||||
vecs.push(&mut pr.dateindex);
|
||||
}
|
||||
vecs.into_par_iter()
|
||||
}
|
||||
|
||||
/// Compute aggregate values from separate cohorts.
|
||||
@@ -232,6 +382,78 @@ impl UnrealizedMetrics {
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
self.invested_capital_in_profit
|
||||
.height
|
||||
.compute_sum_of_others(
|
||||
starting_indexes.height,
|
||||
&others
|
||||
.iter()
|
||||
.map(|v| &v.invested_capital_in_profit.height)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
self.invested_capital_in_loss.height.compute_sum_of_others(
|
||||
starting_indexes.height,
|
||||
&others
|
||||
.iter()
|
||||
.map(|v| &v.invested_capital_in_loss.height)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
|
||||
// Raw values for aggregation - manually sum since BytesVec doesn't have compute_sum_of_others
|
||||
// Create iterators for each source vec
|
||||
let mut iters: Vec<_> = others
|
||||
.iter()
|
||||
.filter_map(|o| {
|
||||
Some((
|
||||
o.invested_capital_in_profit_raw.iter().ok()?,
|
||||
o.invested_capital_in_loss_raw.iter().ok()?,
|
||||
o.investor_cap_in_profit_raw.iter().ok()?,
|
||||
o.investor_cap_in_loss_raw.iter().ok()?,
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Start from where the target vecs left off (handles fresh/reset vecs)
|
||||
let start = self
|
||||
.invested_capital_in_profit_raw
|
||||
.len()
|
||||
.min(self.invested_capital_in_loss_raw.len())
|
||||
.min(self.investor_cap_in_profit_raw.len())
|
||||
.min(self.investor_cap_in_loss_raw.len());
|
||||
// End at the minimum length across all source vecs
|
||||
let end = others
|
||||
.iter()
|
||||
.map(|o| o.invested_capital_in_profit_raw.len())
|
||||
.min()
|
||||
.unwrap_or(0);
|
||||
|
||||
for i in start..end {
|
||||
let height = Height::from(i);
|
||||
|
||||
let mut sum_invested_profit = CentsSats::ZERO;
|
||||
let mut sum_invested_loss = CentsSats::ZERO;
|
||||
let mut sum_investor_profit = CentsSquaredSats::ZERO;
|
||||
let mut sum_investor_loss = CentsSquaredSats::ZERO;
|
||||
|
||||
for (ip_iter, il_iter, cap_p_iter, cap_l_iter) in &mut iters {
|
||||
sum_invested_profit += ip_iter.get_unwrap(height);
|
||||
sum_invested_loss += il_iter.get_unwrap(height);
|
||||
sum_investor_profit += cap_p_iter.get_unwrap(height);
|
||||
sum_investor_loss += cap_l_iter.get_unwrap(height);
|
||||
}
|
||||
|
||||
self.invested_capital_in_profit_raw
|
||||
.truncate_push(height, sum_invested_profit)?;
|
||||
self.invested_capital_in_loss_raw
|
||||
.truncate_push(height, sum_invested_loss)?;
|
||||
self.investor_cap_in_profit_raw
|
||||
.truncate_push(height, sum_investor_profit)?;
|
||||
self.investor_cap_in_loss_raw
|
||||
.truncate_push(height, sum_investor_loss)?;
|
||||
}
|
||||
|
||||
self.supply_in_profit
|
||||
.indexes
|
||||
.sats_dateindex
|
||||
@@ -270,13 +492,50 @@ impl UnrealizedMetrics {
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
self.invested_capital_in_profit
|
||||
.dateindex
|
||||
.compute_sum_of_others(
|
||||
starting_indexes.dateindex,
|
||||
&others
|
||||
.iter()
|
||||
.map(|v| &v.invested_capital_in_profit.dateindex)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
self.invested_capital_in_loss
|
||||
.dateindex
|
||||
.compute_sum_of_others(
|
||||
starting_indexes.dateindex,
|
||||
&others
|
||||
.iter()
|
||||
.map(|v| &v.invested_capital_in_loss.dateindex)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
|
||||
// Peak regret aggregation (only if this cohort has peak_regret)
|
||||
if let Some(pr) = &mut self.peak_regret {
|
||||
let other_prs: Vec<_> = others.iter().filter_map(|v| v.peak_regret.as_ref()).collect();
|
||||
if !other_prs.is_empty() {
|
||||
pr.dateindex.compute_sum_of_others(
|
||||
starting_indexes.dateindex,
|
||||
&other_prs
|
||||
.iter()
|
||||
.map(|v| &v.dateindex)
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// First phase of computed metrics.
|
||||
pub fn compute_rest_part1(
|
||||
/// Compute derived metrics from stored values + price.
|
||||
pub fn compute_rest(
|
||||
&mut self,
|
||||
price: Option<&crate::price::Vecs>,
|
||||
indexes: &indexes::Vecs,
|
||||
price: Option<&price::Vecs>,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
@@ -286,6 +545,85 @@ impl UnrealizedMetrics {
|
||||
self.supply_in_loss
|
||||
.compute_dollars_from_price(price, starting_indexes, exit)?;
|
||||
|
||||
// Compute pain/greed/net from raw values + spot price
|
||||
let Some(price) = price else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Pain index: investor_price_of_losers - spot
|
||||
self.pain_index
|
||||
.compute_all(indexes, starting_indexes, exit, |vec| {
|
||||
Ok(vec.compute_transform3(
|
||||
starting_indexes.height,
|
||||
&self.investor_cap_in_loss_raw,
|
||||
&self.invested_capital_in_loss_raw,
|
||||
&price.cents.split.height.close,
|
||||
|(h, investor_cap, invested_cap, spot, ..)| {
|
||||
if invested_cap.inner() == 0 {
|
||||
return (h, Dollars::ZERO);
|
||||
}
|
||||
let investor_price_losers = investor_cap.inner() / invested_cap.inner();
|
||||
let spot_u128 = (*spot).as_u128();
|
||||
(
|
||||
h,
|
||||
CentsUnsigned::new((investor_price_losers - spot_u128) as u64)
|
||||
.to_dollars(),
|
||||
)
|
||||
},
|
||||
exit,
|
||||
)?)
|
||||
})?;
|
||||
|
||||
// Greed index: spot - investor_price_of_winners
|
||||
self.greed_index
|
||||
.compute_all(indexes, starting_indexes, exit, |vec| {
|
||||
Ok(vec.compute_transform3(
|
||||
starting_indexes.height,
|
||||
&self.investor_cap_in_profit_raw,
|
||||
&self.invested_capital_in_profit_raw,
|
||||
&price.cents.split.height.close,
|
||||
|(h, investor_cap, invested_cap, spot, ..)| {
|
||||
if invested_cap.inner() == 0 {
|
||||
return (h, Dollars::ZERO);
|
||||
}
|
||||
let investor_price_winners = investor_cap.inner() / invested_cap.inner();
|
||||
let spot_u128 = (*spot).as_u128();
|
||||
(
|
||||
h,
|
||||
CentsUnsigned::new((spot_u128 - investor_price_winners) as u64)
|
||||
.to_dollars(),
|
||||
)
|
||||
},
|
||||
exit,
|
||||
)?)
|
||||
})?;
|
||||
|
||||
// Net sentiment height (greed - pain) computed separately for separate cohorts only
|
||||
// Aggregate cohorts compute it via weighted average in compute_from_stateful
|
||||
// Dateindex derivation for ALL cohorts happens in compute_net_sentiment_rest
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute net_sentiment.height for separate cohorts (greed - pain).
|
||||
/// Aggregate cohorts skip this - their height is computed via weighted average in compute_from_stateful.
|
||||
pub fn compute_net_sentiment_height(&mut self, starting_indexes: &ComputeIndexes, exit: &Exit) -> Result<()> {
|
||||
Ok(self.net_sentiment.height.compute_subtract(
|
||||
starting_indexes.height,
|
||||
&self.greed_index.height,
|
||||
&self.pain_index.height,
|
||||
exit,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Compute net_sentiment dateindex derivation from height.
|
||||
/// Called for ALL cohorts after height is computed (either via greed-pain or weighted avg).
|
||||
pub fn compute_net_sentiment_rest(
|
||||
&mut self,
|
||||
indexes: &indexes::Vecs,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.net_sentiment.compute_rest(indexes, starting_indexes, exit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::ops::{Add, AddAssign, SubAssign};
|
||||
|
||||
use brk_types::{Dollars, SupplyState, Timestamp};
|
||||
use brk_types::{CentsUnsigned, SupplyState, Timestamp};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@@ -8,7 +8,7 @@ pub struct BlockState {
|
||||
#[serde(flatten)]
|
||||
pub supply: SupplyState,
|
||||
#[serde(skip)]
|
||||
pub price: Option<Dollars>,
|
||||
pub price: Option<CentsUnsigned>,
|
||||
#[serde(skip)]
|
||||
pub timestamp: Timestamp,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::Path;
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_types::{Age, Dollars, Height, LoadedAddressData, Sats, SupplyState};
|
||||
use brk_types::{Age, CentsUnsigned, FundedAddressData, Height, Sats, SupplyState};
|
||||
use vecdb::unlikely;
|
||||
|
||||
use super::{super::cost_basis::RealizedState, base::CohortState};
|
||||
@@ -28,12 +28,12 @@ impl AddressCohortState {
|
||||
self.inner.satblocks_destroyed = Sats::ZERO;
|
||||
self.inner.satdays_destroyed = Sats::ZERO;
|
||||
if let Some(realized) = self.inner.realized.as_mut() {
|
||||
*realized = RealizedState::NAN;
|
||||
*realized = RealizedState::default();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_price_to_amount_if_needed(&mut self) -> Result<()> {
|
||||
self.inner.reset_price_to_amount_if_needed()
|
||||
pub fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> {
|
||||
self.inner.reset_cost_basis_data_if_needed()
|
||||
}
|
||||
|
||||
pub fn reset_single_iteration_values(&mut self) {
|
||||
@@ -42,37 +42,28 @@ impl AddressCohortState {
|
||||
|
||||
pub fn send(
|
||||
&mut self,
|
||||
addressdata: &mut LoadedAddressData,
|
||||
addressdata: &mut FundedAddressData,
|
||||
value: Sats,
|
||||
current_price: Option<Dollars>,
|
||||
prev_price: Option<Dollars>,
|
||||
current_price: CentsUnsigned,
|
||||
prev_price: CentsUnsigned,
|
||||
ath: CentsUnsigned,
|
||||
age: Age,
|
||||
) -> Result<()> {
|
||||
let compute_price = current_price.is_some();
|
||||
let prev = addressdata.cost_basis_snapshot();
|
||||
addressdata.send(value, Some(prev_price))?;
|
||||
let current = addressdata.cost_basis_snapshot();
|
||||
|
||||
let prev_realized_price = compute_price.then(|| addressdata.realized_price());
|
||||
let prev_supply_state = SupplyState {
|
||||
utxo_count: addressdata.utxo_count() as u64,
|
||||
value: addressdata.balance(),
|
||||
};
|
||||
|
||||
addressdata.send(value, prev_price)?;
|
||||
|
||||
let supply_state = SupplyState {
|
||||
utxo_count: addressdata.utxo_count() as u64,
|
||||
value: addressdata.balance(),
|
||||
};
|
||||
|
||||
self.inner.send_(
|
||||
self.inner.send_address(
|
||||
&SupplyState {
|
||||
utxo_count: 1,
|
||||
value,
|
||||
},
|
||||
current_price,
|
||||
prev_price,
|
||||
ath,
|
||||
age,
|
||||
compute_price.then(|| (addressdata.realized_price(), &supply_state)),
|
||||
prev_realized_price.map(|prev_price| (prev_price, &prev_supply_state)),
|
||||
¤t,
|
||||
&prev,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -80,61 +71,46 @@ impl AddressCohortState {
|
||||
|
||||
pub fn receive(
|
||||
&mut self,
|
||||
address_data: &mut LoadedAddressData,
|
||||
address_data: &mut FundedAddressData,
|
||||
value: Sats,
|
||||
price: Option<Dollars>,
|
||||
price: CentsUnsigned,
|
||||
) {
|
||||
self.receive_outputs(address_data, value, price, 1);
|
||||
}
|
||||
|
||||
pub fn receive_outputs(
|
||||
&mut self,
|
||||
address_data: &mut LoadedAddressData,
|
||||
address_data: &mut FundedAddressData,
|
||||
value: Sats,
|
||||
price: Option<Dollars>,
|
||||
price: CentsUnsigned,
|
||||
output_count: u32,
|
||||
) {
|
||||
let compute_price = price.is_some();
|
||||
let prev = address_data.cost_basis_snapshot();
|
||||
address_data.receive_outputs(value, Some(price), output_count);
|
||||
let current = address_data.cost_basis_snapshot();
|
||||
|
||||
let prev_realized_price = compute_price.then(|| address_data.realized_price());
|
||||
let prev_supply_state = SupplyState {
|
||||
utxo_count: address_data.utxo_count() as u64,
|
||||
value: address_data.balance(),
|
||||
};
|
||||
|
||||
address_data.receive_outputs(value, price, output_count);
|
||||
|
||||
let supply_state = SupplyState {
|
||||
utxo_count: address_data.utxo_count() as u64,
|
||||
value: address_data.balance(),
|
||||
};
|
||||
|
||||
self.inner.receive_(
|
||||
self.inner.receive_address(
|
||||
&SupplyState {
|
||||
utxo_count: output_count as u64,
|
||||
value,
|
||||
},
|
||||
price,
|
||||
compute_price.then(|| (address_data.realized_price(), &supply_state)),
|
||||
prev_realized_price.map(|prev_price| (prev_price, &prev_supply_state)),
|
||||
¤t,
|
||||
&prev,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn add(&mut self, addressdata: &LoadedAddressData) {
|
||||
pub fn add(&mut self, addressdata: &FundedAddressData) {
|
||||
self.addr_count += 1;
|
||||
self.inner.increment_(
|
||||
&addressdata.into(),
|
||||
addressdata.realized_cap,
|
||||
addressdata.realized_price(),
|
||||
);
|
||||
self.inner
|
||||
.increment_snapshot(&addressdata.cost_basis_snapshot());
|
||||
}
|
||||
|
||||
pub fn subtract(&mut self, addressdata: &LoadedAddressData) {
|
||||
let addr_supply: SupplyState = addressdata.into();
|
||||
let realized_price = addressdata.realized_price();
|
||||
pub fn subtract(&mut self, addressdata: &FundedAddressData) {
|
||||
let snapshot = addressdata.cost_basis_snapshot();
|
||||
|
||||
// Check for potential underflow before it happens
|
||||
if unlikely(self.inner.supply.utxo_count < addr_supply.utxo_count) {
|
||||
if unlikely(self.inner.supply.utxo_count < snapshot.supply_state.utxo_count) {
|
||||
panic!(
|
||||
"AddressCohortState::subtract underflow!\n\
|
||||
Cohort state: addr_count={}, supply={}\n\
|
||||
@@ -142,10 +118,14 @@ impl AddressCohortState {
|
||||
Address supply: {}\n\
|
||||
Realized price: {}\n\
|
||||
This means the address is not properly tracked in this cohort.",
|
||||
self.addr_count, self.inner.supply, addressdata, addr_supply, realized_price
|
||||
self.addr_count,
|
||||
self.inner.supply,
|
||||
addressdata,
|
||||
snapshot.supply_state,
|
||||
snapshot.realized_price
|
||||
);
|
||||
}
|
||||
if unlikely(self.inner.supply.value < addr_supply.value) {
|
||||
if unlikely(self.inner.supply.value < snapshot.supply_state.value) {
|
||||
panic!(
|
||||
"AddressCohortState::subtract value underflow!\n\
|
||||
Cohort state: addr_count={}, supply={}\n\
|
||||
@@ -153,7 +133,11 @@ impl AddressCohortState {
|
||||
Address supply: {}\n\
|
||||
Realized price: {}\n\
|
||||
This means the address is not properly tracked in this cohort.",
|
||||
self.addr_count, self.inner.supply, addressdata, addr_supply, realized_price
|
||||
self.addr_count,
|
||||
self.inner.supply,
|
||||
addressdata,
|
||||
snapshot.supply_state,
|
||||
snapshot.realized_price
|
||||
);
|
||||
}
|
||||
|
||||
@@ -162,12 +146,11 @@ impl AddressCohortState {
|
||||
"AddressCohortState::subtract addr_count underflow! addr_count=0\n\
|
||||
Address being subtracted: {}\n\
|
||||
Realized price: {}",
|
||||
addressdata, realized_price
|
||||
addressdata, snapshot.realized_price
|
||||
)
|
||||
});
|
||||
|
||||
self.inner
|
||||
.decrement_(&addr_supply, addressdata.realized_cap, realized_price);
|
||||
self.inner.decrement_snapshot(&snapshot);
|
||||
}
|
||||
|
||||
pub fn write(&mut self, height: Height, cleanup: bool) -> Result<()> {
|
||||
|
||||
@@ -1,93 +1,78 @@
|
||||
use std::path::Path;
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_types::{Age, Dollars, Height, Sats, SupplyState};
|
||||
|
||||
use crate::internal::PERCENTILES_LEN;
|
||||
use brk_types::{Age, CentsSats, CentsUnsigned, CostBasisSnapshot, Height, Sats, SupplyState};
|
||||
|
||||
use super::super::cost_basis::{
|
||||
CachedUnrealizedState, PriceToAmount, RealizedState, UnrealizedState,
|
||||
CachedUnrealizedState, Percentiles, CostBasisData, RealizedState, UnrealizedState,
|
||||
};
|
||||
|
||||
/// State tracked for each cohort during computation.
|
||||
#[derive(Clone)]
|
||||
pub struct CohortState {
|
||||
/// Current supply in this cohort
|
||||
pub supply: SupplyState,
|
||||
|
||||
/// Realized cap and profit/loss (requires price data)
|
||||
pub realized: Option<RealizedState>,
|
||||
|
||||
/// Amount sent in current block
|
||||
pub sent: Sats,
|
||||
|
||||
/// Satoshi-blocks destroyed (supply * blocks_old when spent)
|
||||
pub satblocks_destroyed: Sats,
|
||||
|
||||
/// Satoshi-days destroyed (supply * days_old when spent)
|
||||
pub satdays_destroyed: Sats,
|
||||
|
||||
/// Price distribution for percentile calculations (requires price data)
|
||||
price_to_amount: Option<PriceToAmount>,
|
||||
|
||||
/// Cached unrealized state for O(k) incremental updates.
|
||||
cost_basis_data: Option<CostBasisData>,
|
||||
cached_unrealized: Option<CachedUnrealizedState>,
|
||||
}
|
||||
|
||||
impl CohortState {
|
||||
/// Create new cohort state.
|
||||
pub fn new(path: &Path, name: &str, compute_dollars: bool) -> Self {
|
||||
Self {
|
||||
supply: SupplyState::default(),
|
||||
realized: compute_dollars.then_some(RealizedState::NAN),
|
||||
realized: compute_dollars.then_some(RealizedState::default()),
|
||||
sent: Sats::ZERO,
|
||||
satblocks_destroyed: Sats::ZERO,
|
||||
satdays_destroyed: Sats::ZERO,
|
||||
price_to_amount: compute_dollars.then_some(PriceToAmount::create(path, name)),
|
||||
cost_basis_data: compute_dollars.then_some(CostBasisData::create(path, name)),
|
||||
cached_unrealized: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Import state from checkpoint.
|
||||
pub fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
|
||||
// Invalidate cache when importing new data
|
||||
self.cached_unrealized = None;
|
||||
|
||||
match self.price_to_amount.as_mut() {
|
||||
match self.cost_basis_data.as_mut() {
|
||||
Some(p) => p.import_at_or_before(height),
|
||||
None => Ok(height),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset price_to_amount if needed (for starting fresh).
|
||||
pub fn reset_price_to_amount_if_needed(&mut self) -> Result<()> {
|
||||
if let Some(p) = self.price_to_amount.as_mut() {
|
||||
/// Restore realized cap from cost_basis_data after import.
|
||||
/// Uses the exact persisted values instead of recomputing from the map.
|
||||
pub fn restore_realized_cap(&mut self) {
|
||||
if let Some(cost_basis_data) = self.cost_basis_data.as_ref()
|
||||
&& let Some(realized) = self.realized.as_mut()
|
||||
{
|
||||
realized.set_cap_raw(cost_basis_data.cap_raw());
|
||||
realized.set_investor_cap_raw(cost_basis_data.investor_cap_raw());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> {
|
||||
if let Some(p) = self.cost_basis_data.as_mut() {
|
||||
p.clean()?;
|
||||
p.init();
|
||||
}
|
||||
// Invalidate cache when data is reset
|
||||
self.cached_unrealized = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply pending price_to_amount updates. Must be called before reads.
|
||||
pub fn apply_pending(&mut self) {
|
||||
if let Some(p) = self.price_to_amount.as_mut() {
|
||||
if let Some(p) = self.cost_basis_data.as_mut() {
|
||||
p.apply_pending();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get first (lowest) price entry in distribution.
|
||||
pub fn price_to_amount_first_key_value(&self) -> Option<(Dollars, &Sats)> {
|
||||
self.price_to_amount.as_ref()?.first_key_value()
|
||||
pub fn cost_basis_data_first_key_value(&self) -> Option<(CentsUnsigned, &Sats)> {
|
||||
self.cost_basis_data.as_ref()?.first_key_value().map(|(k, v)| (k.into(), v))
|
||||
}
|
||||
|
||||
/// Get last (highest) price entry in distribution.
|
||||
pub fn price_to_amount_last_key_value(&self) -> Option<(Dollars, &Sats)> {
|
||||
self.price_to_amount.as_ref()?.last_key_value()
|
||||
pub fn cost_basis_data_last_key_value(&self) -> Option<(CentsUnsigned, &Sats)> {
|
||||
self.cost_basis_data.as_ref()?.last_key_value().map(|(k, v)| (k.into(), v))
|
||||
}
|
||||
|
||||
/// Reset per-block values before processing next block.
|
||||
pub fn reset_single_iteration_values(&mut self) {
|
||||
self.sent = Sats::ZERO;
|
||||
self.satdays_destroyed = Sats::ZERO;
|
||||
@@ -97,177 +82,137 @@ impl CohortState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Add supply to this cohort (e.g., when UTXO ages into cohort).
|
||||
pub fn increment(&mut self, supply: &SupplyState, price: Option<Dollars>) {
|
||||
pub fn increment(&mut self, supply: &SupplyState, price: Option<CentsUnsigned>) {
|
||||
match price {
|
||||
Some(p) => self.increment_snapshot(&CostBasisSnapshot::from_utxo(p, supply)),
|
||||
None => self.supply += supply,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn increment_snapshot(&mut self, s: &CostBasisSnapshot) {
|
||||
self.supply += &s.supply_state;
|
||||
|
||||
if s.supply_state.value > Sats::ZERO
|
||||
&& let Some(realized) = self.realized.as_mut()
|
||||
{
|
||||
realized.increment_snapshot(s.price_sats, s.investor_cap);
|
||||
self.cost_basis_data.as_mut().unwrap().increment(
|
||||
s.realized_price,
|
||||
s.supply_state.value,
|
||||
s.price_sats,
|
||||
s.investor_cap,
|
||||
);
|
||||
|
||||
if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.on_receive(s.realized_price, s.supply_state.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrement(&mut self, supply: &SupplyState, price: Option<CentsUnsigned>) {
|
||||
match price {
|
||||
Some(p) => self.decrement_snapshot(&CostBasisSnapshot::from_utxo(p, supply)),
|
||||
None => self.supply -= supply,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrement_snapshot(&mut self, s: &CostBasisSnapshot) {
|
||||
self.supply -= &s.supply_state;
|
||||
|
||||
if s.supply_state.value > Sats::ZERO
|
||||
&& let Some(realized) = self.realized.as_mut()
|
||||
{
|
||||
realized.decrement_snapshot(s.price_sats, s.investor_cap);
|
||||
self.cost_basis_data.as_mut().unwrap().decrement(
|
||||
s.realized_price,
|
||||
s.supply_state.value,
|
||||
s.price_sats,
|
||||
s.investor_cap,
|
||||
);
|
||||
|
||||
if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.on_send(s.realized_price, s.supply_state.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn receive_utxo(&mut self, supply: &SupplyState, price: Option<CentsUnsigned>) {
|
||||
self.supply += supply;
|
||||
|
||||
if supply.value > Sats::ZERO
|
||||
&& let Some(realized) = self.realized.as_mut()
|
||||
{
|
||||
let price = price.unwrap();
|
||||
realized.increment(supply, price);
|
||||
self.price_to_amount
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.increment(price, supply);
|
||||
let sats = supply.value;
|
||||
|
||||
// Compute once using typed values
|
||||
let price_sats = CentsSats::from_price_sats(price, sats);
|
||||
let investor_cap = price_sats.to_investor_cap(price);
|
||||
|
||||
realized.receive(price, sats);
|
||||
|
||||
self.cost_basis_data.as_mut().unwrap().increment(
|
||||
price,
|
||||
sats,
|
||||
price_sats,
|
||||
investor_cap,
|
||||
);
|
||||
|
||||
// Update cache for added supply
|
||||
if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.on_receive(price, supply.value);
|
||||
cache.on_receive(price, sats);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add supply with pre-computed realized cap (for address cohorts).
|
||||
pub fn increment_(
|
||||
pub fn receive_address(
|
||||
&mut self,
|
||||
supply: &SupplyState,
|
||||
realized_cap: Dollars,
|
||||
realized_price: Dollars,
|
||||
price: CentsUnsigned,
|
||||
current: &CostBasisSnapshot,
|
||||
prev: &CostBasisSnapshot,
|
||||
) {
|
||||
self.supply += supply;
|
||||
|
||||
if supply.value > Sats::ZERO
|
||||
&& let Some(realized) = self.realized.as_mut()
|
||||
{
|
||||
realized.increment_(realized_cap);
|
||||
self.price_to_amount
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.increment(realized_price, supply);
|
||||
realized.receive(price, supply.value);
|
||||
|
||||
// Update cache for added supply
|
||||
if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.on_receive(realized_price, supply.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if current.supply_state.value.is_not_zero() {
|
||||
self.cost_basis_data.as_mut().unwrap().increment(
|
||||
current.realized_price,
|
||||
current.supply_state.value,
|
||||
current.price_sats,
|
||||
current.investor_cap,
|
||||
);
|
||||
|
||||
/// Remove supply from this cohort (e.g., when UTXO ages out of cohort).
|
||||
pub fn decrement(&mut self, supply: &SupplyState, price: Option<Dollars>) {
|
||||
self.supply -= supply;
|
||||
|
||||
if supply.value > Sats::ZERO
|
||||
&& let Some(realized) = self.realized.as_mut()
|
||||
{
|
||||
let price = price.unwrap();
|
||||
realized.decrement(supply, price);
|
||||
self.price_to_amount
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.decrement(price, supply);
|
||||
|
||||
// Update cache for removed supply
|
||||
if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.on_send(price, supply.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove supply with pre-computed realized cap (for address cohorts).
|
||||
pub fn decrement_(
|
||||
&mut self,
|
||||
supply: &SupplyState,
|
||||
realized_cap: Dollars,
|
||||
realized_price: Dollars,
|
||||
) {
|
||||
self.supply -= supply;
|
||||
|
||||
if supply.value > Sats::ZERO
|
||||
&& let Some(realized) = self.realized.as_mut()
|
||||
{
|
||||
realized.decrement_(realized_cap);
|
||||
self.price_to_amount
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.decrement(realized_price, supply);
|
||||
|
||||
// Update cache for removed supply
|
||||
if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.on_send(realized_price, supply.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Process received output (new UTXO in cohort).
|
||||
pub fn receive(&mut self, supply: &SupplyState, price: Option<Dollars>) {
|
||||
self.receive_(supply, price, price.map(|price| (price, supply)), None);
|
||||
}
|
||||
|
||||
/// Process received output with custom price_to_amount updates (for address cohorts).
|
||||
pub fn receive_(
|
||||
&mut self,
|
||||
supply: &SupplyState,
|
||||
price: Option<Dollars>,
|
||||
price_to_amount_increment: Option<(Dollars, &SupplyState)>,
|
||||
price_to_amount_decrement: Option<(Dollars, &SupplyState)>,
|
||||
) {
|
||||
self.supply += supply;
|
||||
|
||||
if supply.value > Sats::ZERO
|
||||
&& let Some(realized) = self.realized.as_mut()
|
||||
{
|
||||
let price = price.unwrap();
|
||||
realized.receive(supply, price);
|
||||
|
||||
if let Some((price, supply)) = price_to_amount_increment
|
||||
&& supply.value.is_not_zero()
|
||||
{
|
||||
self.price_to_amount
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.increment(price, supply);
|
||||
|
||||
// Update cache for added supply
|
||||
if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.on_receive(price, supply.value);
|
||||
cache.on_receive(current.realized_price, current.supply_state.value);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((price, supply)) = price_to_amount_decrement
|
||||
&& supply.value.is_not_zero()
|
||||
{
|
||||
self.price_to_amount
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.decrement(price, supply);
|
||||
if prev.supply_state.value.is_not_zero() {
|
||||
self.cost_basis_data.as_mut().unwrap().decrement(
|
||||
prev.realized_price,
|
||||
prev.supply_state.value,
|
||||
prev.price_sats,
|
||||
prev.investor_cap,
|
||||
);
|
||||
|
||||
// Update cache for removed supply
|
||||
if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.on_send(price, supply.value);
|
||||
cache.on_send(prev.realized_price, prev.supply_state.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Process spent input (UTXO leaving cohort).
|
||||
pub fn send(
|
||||
pub fn send_utxo(
|
||||
&mut self,
|
||||
supply: &SupplyState,
|
||||
current_price: Option<Dollars>,
|
||||
prev_price: Option<Dollars>,
|
||||
current_price: Option<CentsUnsigned>,
|
||||
prev_price: Option<CentsUnsigned>,
|
||||
ath: Option<CentsUnsigned>,
|
||||
age: Age,
|
||||
) {
|
||||
self.send_(
|
||||
supply,
|
||||
current_price,
|
||||
prev_price,
|
||||
age,
|
||||
None,
|
||||
prev_price.map(|prev_price| (prev_price, supply)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Process spent input with custom price_to_amount updates (for address cohorts).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn send_(
|
||||
&mut self,
|
||||
supply: &SupplyState,
|
||||
current_price: Option<Dollars>,
|
||||
prev_price: Option<Dollars>,
|
||||
age: Age,
|
||||
price_to_amount_increment: Option<(Dollars, &SupplyState)>,
|
||||
price_to_amount_decrement: Option<(Dollars, &SupplyState)>,
|
||||
) {
|
||||
if supply.utxo_count == 0 {
|
||||
return;
|
||||
@@ -281,77 +226,118 @@ impl CohortState {
|
||||
self.satdays_destroyed += age.satdays_destroyed(supply.value);
|
||||
|
||||
if let Some(realized) = self.realized.as_mut() {
|
||||
let current_price = current_price.unwrap();
|
||||
let prev_price = prev_price.unwrap();
|
||||
realized.send(supply, current_price, prev_price);
|
||||
let cp = current_price.unwrap();
|
||||
let pp = prev_price.unwrap();
|
||||
let ath_price = ath.unwrap();
|
||||
let sats = supply.value;
|
||||
|
||||
if let Some((price, supply)) = price_to_amount_increment
|
||||
&& supply.value.is_not_zero()
|
||||
{
|
||||
self.price_to_amount
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.increment(price, supply);
|
||||
// Compute ONCE using typed values
|
||||
let current_ps = CentsSats::from_price_sats(cp, sats);
|
||||
let prev_ps = CentsSats::from_price_sats(pp, sats);
|
||||
let ath_ps = CentsSats::from_price_sats(ath_price, sats);
|
||||
let prev_investor_cap = prev_ps.to_investor_cap(pp);
|
||||
|
||||
realized.send(sats, current_ps, prev_ps, ath_ps, prev_investor_cap);
|
||||
|
||||
self.cost_basis_data.as_mut().unwrap().decrement(
|
||||
pp,
|
||||
sats,
|
||||
prev_ps,
|
||||
prev_investor_cap,
|
||||
);
|
||||
|
||||
if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.on_send(pp, sats);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn send_address(
|
||||
&mut self,
|
||||
supply: &SupplyState,
|
||||
current_price: CentsUnsigned,
|
||||
prev_price: CentsUnsigned,
|
||||
ath: CentsUnsigned,
|
||||
age: Age,
|
||||
current: &CostBasisSnapshot,
|
||||
prev: &CostBasisSnapshot,
|
||||
) {
|
||||
if supply.utxo_count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.supply -= supply;
|
||||
|
||||
if supply.value > Sats::ZERO {
|
||||
self.sent += supply.value;
|
||||
self.satblocks_destroyed += age.satblocks_destroyed(supply.value);
|
||||
self.satdays_destroyed += age.satdays_destroyed(supply.value);
|
||||
|
||||
if let Some(realized) = self.realized.as_mut() {
|
||||
let sats = supply.value;
|
||||
|
||||
// Compute once for realized.send using typed values
|
||||
let current_ps = CentsSats::from_price_sats(current_price, sats);
|
||||
let prev_ps = CentsSats::from_price_sats(prev_price, sats);
|
||||
let ath_ps = CentsSats::from_price_sats(ath, sats);
|
||||
let prev_investor_cap = prev_ps.to_investor_cap(prev_price);
|
||||
|
||||
realized.send(sats, current_ps, prev_ps, ath_ps, prev_investor_cap);
|
||||
|
||||
if current.supply_state.value.is_not_zero() {
|
||||
self.cost_basis_data.as_mut().unwrap().increment(
|
||||
current.realized_price,
|
||||
current.supply_state.value,
|
||||
current.price_sats,
|
||||
current.investor_cap,
|
||||
);
|
||||
|
||||
// Update cache for added supply
|
||||
if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.on_receive(price, supply.value);
|
||||
cache.on_receive(current.realized_price, current.supply_state.value);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((price, supply)) = price_to_amount_decrement
|
||||
&& supply.value.is_not_zero()
|
||||
{
|
||||
self.price_to_amount
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.decrement(price, supply);
|
||||
if prev.supply_state.value.is_not_zero() {
|
||||
self.cost_basis_data.as_mut().unwrap().decrement(
|
||||
prev.realized_price,
|
||||
prev.supply_state.value,
|
||||
prev.price_sats,
|
||||
prev.investor_cap,
|
||||
);
|
||||
|
||||
// Update cache for removed supply
|
||||
if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.on_send(price, supply.value);
|
||||
cache.on_send(prev.realized_price, prev.supply_state.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute prices at percentile thresholds.
|
||||
pub fn compute_percentile_prices(&self) -> [Dollars; PERCENTILES_LEN] {
|
||||
match self.price_to_amount.as_ref() {
|
||||
Some(p) if !p.is_empty() => p.compute_percentiles(),
|
||||
_ => [Dollars::NAN; PERCENTILES_LEN],
|
||||
}
|
||||
pub fn compute_percentiles(&self) -> Option<Percentiles> {
|
||||
self.cost_basis_data.as_ref()?.compute_percentiles()
|
||||
}
|
||||
|
||||
/// Compute unrealized profit/loss at current price.
|
||||
/// Uses O(k) incremental updates for height_price where k = flip range size.
|
||||
pub fn compute_unrealized_states(
|
||||
&mut self,
|
||||
height_price: Dollars,
|
||||
date_price: Option<Dollars>,
|
||||
height_price: CentsUnsigned,
|
||||
date_price: Option<CentsUnsigned>,
|
||||
) -> (UnrealizedState, Option<UnrealizedState>) {
|
||||
let price_to_amount = match self.price_to_amount.as_ref() {
|
||||
let cost_basis_data = match self.cost_basis_data.as_ref() {
|
||||
Some(p) if !p.is_empty() => p,
|
||||
_ => {
|
||||
return (
|
||||
UnrealizedState::NAN,
|
||||
date_price.map(|_| UnrealizedState::NAN),
|
||||
);
|
||||
}
|
||||
_ => return (UnrealizedState::ZERO, date_price.map(|_| UnrealizedState::ZERO)),
|
||||
};
|
||||
|
||||
// Date unrealized: compute from scratch (only at date boundaries, ~144x less frequent)
|
||||
let date_state = date_price.map(|date_price| {
|
||||
CachedUnrealizedState::compute_full_standalone(date_price, price_to_amount)
|
||||
CachedUnrealizedState::compute_full_standalone(date_price.into(), cost_basis_data)
|
||||
});
|
||||
|
||||
// Height unrealized: use incremental cache (O(k) where k = flip range)
|
||||
let height_state = if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.get_at_price(height_price, price_to_amount).clone()
|
||||
cache.get_at_price(height_price, cost_basis_data)
|
||||
} else {
|
||||
let cache = CachedUnrealizedState::compute_fresh(height_price, price_to_amount);
|
||||
let state = cache.state.clone();
|
||||
let cache = CachedUnrealizedState::compute_fresh(height_price, cost_basis_data);
|
||||
let state = cache.current_state();
|
||||
self.cached_unrealized = Some(cache);
|
||||
state
|
||||
};
|
||||
@@ -359,33 +345,24 @@ impl CohortState {
|
||||
(height_state, date_state)
|
||||
}
|
||||
|
||||
/// Flush state to disk at checkpoint.
|
||||
pub fn write(&mut self, height: Height, cleanup: bool) -> Result<()> {
|
||||
if let Some(p) = self.price_to_amount.as_mut() {
|
||||
if let Some(p) = self.cost_basis_data.as_mut() {
|
||||
p.write(height, cleanup)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get first (lowest) price in distribution.
|
||||
pub fn min_price(&self) -> Option<Dollars> {
|
||||
self.price_to_amount
|
||||
.as_ref()?
|
||||
.first_key_value()
|
||||
.map(|(k, _)| k)
|
||||
pub fn min_price(&self) -> Option<CentsUnsigned> {
|
||||
self.cost_basis_data.as_ref()?.first_key_value().map(|(k, _)| k.into())
|
||||
}
|
||||
|
||||
/// Get last (highest) price in distribution.
|
||||
pub fn max_price(&self) -> Option<Dollars> {
|
||||
self.price_to_amount
|
||||
.as_ref()?
|
||||
.last_key_value()
|
||||
.map(|(k, _)| k)
|
||||
pub fn max_price(&self) -> Option<CentsUnsigned> {
|
||||
self.cost_basis_data.as_ref()?.last_key_value().map(|(k, _)| k.into())
|
||||
}
|
||||
|
||||
/// Get iterator over price_to_amount for merged percentile computation.
|
||||
/// Returns None if price data is not tracked for this cohort.
|
||||
pub fn price_to_amount_iter(&self) -> Option<impl Iterator<Item = (Dollars, &Sats)>> {
|
||||
self.price_to_amount.as_ref().map(|p| p.iter())
|
||||
pub fn cost_basis_data_iter(
|
||||
&self,
|
||||
) -> Option<impl Iterator<Item = (CentsUnsigned, &Sats)>> {
|
||||
self.cost_basis_data.as_ref().map(|p| p.iter().map(|(k, v)| (k.into(), v)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ impl UTXOCohortState {
|
||||
Self(CohortState::new(path, name, compute_dollars))
|
||||
}
|
||||
|
||||
pub fn reset_price_to_amount_if_needed(&mut self) -> Result<()> {
|
||||
self.0.reset_price_to_amount_if_needed()
|
||||
pub fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> {
|
||||
self.0.reset_cost_basis_data_if_needed()
|
||||
}
|
||||
|
||||
/// Reset state for fresh start.
|
||||
@@ -25,7 +25,7 @@ impl UTXOCohortState {
|
||||
self.0.satblocks_destroyed = Sats::ZERO;
|
||||
self.0.satdays_destroyed = Sats::ZERO;
|
||||
if let Some(realized) = self.0.realized.as_mut() {
|
||||
*realized = RealizedState::NAN;
|
||||
*realized = RealizedState::default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fs,
|
||||
ops::Bound,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::{CentsSats, CentsSquaredSats, CentsUnsigned, CentsUnsignedCompact, Height, Sats};
|
||||
use pco::{
|
||||
ChunkConfig,
|
||||
standalone::{simple_compress, simple_decompress},
|
||||
};
|
||||
use rustc_hash::FxHashMap;
|
||||
use vecdb::Bytes;
|
||||
|
||||
use crate::utils::OptionExt;
|
||||
|
||||
use super::Percentiles;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct PendingRaw {
|
||||
cap_inc: CentsSats,
|
||||
cap_dec: CentsSats,
|
||||
investor_cap_inc: CentsSquaredSats,
|
||||
investor_cap_dec: CentsSquaredSats,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CostBasisData {
|
||||
pathbuf: PathBuf,
|
||||
state: Option<State>,
|
||||
pending: FxHashMap<CentsUnsignedCompact, (Sats, Sats)>,
|
||||
pending_raw: PendingRaw,
|
||||
}
|
||||
|
||||
const STATE_TO_KEEP: usize = 10;
|
||||
|
||||
impl CostBasisData {
|
||||
pub fn create(path: &Path, name: &str) -> Self {
|
||||
Self {
|
||||
pathbuf: path.join(format!("{name}_cost_basis")),
|
||||
state: None,
|
||||
pending: FxHashMap::default(),
|
||||
pending_raw: PendingRaw::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
|
||||
let files = self.read_dir(None)?;
|
||||
let (&height, path) = files.range(..=height).next_back().ok_or(Error::NotFound(
|
||||
"No cost basis state found at or before height".into(),
|
||||
))?;
|
||||
self.state = Some(State::deserialize(&fs::read(path)?)?);
|
||||
self.pending.clear();
|
||||
self.pending_raw = PendingRaw::default();
|
||||
Ok(height)
|
||||
}
|
||||
|
||||
fn assert_pending_empty(&self) {
|
||||
assert!(
|
||||
self.pending.is_empty() && self.pending_raw_is_zero(),
|
||||
"CostBasisData: pending not empty, call apply_pending first"
|
||||
);
|
||||
}
|
||||
|
||||
fn pending_raw_is_zero(&self) -> bool {
|
||||
self.pending_raw.cap_inc == CentsSats::ZERO
|
||||
&& self.pending_raw.cap_dec == CentsSats::ZERO
|
||||
&& self.pending_raw.investor_cap_inc == CentsSquaredSats::ZERO
|
||||
&& self.pending_raw.investor_cap_dec == CentsSquaredSats::ZERO
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (CentsUnsignedCompact, &Sats)> {
|
||||
self.assert_pending_empty();
|
||||
self.state.u().map.iter().map(|(&k, v)| (k, v))
|
||||
}
|
||||
|
||||
pub fn range(
|
||||
&self,
|
||||
bounds: (Bound<CentsUnsignedCompact>, Bound<CentsUnsignedCompact>),
|
||||
) -> impl Iterator<Item = (CentsUnsignedCompact, &Sats)> {
|
||||
self.assert_pending_empty();
|
||||
self.state.u().map.range(bounds).map(|(&k, v)| (k, v))
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.pending.is_empty() && self.state.u().map.is_empty()
|
||||
}
|
||||
|
||||
pub fn first_key_value(&self) -> Option<(CentsUnsignedCompact, &Sats)> {
|
||||
self.assert_pending_empty();
|
||||
self.state.u().map.first_key_value().map(|(&k, v)| (k, v))
|
||||
}
|
||||
|
||||
pub fn last_key_value(&self) -> Option<(CentsUnsignedCompact, &Sats)> {
|
||||
self.assert_pending_empty();
|
||||
self.state.u().map.last_key_value().map(|(&k, v)| (k, v))
|
||||
}
|
||||
|
||||
/// Get the exact cap_raw value (not recomputed from map).
|
||||
pub fn cap_raw(&self) -> CentsSats {
|
||||
self.assert_pending_empty();
|
||||
self.state.u().cap_raw
|
||||
}
|
||||
|
||||
/// Get the exact investor_cap_raw value (not recomputed from map).
|
||||
pub fn investor_cap_raw(&self) -> CentsSquaredSats {
|
||||
self.assert_pending_empty();
|
||||
self.state.u().investor_cap_raw
|
||||
}
|
||||
|
||||
/// Increment with pre-computed typed values
|
||||
pub fn increment(
|
||||
&mut self,
|
||||
price: CentsUnsigned,
|
||||
sats: Sats,
|
||||
price_sats: CentsSats,
|
||||
investor_cap: CentsSquaredSats,
|
||||
) {
|
||||
self.pending.entry(price.into()).or_default().0 += sats;
|
||||
self.pending_raw.cap_inc += price_sats;
|
||||
if investor_cap != CentsSquaredSats::ZERO {
|
||||
self.pending_raw.investor_cap_inc += investor_cap;
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrement with pre-computed typed values
|
||||
pub fn decrement(
|
||||
&mut self,
|
||||
price: CentsUnsigned,
|
||||
sats: Sats,
|
||||
price_sats: CentsSats,
|
||||
investor_cap: CentsSquaredSats,
|
||||
) {
|
||||
self.pending.entry(price.into()).or_default().1 += sats;
|
||||
self.pending_raw.cap_dec += price_sats;
|
||||
if investor_cap != CentsSquaredSats::ZERO {
|
||||
self.pending_raw.investor_cap_dec += investor_cap;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_pending(&mut self) {
|
||||
for (cents, (inc, dec)) in self.pending.drain() {
|
||||
let entry = self.state.um().map.entry(cents).or_default();
|
||||
*entry += inc;
|
||||
if *entry < dec {
|
||||
panic!(
|
||||
"CostBasisData::apply_pending underflow!\n\
|
||||
Path: {:?}\n\
|
||||
Price: {}\n\
|
||||
Current + increments: {}\n\
|
||||
Trying to decrement by: {}",
|
||||
self.pathbuf,
|
||||
cents.to_dollars(),
|
||||
entry,
|
||||
dec
|
||||
);
|
||||
}
|
||||
*entry -= dec;
|
||||
if *entry == Sats::ZERO {
|
||||
self.state.um().map.remove(¢s);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply raw values
|
||||
let state = self.state.um();
|
||||
state.cap_raw += self.pending_raw.cap_inc;
|
||||
|
||||
// Check for underflow before subtracting
|
||||
if state.cap_raw.inner() < self.pending_raw.cap_dec.inner() {
|
||||
panic!(
|
||||
"CostBasisData::apply_pending cap_raw underflow!\n\
|
||||
Path: {:?}\n\
|
||||
Current cap_raw (after increments): {}\n\
|
||||
Trying to decrement by: {}",
|
||||
self.pathbuf, state.cap_raw, self.pending_raw.cap_dec
|
||||
);
|
||||
}
|
||||
state.cap_raw -= self.pending_raw.cap_dec;
|
||||
|
||||
// Only process investor_cap if there are non-zero values
|
||||
let has_investor_cap = self.pending_raw.investor_cap_inc != CentsSquaredSats::ZERO
|
||||
|| self.pending_raw.investor_cap_dec != CentsSquaredSats::ZERO;
|
||||
|
||||
if has_investor_cap {
|
||||
state.investor_cap_raw += self.pending_raw.investor_cap_inc;
|
||||
|
||||
if state.investor_cap_raw.inner() < self.pending_raw.investor_cap_dec.inner() {
|
||||
panic!(
|
||||
"CostBasisData::apply_pending investor_cap_raw underflow!\n\
|
||||
Path: {:?}\n\
|
||||
Current investor_cap_raw (after increments): {}\n\
|
||||
Trying to decrement by: {}",
|
||||
self.pathbuf, state.investor_cap_raw, self.pending_raw.investor_cap_dec
|
||||
);
|
||||
}
|
||||
state.investor_cap_raw -= self.pending_raw.investor_cap_dec;
|
||||
}
|
||||
|
||||
self.pending_raw = PendingRaw::default();
|
||||
}
|
||||
|
||||
pub fn init(&mut self) {
|
||||
self.state.replace(State::default());
|
||||
self.pending.clear();
|
||||
self.pending_raw = PendingRaw::default();
|
||||
}
|
||||
|
||||
pub fn compute_percentiles(&self) -> Option<Percentiles> {
|
||||
self.assert_pending_empty();
|
||||
Percentiles::compute(self.iter().map(|(k, &v)| (k, v)))
|
||||
}
|
||||
|
||||
pub fn clean(&mut self) -> Result<()> {
|
||||
let _ = fs::remove_dir_all(&self.pathbuf);
|
||||
fs::create_dir_all(&self.pathbuf)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_dir(&self, keep_only_before: Option<Height>) -> Result<BTreeMap<Height, PathBuf>> {
|
||||
Ok(fs::read_dir(&self.pathbuf)?
|
||||
.filter_map(|entry| {
|
||||
let path = entry.ok()?.path();
|
||||
let name = path.file_name()?.to_str()?;
|
||||
if let Ok(h) = name.parse::<u32>().map(Height::from) {
|
||||
if keep_only_before.is_none_or(|height| h < height) {
|
||||
Some((h, path))
|
||||
} else {
|
||||
let _ = fs::remove_file(path);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeMap<Height, PathBuf>>())
|
||||
}
|
||||
|
||||
pub fn write(&mut self, height: Height, cleanup: bool) -> Result<()> {
|
||||
self.apply_pending();
|
||||
|
||||
if cleanup {
|
||||
let files = self.read_dir(Some(height))?;
|
||||
|
||||
for (_, path) in files
|
||||
.iter()
|
||||
.take(files.len().saturating_sub(STATE_TO_KEEP - 1))
|
||||
{
|
||||
fs::remove_file(path)?;
|
||||
}
|
||||
}
|
||||
|
||||
fs::write(self.path_state(height), self.state.u().serialize()?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn path_state(&self, height: Height) -> PathBuf {
|
||||
self.pathbuf.join(u32::from(height).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
struct State {
|
||||
map: BTreeMap<CentsUnsignedCompact, Sats>,
|
||||
/// Exact realized cap: Σ(price × sats)
|
||||
cap_raw: CentsSats,
|
||||
/// Exact investor cap: Σ(price² × sats)
|
||||
investor_cap_raw: CentsSquaredSats,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn serialize(&self) -> vecdb::Result<Vec<u8>> {
|
||||
let keys: Vec<u32> = self.map.keys().map(|k| k.inner()).collect();
|
||||
let values: Vec<u64> = self.map.values().map(|v| u64::from(*v)).collect();
|
||||
|
||||
let config = ChunkConfig::default();
|
||||
let compressed_keys = simple_compress(&keys, &config)?;
|
||||
let compressed_values = simple_compress(&values, &config)?;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
buffer.extend(keys.len().to_bytes());
|
||||
buffer.extend(compressed_keys.len().to_bytes());
|
||||
buffer.extend(compressed_values.len().to_bytes());
|
||||
buffer.extend(compressed_keys);
|
||||
buffer.extend(compressed_values);
|
||||
buffer.extend(self.cap_raw.to_bytes());
|
||||
buffer.extend(self.investor_cap_raw.to_bytes());
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
fn deserialize(data: &[u8]) -> vecdb::Result<Self> {
|
||||
let entry_count = usize::from_bytes(&data[0..8])?;
|
||||
let keys_len = usize::from_bytes(&data[8..16])?;
|
||||
let values_len = usize::from_bytes(&data[16..24])?;
|
||||
|
||||
let keys_start = 24;
|
||||
let values_start = keys_start + keys_len;
|
||||
let raw_start = values_start + values_len;
|
||||
|
||||
let keys: Vec<u32> = simple_decompress(&data[keys_start..values_start])?;
|
||||
let values: Vec<u64> = simple_decompress(&data[values_start..raw_start])?;
|
||||
|
||||
let map: BTreeMap<CentsUnsignedCompact, Sats> = keys
|
||||
.into_iter()
|
||||
.zip(values)
|
||||
.map(|(k, v)| (CentsUnsignedCompact::new(k), Sats::from(v)))
|
||||
.collect();
|
||||
|
||||
assert_eq!(map.len(), entry_count);
|
||||
|
||||
let cap_raw = CentsSats::from_bytes(&data[raw_start..raw_start + 16])?;
|
||||
let investor_cap_raw = CentsSquaredSats::from_bytes(&data[raw_start + 16..raw_start + 32])?;
|
||||
|
||||
Ok(Self {
|
||||
map,
|
||||
cap_raw,
|
||||
investor_cap_raw,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
mod price_to_amount;
|
||||
mod cost_basis_data;
|
||||
mod percentiles;
|
||||
mod realized;
|
||||
mod unrealized;
|
||||
|
||||
pub use price_to_amount::*;
|
||||
pub use cost_basis_data::*;
|
||||
pub use percentiles::*;
|
||||
pub use realized::*;
|
||||
pub use unrealized::*;
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
use brk_types::{CentsUnsigned, CentsUnsignedCompact, Sats};
|
||||
|
||||
use crate::internal::{PERCENTILES, PERCENTILES_LEN};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Percentiles {
|
||||
/// Sat-weighted: percentiles by coin count
|
||||
pub sat_weighted: [CentsUnsigned; PERCENTILES_LEN],
|
||||
/// USD-weighted: percentiles by invested capital (sats × price)
|
||||
pub usd_weighted: [CentsUnsigned; PERCENTILES_LEN],
|
||||
}
|
||||
|
||||
impl Percentiles {
|
||||
/// Compute both sat-weighted and USD-weighted percentiles in a single pass.
|
||||
/// Takes an iterator over (price, sats) pairs, assumed sorted by price ascending.
|
||||
pub fn compute(iter: impl Iterator<Item = (CentsUnsignedCompact, Sats)>) -> Option<Self> {
|
||||
// Collect to allow two passes: one for totals, one for percentiles
|
||||
let entries: Vec<_> = iter.collect();
|
||||
if entries.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Compute totals
|
||||
let mut total_sats: u64 = 0;
|
||||
let mut total_usd: u128 = 0;
|
||||
for &(cents, sats) in &entries {
|
||||
total_sats += u64::from(sats);
|
||||
total_usd += cents.as_u128() * sats.as_u128();
|
||||
}
|
||||
|
||||
if total_sats == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut sat_weighted = [CentsUnsigned::ZERO; PERCENTILES_LEN];
|
||||
let mut usd_weighted = [CentsUnsigned::ZERO; PERCENTILES_LEN];
|
||||
let mut cumsum_sats: u64 = 0;
|
||||
let mut cumsum_usd: u128 = 0;
|
||||
let mut sat_idx = 0;
|
||||
let mut usd_idx = 0;
|
||||
|
||||
for (cents, sats) in entries {
|
||||
cumsum_sats += u64::from(sats);
|
||||
cumsum_usd += cents.as_u128() * sats.as_u128();
|
||||
|
||||
while sat_idx < PERCENTILES_LEN
|
||||
&& cumsum_sats >= total_sats * u64::from(PERCENTILES[sat_idx]) / 100
|
||||
{
|
||||
sat_weighted[sat_idx] = cents.into();
|
||||
sat_idx += 1;
|
||||
}
|
||||
|
||||
while usd_idx < PERCENTILES_LEN
|
||||
&& cumsum_usd >= total_usd * u128::from(PERCENTILES[usd_idx]) / 100
|
||||
{
|
||||
usd_weighted[usd_idx] = cents.into();
|
||||
usd_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Some(Self {
|
||||
sat_weighted,
|
||||
usd_weighted,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fs,
|
||||
ops::Bound,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::{CentsCompact, Dollars, Height, Sats, SupplyState};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use pco::{standalone::{simple_compress, simple_decompress}, ChunkConfig};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use vecdb::Bytes;
|
||||
|
||||
use crate::{
|
||||
internal::{PERCENTILES, PERCENTILES_LEN},
|
||||
utils::OptionExt,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PriceToAmount {
|
||||
pathbuf: PathBuf,
|
||||
state: Option<State>,
|
||||
/// Pending deltas: (total_increment, total_decrement) per price.
|
||||
/// Flushed to BTreeMap before reads and at end of block.
|
||||
pending: FxHashMap<CentsCompact, (Sats, Sats)>,
|
||||
}
|
||||
|
||||
const STATE_AT_: &str = "state_at_";
|
||||
const STATE_TO_KEEP: usize = 10;
|
||||
|
||||
impl PriceToAmount {
|
||||
pub fn create(path: &Path, name: &str) -> Self {
|
||||
Self {
|
||||
pathbuf: path.join(format!("{name}_price_to_amount")),
|
||||
state: None,
|
||||
pending: FxHashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
|
||||
let files = self.read_dir(None)?;
|
||||
let (&height, path) = files.range(..=height).next_back().ok_or(Error::NotFound(
|
||||
"No price state found at or before height".into(),
|
||||
))?;
|
||||
self.state = Some(State::deserialize(&fs::read(path)?)?);
|
||||
self.pending.clear();
|
||||
Ok(height)
|
||||
}
|
||||
|
||||
fn assert_pending_empty(&self) {
|
||||
assert!(
|
||||
self.pending.is_empty(),
|
||||
"PriceToAmount: pending not empty, call apply_pending first"
|
||||
);
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (Dollars, &Sats)> {
|
||||
self.assert_pending_empty();
|
||||
self.state.u().iter().map(|(k, v)| (k.to_dollars(), v))
|
||||
}
|
||||
|
||||
/// Iterate over entries in a price range with explicit bounds.
|
||||
pub fn range(
|
||||
&self,
|
||||
bounds: (Bound<Dollars>, Bound<Dollars>),
|
||||
) -> impl Iterator<Item = (Dollars, &Sats)> {
|
||||
self.assert_pending_empty();
|
||||
|
||||
let start = match bounds.0 {
|
||||
Bound::Included(d) => Bound::Included(CentsCompact::from(d)),
|
||||
Bound::Excluded(d) => Bound::Excluded(CentsCompact::from(d)),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
};
|
||||
|
||||
let end = match bounds.1 {
|
||||
Bound::Included(d) => Bound::Included(CentsCompact::from(d)),
|
||||
Bound::Excluded(d) => Bound::Excluded(CentsCompact::from(d)),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
};
|
||||
|
||||
self.state
|
||||
.u()
|
||||
.range((start, end))
|
||||
.map(|(k, v)| (k.to_dollars(), v))
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.pending.is_empty() && self.state.u().is_empty()
|
||||
}
|
||||
|
||||
pub fn first_key_value(&self) -> Option<(Dollars, &Sats)> {
|
||||
self.assert_pending_empty();
|
||||
self.state
|
||||
.u()
|
||||
.first_key_value()
|
||||
.map(|(k, v)| (k.to_dollars(), v))
|
||||
}
|
||||
|
||||
pub fn last_key_value(&self) -> Option<(Dollars, &Sats)> {
|
||||
self.assert_pending_empty();
|
||||
self.state
|
||||
.u()
|
||||
.last_key_value()
|
||||
.map(|(k, v)| (k.to_dollars(), v))
|
||||
}
|
||||
|
||||
/// Accumulate increment in pending batch. O(1).
|
||||
pub fn increment(&mut self, price: Dollars, supply_state: &SupplyState) {
|
||||
self.pending.entry(CentsCompact::from(price)).or_default().0 += supply_state.value;
|
||||
}
|
||||
|
||||
/// Accumulate decrement in pending batch. O(1).
|
||||
pub fn decrement(&mut self, price: Dollars, supply_state: &SupplyState) {
|
||||
self.pending.entry(CentsCompact::from(price)).or_default().1 += supply_state.value;
|
||||
}
|
||||
|
||||
/// Apply pending deltas to BTreeMap. O(k log n) where k = unique prices in pending.
|
||||
/// Must be called before any read operations.
|
||||
pub fn apply_pending(&mut self) {
|
||||
for (cents, (inc, dec)) in self.pending.drain() {
|
||||
let entry = self.state.um().entry(cents).or_default();
|
||||
*entry += inc;
|
||||
if *entry < dec {
|
||||
panic!(
|
||||
"PriceToAmount::apply_pending underflow!\n\
|
||||
Path: {:?}\n\
|
||||
Price: {}\n\
|
||||
Current + increments: {}\n\
|
||||
Trying to decrement by: {}",
|
||||
self.pathbuf,
|
||||
cents.to_dollars(),
|
||||
entry,
|
||||
dec
|
||||
);
|
||||
}
|
||||
*entry -= dec;
|
||||
if *entry == Sats::ZERO {
|
||||
self.state.um().remove(¢s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(&mut self) {
|
||||
self.state.replace(State::default());
|
||||
self.pending.clear();
|
||||
}
|
||||
|
||||
/// Compute percentile prices by iterating the BTreeMap directly.
|
||||
/// O(n) where n = number of unique prices.
|
||||
pub fn compute_percentiles(&self) -> [Dollars; PERCENTILES_LEN] {
|
||||
self.assert_pending_empty();
|
||||
|
||||
let state = match self.state.as_ref() {
|
||||
Some(s) if !s.is_empty() => s,
|
||||
_ => return [Dollars::NAN; PERCENTILES_LEN],
|
||||
};
|
||||
|
||||
let total: u64 = state.values().map(|&s| u64::from(s)).sum();
|
||||
if total == 0 {
|
||||
return [Dollars::NAN; PERCENTILES_LEN];
|
||||
}
|
||||
|
||||
let mut result = [Dollars::NAN; PERCENTILES_LEN];
|
||||
let mut cumsum = 0u64;
|
||||
let mut idx = 0;
|
||||
|
||||
for (¢s, &amount) in state.iter() {
|
||||
cumsum += u64::from(amount);
|
||||
while idx < PERCENTILES_LEN && cumsum >= total * u64::from(PERCENTILES[idx]) / 100 {
|
||||
result[idx] = cents.to_dollars();
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn clean(&mut self) -> Result<()> {
|
||||
let _ = fs::remove_dir_all(&self.pathbuf);
|
||||
fs::create_dir_all(&self.pathbuf)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_dir(&self, keep_only_before: Option<Height>) -> Result<BTreeMap<Height, PathBuf>> {
|
||||
Ok(fs::read_dir(&self.pathbuf)?
|
||||
.filter_map(|entry| {
|
||||
let path = entry.ok()?.path();
|
||||
let name = path.file_name()?.to_str()?;
|
||||
let height_str = name.strip_prefix(STATE_AT_).unwrap_or(name);
|
||||
if let Ok(h) = height_str.parse::<u32>().map(Height::from) {
|
||||
if keep_only_before.is_none_or(|height| h < height) {
|
||||
Some((h, path))
|
||||
} else {
|
||||
let _ = fs::remove_file(path);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeMap<Height, PathBuf>>())
|
||||
}
|
||||
|
||||
/// Flush state to disk, optionally cleaning up old state files.
|
||||
pub fn write(&mut self, height: Height, cleanup: bool) -> Result<()> {
|
||||
self.apply_pending();
|
||||
|
||||
if cleanup {
|
||||
let files = self.read_dir(Some(height))?;
|
||||
|
||||
for (_, path) in files
|
||||
.iter()
|
||||
.take(files.len().saturating_sub(STATE_TO_KEEP - 1))
|
||||
{
|
||||
fs::remove_file(path)?;
|
||||
}
|
||||
}
|
||||
|
||||
fs::write(self.path_state(height), self.state.u().serialize()?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn path_state(&self, height: Height) -> PathBuf {
|
||||
Self::path_state_(&self.pathbuf, height)
|
||||
}
|
||||
fn path_state_(path: &Path, height: Height) -> PathBuf {
|
||||
path.join(u32::from(height).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, Deref, DerefMut, Serialize, Deserialize)]
|
||||
struct State(BTreeMap<CentsCompact, Sats>);
|
||||
|
||||
impl State {
|
||||
fn serialize(&self) -> vecdb::Result<Vec<u8>> {
|
||||
let keys: Vec<i32> = self.keys().map(|k| i32::from(*k)).collect();
|
||||
let values: Vec<u64> = self.values().map(|v| u64::from(*v)).collect();
|
||||
|
||||
let config = ChunkConfig::default();
|
||||
let compressed_keys = simple_compress(&keys, &config)?;
|
||||
let compressed_values = simple_compress(&values, &config)?;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
buffer.extend(keys.len().to_bytes());
|
||||
buffer.extend(compressed_keys.len().to_bytes());
|
||||
buffer.extend(compressed_keys);
|
||||
buffer.extend(compressed_values);
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
fn deserialize(data: &[u8]) -> vecdb::Result<Self> {
|
||||
let entry_count = usize::from_bytes(&data[0..8])?;
|
||||
let keys_len = usize::from_bytes(&data[8..16])?;
|
||||
|
||||
let keys: Vec<i32> = simple_decompress(&data[16..16 + keys_len])?;
|
||||
let values: Vec<u64> = simple_decompress(&data[16 + keys_len..])?;
|
||||
|
||||
let map: BTreeMap<CentsCompact, Sats> = keys
|
||||
.into_iter()
|
||||
.zip(values)
|
||||
.map(|(k, v)| (CentsCompact::from(k), Sats::from(v)))
|
||||
.collect();
|
||||
|
||||
assert_eq!(map.len(), entry_count);
|
||||
|
||||
Ok(Self(map))
|
||||
}
|
||||
}
|
||||
@@ -1,88 +1,244 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use brk_types::{CheckedSub, Dollars, SupplyState};
|
||||
use brk_types::{CentsSats, CentsSquaredSats, CentsUnsigned, Sats};
|
||||
|
||||
/// Realized state using u128 for raw cent*sat values internally.
|
||||
/// This avoids overflow and defers division to output time for efficiency.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct RealizedState {
|
||||
pub cap: Dollars,
|
||||
pub profit: Dollars,
|
||||
pub loss: Dollars,
|
||||
pub value_created: Dollars,
|
||||
pub value_destroyed: Dollars,
|
||||
/// Raw realized cap: Σ(price × sats)
|
||||
cap_raw: u128,
|
||||
/// Raw investor cap: Σ(price² × sats)
|
||||
/// investor_price = investor_cap_raw / cap_raw (gives cents directly)
|
||||
investor_cap_raw: CentsSquaredSats,
|
||||
/// Raw realized profit (cents * sats)
|
||||
profit_raw: u128,
|
||||
/// Raw realized loss (cents * sats)
|
||||
loss_raw: u128,
|
||||
/// sell_price × sats for profit cases
|
||||
profit_value_created_raw: u128,
|
||||
/// cost_basis × sats for profit cases
|
||||
profit_value_destroyed_raw: u128,
|
||||
/// sell_price × sats for loss cases
|
||||
loss_value_created_raw: u128,
|
||||
/// cost_basis × sats for loss cases (= capitulation_flow)
|
||||
loss_value_destroyed_raw: u128,
|
||||
/// Raw realized peak regret: Σ((peak - sell_price) × sats)
|
||||
peak_regret_raw: u128,
|
||||
/// Sats sent in profit
|
||||
sent_in_profit: Sats,
|
||||
/// Sats sent in loss
|
||||
sent_in_loss: Sats,
|
||||
}
|
||||
|
||||
impl RealizedState {
|
||||
pub const NAN: Self = Self {
|
||||
cap: Dollars::NAN,
|
||||
profit: Dollars::NAN,
|
||||
loss: Dollars::NAN,
|
||||
value_created: Dollars::NAN,
|
||||
value_destroyed: Dollars::NAN,
|
||||
};
|
||||
/// Get realized cap as CentsUnsigned (divides by ONE_BTC).
|
||||
#[inline]
|
||||
pub fn cap(&self) -> CentsUnsigned {
|
||||
CentsUnsigned::new((self.cap_raw / Sats::ONE_BTC_U128) as u64)
|
||||
}
|
||||
|
||||
/// Set cap_raw directly from persisted value.
|
||||
#[inline]
|
||||
pub fn set_cap_raw(&mut self, cap_raw: CentsSats) {
|
||||
self.cap_raw = cap_raw.inner();
|
||||
}
|
||||
|
||||
/// Set investor_cap_raw directly from persisted value.
|
||||
#[inline]
|
||||
pub fn set_investor_cap_raw(&mut self, investor_cap_raw: CentsSquaredSats) {
|
||||
self.investor_cap_raw = investor_cap_raw;
|
||||
}
|
||||
|
||||
/// Get investor price as CentsUnsigned.
|
||||
/// investor_price = Σ(price² × sats) / Σ(price × sats)
|
||||
/// This is the dollar-weighted average acquisition price.
|
||||
#[inline]
|
||||
pub fn investor_price(&self) -> CentsUnsigned {
|
||||
if self.cap_raw == 0 {
|
||||
return CentsUnsigned::ZERO;
|
||||
}
|
||||
CentsUnsigned::new((self.investor_cap_raw / self.cap_raw) as u64)
|
||||
}
|
||||
|
||||
/// Get raw realized cap for aggregation.
|
||||
#[inline]
|
||||
pub fn cap_raw(&self) -> CentsSats {
|
||||
CentsSats::new(self.cap_raw)
|
||||
}
|
||||
|
||||
/// Get raw investor cap for aggregation.
|
||||
#[inline]
|
||||
pub fn investor_cap_raw(&self) -> CentsSquaredSats {
|
||||
self.investor_cap_raw
|
||||
}
|
||||
|
||||
/// Get realized profit as CentsUnsigned.
|
||||
#[inline]
|
||||
pub fn profit(&self) -> CentsUnsigned {
|
||||
CentsUnsigned::new((self.profit_raw / Sats::ONE_BTC_U128) as u64)
|
||||
}
|
||||
|
||||
/// Get realized loss as CentsUnsigned.
|
||||
#[inline]
|
||||
pub fn loss(&self) -> CentsUnsigned {
|
||||
CentsUnsigned::new((self.loss_raw / Sats::ONE_BTC_U128) as u64)
|
||||
}
|
||||
|
||||
/// Get value created as CentsUnsigned (derived from profit + loss splits).
|
||||
#[inline]
|
||||
pub fn value_created(&self) -> CentsUnsigned {
|
||||
let raw = self.profit_value_created_raw + self.loss_value_created_raw;
|
||||
CentsUnsigned::new((raw / Sats::ONE_BTC_U128) as u64)
|
||||
}
|
||||
|
||||
/// Get value destroyed as CentsUnsigned (derived from profit + loss splits).
|
||||
#[inline]
|
||||
pub fn value_destroyed(&self) -> CentsUnsigned {
|
||||
let raw = self.profit_value_destroyed_raw + self.loss_value_destroyed_raw;
|
||||
CentsUnsigned::new((raw / Sats::ONE_BTC_U128) as u64)
|
||||
}
|
||||
|
||||
/// Get profit value created as CentsUnsigned (sell_price × sats for profit cases).
|
||||
#[inline]
|
||||
pub fn profit_value_created(&self) -> CentsUnsigned {
|
||||
CentsUnsigned::new((self.profit_value_created_raw / Sats::ONE_BTC_U128) as u64)
|
||||
}
|
||||
|
||||
/// Get profit value destroyed as CentsUnsigned (cost_basis × sats for profit cases).
|
||||
/// This is also known as profit_flow.
|
||||
#[inline]
|
||||
pub fn profit_value_destroyed(&self) -> CentsUnsigned {
|
||||
CentsUnsigned::new((self.profit_value_destroyed_raw / Sats::ONE_BTC_U128) as u64)
|
||||
}
|
||||
|
||||
/// Get loss value created as CentsUnsigned (sell_price × sats for loss cases).
|
||||
#[inline]
|
||||
pub fn loss_value_created(&self) -> CentsUnsigned {
|
||||
CentsUnsigned::new((self.loss_value_created_raw / Sats::ONE_BTC_U128) as u64)
|
||||
}
|
||||
|
||||
/// Get loss value destroyed as CentsUnsigned (cost_basis × sats for loss cases).
|
||||
/// This is also known as capitulation_flow.
|
||||
#[inline]
|
||||
pub fn loss_value_destroyed(&self) -> CentsUnsigned {
|
||||
CentsUnsigned::new((self.loss_value_destroyed_raw / Sats::ONE_BTC_U128) as u64)
|
||||
}
|
||||
|
||||
/// Get capitulation flow as CentsUnsigned.
|
||||
/// This is the invested capital (cost_basis × sats) sold at a loss.
|
||||
/// Alias for loss_value_destroyed.
|
||||
#[inline]
|
||||
pub fn capitulation_flow(&self) -> CentsUnsigned {
|
||||
self.loss_value_destroyed()
|
||||
}
|
||||
|
||||
/// Get profit flow as CentsUnsigned.
|
||||
/// This is the invested capital (cost_basis × sats) sold at a profit.
|
||||
/// Alias for profit_value_destroyed.
|
||||
#[inline]
|
||||
pub fn profit_flow(&self) -> CentsUnsigned {
|
||||
self.profit_value_destroyed()
|
||||
}
|
||||
|
||||
/// Get realized peak regret as CentsUnsigned.
|
||||
/// This is Σ((peak - sell_price) × sats) - how much more could have been made
|
||||
/// by selling at peak instead of when actually sold.
|
||||
#[inline]
|
||||
pub fn peak_regret(&self) -> CentsUnsigned {
|
||||
CentsUnsigned::new((self.peak_regret_raw / Sats::ONE_BTC_U128) as u64)
|
||||
}
|
||||
|
||||
/// Get sats sent in profit.
|
||||
#[inline]
|
||||
pub fn sent_in_profit(&self) -> Sats {
|
||||
self.sent_in_profit
|
||||
}
|
||||
|
||||
/// Get sats sent in loss.
|
||||
#[inline]
|
||||
pub fn sent_in_loss(&self) -> Sats {
|
||||
self.sent_in_loss
|
||||
}
|
||||
|
||||
pub fn reset_single_iteration_values(&mut self) {
|
||||
if self.cap != Dollars::NAN {
|
||||
self.profit = Dollars::ZERO;
|
||||
self.loss = Dollars::ZERO;
|
||||
self.value_created = Dollars::ZERO;
|
||||
self.value_destroyed = Dollars::ZERO;
|
||||
}
|
||||
self.profit_raw = 0;
|
||||
self.loss_raw = 0;
|
||||
self.profit_value_created_raw = 0;
|
||||
self.profit_value_destroyed_raw = 0;
|
||||
self.loss_value_created_raw = 0;
|
||||
self.loss_value_destroyed_raw = 0;
|
||||
self.peak_regret_raw = 0;
|
||||
self.sent_in_profit = Sats::ZERO;
|
||||
self.sent_in_loss = Sats::ZERO;
|
||||
}
|
||||
|
||||
pub fn increment(&mut self, supply_state: &SupplyState, price: Dollars) {
|
||||
if supply_state.value.is_zero() {
|
||||
/// Increment using pre-computed values (for UTXO path)
|
||||
#[inline]
|
||||
pub fn increment(&mut self, price: CentsUnsigned, sats: Sats) {
|
||||
if sats.is_zero() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.increment_(price * supply_state.value)
|
||||
let price_sats = CentsSats::from_price_sats(price, sats);
|
||||
self.cap_raw += price_sats.as_u128();
|
||||
self.investor_cap_raw += price_sats.to_investor_cap(price);
|
||||
}
|
||||
|
||||
pub fn increment_(&mut self, realized_cap: Dollars) {
|
||||
if self.cap == Dollars::NAN {
|
||||
self.cap = Dollars::ZERO;
|
||||
self.profit = Dollars::ZERO;
|
||||
self.loss = Dollars::ZERO;
|
||||
self.value_created = Dollars::ZERO;
|
||||
self.value_destroyed = Dollars::ZERO;
|
||||
}
|
||||
|
||||
self.cap += realized_cap;
|
||||
/// Increment using pre-computed snapshot values (for address path)
|
||||
#[inline]
|
||||
pub fn increment_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats) {
|
||||
self.cap_raw += price_sats.as_u128();
|
||||
self.investor_cap_raw += investor_cap;
|
||||
}
|
||||
|
||||
pub fn decrement(&mut self, supply_state: &SupplyState, price: Dollars) {
|
||||
self.decrement_(price * supply_state.value);
|
||||
/// Decrement using pre-computed snapshot values (for address path)
|
||||
#[inline]
|
||||
pub fn decrement_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats) {
|
||||
self.cap_raw -= price_sats.as_u128();
|
||||
self.investor_cap_raw -= investor_cap;
|
||||
}
|
||||
|
||||
pub fn decrement_(&mut self, realized_cap: Dollars) {
|
||||
self.cap = self.cap.checked_sub(realized_cap).unwrap();
|
||||
}
|
||||
|
||||
pub fn receive(&mut self, supply_state: &SupplyState, current_price: Dollars) {
|
||||
self.increment(supply_state, current_price);
|
||||
#[inline]
|
||||
pub fn receive(&mut self, price: CentsUnsigned, sats: Sats) {
|
||||
self.increment(price, sats);
|
||||
}
|
||||
|
||||
/// Send with pre-computed typed values. Inlines decrement to avoid recomputation.
|
||||
#[inline]
|
||||
pub fn send(
|
||||
&mut self,
|
||||
supply_state: &SupplyState,
|
||||
current_price: Dollars,
|
||||
prev_price: Dollars,
|
||||
sats: Sats,
|
||||
current_ps: CentsSats,
|
||||
prev_ps: CentsSats,
|
||||
ath_ps: CentsSats,
|
||||
prev_investor_cap: CentsSquaredSats,
|
||||
) {
|
||||
let current_value = current_price * supply_state.value;
|
||||
let prev_value = prev_price * supply_state.value;
|
||||
|
||||
self.value_created += current_value;
|
||||
self.value_destroyed += prev_value;
|
||||
|
||||
match current_price.cmp(&prev_price) {
|
||||
match current_ps.cmp(&prev_ps) {
|
||||
Ordering::Greater => {
|
||||
self.profit += current_value.checked_sub(prev_value).unwrap();
|
||||
self.profit_raw += (current_ps - prev_ps).as_u128();
|
||||
self.profit_value_created_raw += current_ps.as_u128();
|
||||
self.profit_value_destroyed_raw += prev_ps.as_u128();
|
||||
self.sent_in_profit += sats;
|
||||
}
|
||||
Ordering::Less => {
|
||||
self.loss += prev_value.checked_sub(current_value).unwrap();
|
||||
self.loss_raw += (prev_ps - current_ps).as_u128();
|
||||
self.loss_value_created_raw += current_ps.as_u128();
|
||||
self.loss_value_destroyed_raw += prev_ps.as_u128();
|
||||
self.sent_in_loss += sats;
|
||||
}
|
||||
Ordering::Equal => {
|
||||
// Break-even: count as profit side (arbitrary but consistent)
|
||||
self.profit_value_created_raw += current_ps.as_u128();
|
||||
self.profit_value_destroyed_raw += prev_ps.as_u128();
|
||||
self.sent_in_profit += sats;
|
||||
}
|
||||
Ordering::Equal => {}
|
||||
}
|
||||
|
||||
self.decrement(supply_state, prev_price);
|
||||
// Track peak regret: (peak - sell_price) × sats
|
||||
self.peak_regret_raw += (ath_ps - current_ps).as_u128();
|
||||
|
||||
// Inline decrement to avoid recomputation
|
||||
self.cap_raw -= prev_ps.as_u128();
|
||||
self.investor_cap_raw -= prev_investor_cap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,253 +1,328 @@
|
||||
use std::ops::Bound;
|
||||
|
||||
use brk_types::{CentsUnsigned, Dollars, Sats};
|
||||
use vecdb::CheckedSub;
|
||||
use brk_types::{CentsUnsigned, CentsUnsignedCompact, Sats};
|
||||
|
||||
use super::price_to_amount::PriceToAmount;
|
||||
use super::cost_basis_data::CostBasisData;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct UnrealizedState {
|
||||
pub supply_in_profit: Sats,
|
||||
pub supply_in_loss: Sats,
|
||||
pub unrealized_profit: Dollars,
|
||||
pub unrealized_loss: Dollars,
|
||||
/// Invested capital in profit: Σ(sats × price) where price <= spot
|
||||
pub invested_capital_in_profit: Dollars,
|
||||
/// Invested capital in loss: Σ(sats × price) where price > spot
|
||||
pub invested_capital_in_loss: Dollars,
|
||||
pub unrealized_profit: CentsUnsigned,
|
||||
pub unrealized_loss: CentsUnsigned,
|
||||
pub invested_capital_in_profit: CentsUnsigned,
|
||||
pub invested_capital_in_loss: CentsUnsigned,
|
||||
/// Raw Σ(price² × sats) for UTXOs in profit. Used for aggregation.
|
||||
pub investor_cap_in_profit_raw: u128,
|
||||
/// Raw Σ(price² × sats) for UTXOs in loss. Used for aggregation.
|
||||
pub investor_cap_in_loss_raw: u128,
|
||||
/// Raw Σ(price × sats) for UTXOs in profit. Used for aggregation.
|
||||
pub invested_capital_in_profit_raw: u128,
|
||||
/// Raw Σ(price × sats) for UTXOs in loss. Used for aggregation.
|
||||
pub invested_capital_in_loss_raw: u128,
|
||||
}
|
||||
|
||||
impl UnrealizedState {
|
||||
pub const NAN: Self = Self {
|
||||
supply_in_profit: Sats::ZERO,
|
||||
supply_in_loss: Sats::ZERO,
|
||||
unrealized_profit: Dollars::NAN,
|
||||
unrealized_loss: Dollars::NAN,
|
||||
invested_capital_in_profit: Dollars::NAN,
|
||||
invested_capital_in_loss: Dollars::NAN,
|
||||
};
|
||||
|
||||
pub const ZERO: Self = Self {
|
||||
supply_in_profit: Sats::ZERO,
|
||||
supply_in_loss: Sats::ZERO,
|
||||
unrealized_profit: Dollars::ZERO,
|
||||
unrealized_loss: Dollars::ZERO,
|
||||
invested_capital_in_profit: Dollars::ZERO,
|
||||
invested_capital_in_loss: Dollars::ZERO,
|
||||
unrealized_profit: CentsUnsigned::ZERO,
|
||||
unrealized_loss: CentsUnsigned::ZERO,
|
||||
invested_capital_in_profit: CentsUnsigned::ZERO,
|
||||
invested_capital_in_loss: CentsUnsigned::ZERO,
|
||||
investor_cap_in_profit_raw: 0,
|
||||
investor_cap_in_loss_raw: 0,
|
||||
invested_capital_in_profit_raw: 0,
|
||||
invested_capital_in_loss_raw: 0,
|
||||
};
|
||||
|
||||
/// Compute pain_index from raw values.
|
||||
/// pain_index = investor_price_of_losers - spot
|
||||
#[inline]
|
||||
pub fn pain_index(&self, spot: CentsUnsigned) -> CentsUnsigned {
|
||||
if self.invested_capital_in_loss_raw == 0 {
|
||||
return CentsUnsigned::ZERO;
|
||||
}
|
||||
let investor_price_losers =
|
||||
self.investor_cap_in_loss_raw / self.invested_capital_in_loss_raw;
|
||||
CentsUnsigned::new((investor_price_losers - spot.as_u128()) as u64)
|
||||
}
|
||||
|
||||
/// Compute greed_index from raw values.
|
||||
/// greed_index = spot - investor_price_of_winners
|
||||
#[inline]
|
||||
pub fn greed_index(&self, spot: CentsUnsigned) -> CentsUnsigned {
|
||||
if self.invested_capital_in_profit_raw == 0 {
|
||||
return CentsUnsigned::ZERO;
|
||||
}
|
||||
let investor_price_winners =
|
||||
self.investor_cap_in_profit_raw / self.invested_capital_in_profit_raw;
|
||||
CentsUnsigned::new((spot.as_u128() - investor_price_winners) as u64)
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal cache state using u128 for raw cent*sat values.
|
||||
/// This avoids rounding errors from premature division by ONE_BTC.
|
||||
/// Division happens only when converting to UnrealizedState output.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct CachedStateRaw {
|
||||
supply_in_profit: Sats,
|
||||
supply_in_loss: Sats,
|
||||
/// Raw value: sum of (price_cents * sats) for UTXOs in profit
|
||||
unrealized_profit: u128,
|
||||
/// Raw value: sum of (price_cents * sats) for UTXOs in loss
|
||||
unrealized_loss: u128,
|
||||
/// Raw value: sum of (price_cents * sats) for UTXOs in profit
|
||||
invested_capital_in_profit: u128,
|
||||
/// Raw value: sum of (price_cents * sats) for UTXOs in loss
|
||||
invested_capital_in_loss: u128,
|
||||
/// Raw value: sum of (price_cents² * sats) for UTXOs in profit
|
||||
investor_cap_in_profit: u128,
|
||||
/// Raw value: sum of (price_cents² * sats) for UTXOs in loss
|
||||
investor_cap_in_loss: u128,
|
||||
}
|
||||
|
||||
impl CachedStateRaw {
|
||||
/// Convert raw values to final output by dividing by ONE_BTC.
|
||||
fn to_output(&self) -> UnrealizedState {
|
||||
UnrealizedState {
|
||||
supply_in_profit: self.supply_in_profit,
|
||||
supply_in_loss: self.supply_in_loss,
|
||||
unrealized_profit: CentsUnsigned::new(
|
||||
(self.unrealized_profit / Sats::ONE_BTC_U128) as u64,
|
||||
),
|
||||
unrealized_loss: CentsUnsigned::new(
|
||||
(self.unrealized_loss / Sats::ONE_BTC_U128) as u64,
|
||||
),
|
||||
invested_capital_in_profit: CentsUnsigned::new(
|
||||
(self.invested_capital_in_profit / Sats::ONE_BTC_U128) as u64,
|
||||
),
|
||||
invested_capital_in_loss: CentsUnsigned::new(
|
||||
(self.invested_capital_in_loss / Sats::ONE_BTC_U128) as u64,
|
||||
),
|
||||
investor_cap_in_profit_raw: self.investor_cap_in_profit,
|
||||
investor_cap_in_loss_raw: self.investor_cap_in_loss,
|
||||
invested_capital_in_profit_raw: self.invested_capital_in_profit,
|
||||
invested_capital_in_loss_raw: self.invested_capital_in_loss,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cached unrealized state for O(k) incremental updates.
|
||||
/// k = number of entries in price flip range (typically tiny).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedUnrealizedState {
|
||||
pub state: UnrealizedState,
|
||||
at_price: Dollars,
|
||||
state: CachedStateRaw,
|
||||
at_price: CentsUnsignedCompact,
|
||||
}
|
||||
|
||||
impl CachedUnrealizedState {
|
||||
/// Create new cache by computing from scratch. O(n).
|
||||
pub fn compute_fresh(price: Dollars, price_to_amount: &PriceToAmount) -> Self {
|
||||
let state = Self::compute_full_standalone(price, price_to_amount);
|
||||
Self {
|
||||
state,
|
||||
at_price: price,
|
||||
}
|
||||
pub fn compute_fresh(price: CentsUnsigned, cost_basis_data: &CostBasisData) -> Self {
|
||||
let price: CentsUnsignedCompact = price.into();
|
||||
let state = Self::compute_raw(price, cost_basis_data);
|
||||
Self { state, at_price: price }
|
||||
}
|
||||
|
||||
/// Get the current cached state as output (without price update).
|
||||
pub fn current_state(&self) -> UnrealizedState {
|
||||
self.state.to_output()
|
||||
}
|
||||
|
||||
/// Get unrealized state at new_price. O(k) where k = flip range size.
|
||||
pub fn get_at_price(
|
||||
&mut self,
|
||||
new_price: Dollars,
|
||||
price_to_amount: &PriceToAmount,
|
||||
) -> &UnrealizedState {
|
||||
new_price: CentsUnsigned,
|
||||
cost_basis_data: &CostBasisData,
|
||||
) -> UnrealizedState {
|
||||
let new_price: CentsUnsignedCompact = new_price.into();
|
||||
if new_price != self.at_price {
|
||||
self.update_for_price_change(new_price, price_to_amount);
|
||||
self.update_for_price_change(new_price, cost_basis_data);
|
||||
}
|
||||
&self.state
|
||||
self.state.to_output()
|
||||
}
|
||||
|
||||
/// Update cached state when a receive happens.
|
||||
/// Determines profit/loss classification relative to cached price.
|
||||
pub fn on_receive(&mut self, purchase_price: Dollars, sats: Sats) {
|
||||
let invested_capital = purchase_price * sats;
|
||||
if purchase_price <= self.at_price {
|
||||
pub fn on_receive(&mut self, price: CentsUnsigned, sats: Sats) {
|
||||
let price: CentsUnsignedCompact = price.into();
|
||||
let sats_u128 = sats.as_u128();
|
||||
let price_u128 = price.as_u128();
|
||||
let invested_capital = price_u128 * sats_u128;
|
||||
let investor_cap = price_u128 * invested_capital;
|
||||
|
||||
if price <= self.at_price {
|
||||
self.state.supply_in_profit += sats;
|
||||
self.state.invested_capital_in_profit += invested_capital;
|
||||
if purchase_price < self.at_price {
|
||||
let diff = self.at_price.checked_sub(purchase_price).unwrap();
|
||||
self.state.unrealized_profit += diff * sats;
|
||||
self.state.investor_cap_in_profit += investor_cap;
|
||||
if price < self.at_price {
|
||||
let diff = (self.at_price - price).as_u128();
|
||||
self.state.unrealized_profit += diff * sats_u128;
|
||||
}
|
||||
} else {
|
||||
self.state.supply_in_loss += sats;
|
||||
self.state.invested_capital_in_loss += invested_capital;
|
||||
let diff = purchase_price.checked_sub(self.at_price).unwrap();
|
||||
self.state.unrealized_loss += diff * sats;
|
||||
self.state.investor_cap_in_loss += investor_cap;
|
||||
let diff = (price - self.at_price).as_u128();
|
||||
self.state.unrealized_loss += diff * sats_u128;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update cached state when a send happens from historical price.
|
||||
pub fn on_send(&mut self, historical_price: Dollars, sats: Sats) {
|
||||
let invested_capital = historical_price * sats;
|
||||
if historical_price <= self.at_price {
|
||||
// Was in profit
|
||||
pub fn on_send(&mut self, price: CentsUnsigned, sats: Sats) {
|
||||
let price: CentsUnsignedCompact = price.into();
|
||||
let sats_u128 = sats.as_u128();
|
||||
let price_u128 = price.as_u128();
|
||||
let invested_capital = price_u128 * sats_u128;
|
||||
let investor_cap = price_u128 * invested_capital;
|
||||
|
||||
if price <= self.at_price {
|
||||
self.state.supply_in_profit -= sats;
|
||||
self.state.invested_capital_in_profit = self
|
||||
.state
|
||||
.invested_capital_in_profit
|
||||
.checked_sub(invested_capital)
|
||||
.unwrap();
|
||||
if historical_price < self.at_price {
|
||||
let diff = self.at_price.checked_sub(historical_price).unwrap();
|
||||
let profit_removed = diff * sats;
|
||||
self.state.unrealized_profit = self
|
||||
.state
|
||||
.unrealized_profit
|
||||
.checked_sub(profit_removed)
|
||||
.unwrap_or(Dollars::ZERO);
|
||||
self.state.invested_capital_in_profit -= invested_capital;
|
||||
self.state.investor_cap_in_profit -= investor_cap;
|
||||
if price < self.at_price {
|
||||
let diff = (self.at_price - price).as_u128();
|
||||
self.state.unrealized_profit -= diff * sats_u128;
|
||||
}
|
||||
} else {
|
||||
// Was in loss
|
||||
self.state.supply_in_loss -= sats;
|
||||
self.state.invested_capital_in_loss = self
|
||||
.state
|
||||
.invested_capital_in_loss
|
||||
.checked_sub(invested_capital)
|
||||
.unwrap();
|
||||
let diff = historical_price.checked_sub(self.at_price).unwrap();
|
||||
let loss_removed = diff * sats;
|
||||
self.state.unrealized_loss = self
|
||||
.state
|
||||
.unrealized_loss
|
||||
.checked_sub(loss_removed)
|
||||
.unwrap_or(Dollars::ZERO);
|
||||
self.state.invested_capital_in_loss -= invested_capital;
|
||||
self.state.investor_cap_in_loss -= investor_cap;
|
||||
let diff = (price - self.at_price).as_u128();
|
||||
self.state.unrealized_loss -= diff * sats_u128;
|
||||
}
|
||||
}
|
||||
|
||||
/// Incremental update for price change. O(k) where k = entries in flip range.
|
||||
fn update_for_price_change(&mut self, new_price: Dollars, price_to_amount: &PriceToAmount) {
|
||||
fn update_for_price_change(
|
||||
&mut self,
|
||||
new_price: CentsUnsignedCompact,
|
||||
cost_basis_data: &CostBasisData,
|
||||
) {
|
||||
let old_price = self.at_price;
|
||||
let delta_f64 = f64::from(new_price) - f64::from(old_price);
|
||||
|
||||
// Update profit/loss for entries that DON'T flip
|
||||
// Profit changes by delta * supply_in_profit
|
||||
// Loss changes by -delta * supply_in_loss
|
||||
if delta_f64 > 0.0 {
|
||||
// Price went up: profits increase, losses decrease
|
||||
self.state.unrealized_profit += Dollars::from(delta_f64) * self.state.supply_in_profit;
|
||||
let loss_decrease = Dollars::from(delta_f64) * self.state.supply_in_loss;
|
||||
self.state.unrealized_loss = self
|
||||
.state
|
||||
.unrealized_loss
|
||||
.checked_sub(loss_decrease)
|
||||
.unwrap_or(Dollars::ZERO);
|
||||
} else if delta_f64 < 0.0 {
|
||||
// Price went down: profits decrease, losses increase
|
||||
let profit_decrease = Dollars::from(-delta_f64) * self.state.supply_in_profit;
|
||||
self.state.unrealized_profit = self
|
||||
.state
|
||||
.unrealized_profit
|
||||
.checked_sub(profit_decrease)
|
||||
.unwrap_or(Dollars::ZERO);
|
||||
self.state.unrealized_loss += Dollars::from(-delta_f64) * self.state.supply_in_loss;
|
||||
}
|
||||
|
||||
// Handle flipped entries (only iterate the small range between prices)
|
||||
if new_price > old_price {
|
||||
// Price went up: entries where old < price <= new flip from loss to profit
|
||||
let delta = (new_price - old_price).as_u128();
|
||||
|
||||
// Save original supply for delta calculation (before crossing UTXOs move)
|
||||
let original_supply_in_profit = self.state.supply_in_profit.as_u128();
|
||||
|
||||
// First, process UTXOs crossing from loss to profit
|
||||
// Range (old_price, new_price] means: old_price < price <= new_price
|
||||
for (price, &sats) in
|
||||
price_to_amount.range((Bound::Excluded(old_price), Bound::Included(new_price)))
|
||||
cost_basis_data.range((Bound::Excluded(old_price), Bound::Included(new_price)))
|
||||
{
|
||||
// Move from loss to profit
|
||||
let sats_u128 = sats.as_u128();
|
||||
let price_u128 = price.as_u128();
|
||||
let invested_capital = price_u128 * sats_u128;
|
||||
let investor_cap = price_u128 * invested_capital;
|
||||
|
||||
// Move between buckets
|
||||
self.state.supply_in_loss -= sats;
|
||||
self.state.supply_in_profit += sats;
|
||||
self.state.invested_capital_in_loss -= invested_capital;
|
||||
self.state.invested_capital_in_profit += invested_capital;
|
||||
self.state.investor_cap_in_loss -= investor_cap;
|
||||
self.state.investor_cap_in_profit += investor_cap;
|
||||
|
||||
// Undo the loss adjustment applied above for this entry
|
||||
// We decreased loss by delta * sats, but this entry should be removed entirely
|
||||
// Original loss: (price - old_price) * sats
|
||||
// After global adjustment: original - delta * sats (negative, wrong)
|
||||
// Correct: 0 (removed from loss)
|
||||
// Correction: add back delta * sats, then add original loss
|
||||
let delta_adj = Dollars::from(delta_f64) * sats;
|
||||
self.state.unrealized_loss += delta_adj;
|
||||
if price > old_price {
|
||||
let original_loss = price.checked_sub(old_price).unwrap() * sats;
|
||||
self.state.unrealized_loss += original_loss;
|
||||
}
|
||||
// Remove their original contribution to unrealized_loss
|
||||
// (price > old_price is always true due to Bound::Excluded)
|
||||
let original_loss = (price - old_price).as_u128();
|
||||
self.state.unrealized_loss -= original_loss * sats_u128;
|
||||
|
||||
// Undo the profit adjustment applied above for this entry
|
||||
// We increased profit by delta * sats, but this entry was not in profit before
|
||||
// Correct profit: (new_price - price) * sats
|
||||
// Correction: subtract delta * sats, add correct profit
|
||||
let profit_adj = Dollars::from(delta_f64) * sats;
|
||||
self.state.unrealized_profit = self
|
||||
.state
|
||||
.unrealized_profit
|
||||
.checked_sub(profit_adj)
|
||||
.unwrap_or(Dollars::ZERO);
|
||||
if new_price > price {
|
||||
let correct_profit = new_price.checked_sub(price).unwrap() * sats;
|
||||
self.state.unrealized_profit += correct_profit;
|
||||
// Add their new contribution to unrealized_profit (if not at boundary)
|
||||
if price < new_price {
|
||||
let new_profit = (new_price - price).as_u128();
|
||||
self.state.unrealized_profit += new_profit * sats_u128;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply delta to non-crossing UTXOs only
|
||||
// Non-crossing profit UTXOs: their profit increases by delta
|
||||
self.state.unrealized_profit += delta * original_supply_in_profit;
|
||||
// Non-crossing loss UTXOs: their loss decreases by delta
|
||||
let non_crossing_loss_sats =
|
||||
self.state.supply_in_loss.as_u128(); // Already excludes crossing
|
||||
self.state.unrealized_loss -= delta * non_crossing_loss_sats;
|
||||
} else if new_price < old_price {
|
||||
// Price went down: entries where new < price <= old flip from profit to loss
|
||||
let delta = (old_price - new_price).as_u128();
|
||||
|
||||
// Save original supply for delta calculation (before crossing UTXOs move)
|
||||
let original_supply_in_loss = self.state.supply_in_loss.as_u128();
|
||||
|
||||
// First, process UTXOs crossing from profit to loss
|
||||
// Range (new_price, old_price] means: new_price < price <= old_price
|
||||
for (price, &sats) in
|
||||
price_to_amount.range((Bound::Excluded(new_price), Bound::Included(old_price)))
|
||||
cost_basis_data.range((Bound::Excluded(new_price), Bound::Included(old_price)))
|
||||
{
|
||||
// Move from profit to loss
|
||||
let sats_u128 = sats.as_u128();
|
||||
let price_u128 = price.as_u128();
|
||||
let invested_capital = price_u128 * sats_u128;
|
||||
let investor_cap = price_u128 * invested_capital;
|
||||
|
||||
// Move between buckets
|
||||
self.state.supply_in_profit -= sats;
|
||||
self.state.supply_in_loss += sats;
|
||||
self.state.invested_capital_in_profit -= invested_capital;
|
||||
self.state.invested_capital_in_loss += invested_capital;
|
||||
self.state.investor_cap_in_profit -= investor_cap;
|
||||
self.state.investor_cap_in_loss += investor_cap;
|
||||
|
||||
// Undo the profit adjustment applied above for this entry
|
||||
let delta_adj = Dollars::from(-delta_f64) * sats;
|
||||
self.state.unrealized_profit += delta_adj;
|
||||
if old_price > price {
|
||||
let original_profit = old_price.checked_sub(price).unwrap() * sats;
|
||||
self.state.unrealized_profit += original_profit;
|
||||
// Remove their original contribution to unrealized_profit (if not at boundary)
|
||||
if price < old_price {
|
||||
let original_profit = (old_price - price).as_u128();
|
||||
self.state.unrealized_profit -= original_profit * sats_u128;
|
||||
}
|
||||
|
||||
// Undo the loss adjustment applied above for this entry
|
||||
let loss_adj = Dollars::from(-delta_f64) * sats;
|
||||
self.state.unrealized_loss = self
|
||||
.state
|
||||
.unrealized_loss
|
||||
.checked_sub(loss_adj)
|
||||
.unwrap_or(Dollars::ZERO);
|
||||
if price > new_price {
|
||||
let correct_loss = price.checked_sub(new_price).unwrap() * sats;
|
||||
self.state.unrealized_loss += correct_loss;
|
||||
}
|
||||
// Add their new contribution to unrealized_loss
|
||||
// (price > new_price is always true due to Bound::Excluded)
|
||||
let new_loss = (price - new_price).as_u128();
|
||||
self.state.unrealized_loss += new_loss * sats_u128;
|
||||
}
|
||||
|
||||
// Apply delta to non-crossing UTXOs only
|
||||
// Non-crossing loss UTXOs: their loss increases by delta
|
||||
self.state.unrealized_loss += delta * original_supply_in_loss;
|
||||
// Non-crossing profit UTXOs: their profit decreases by delta
|
||||
let non_crossing_profit_sats =
|
||||
self.state.supply_in_profit.as_u128(); // Already excludes crossing
|
||||
self.state.unrealized_profit -= delta * non_crossing_profit_sats;
|
||||
}
|
||||
|
||||
self.at_price = new_price;
|
||||
}
|
||||
|
||||
/// Full computation from scratch (no cache). O(n).
|
||||
pub fn compute_full_standalone(
|
||||
current_price: Dollars,
|
||||
price_to_amount: &PriceToAmount,
|
||||
) -> UnrealizedState {
|
||||
let mut state = UnrealizedState::ZERO;
|
||||
/// Compute raw cached state from cost_basis_data.
|
||||
fn compute_raw(
|
||||
current_price: CentsUnsignedCompact,
|
||||
cost_basis_data: &CostBasisData,
|
||||
) -> CachedStateRaw {
|
||||
let mut state = CachedStateRaw::default();
|
||||
|
||||
for (price, &sats) in cost_basis_data.iter() {
|
||||
let sats_u128 = sats.as_u128();
|
||||
let price_u128 = price.as_u128();
|
||||
let invested_capital = price_u128 * sats_u128;
|
||||
let investor_cap = price_u128 * invested_capital;
|
||||
|
||||
for (price, &sats) in price_to_amount.iter() {
|
||||
let invested_capital = price * sats;
|
||||
if price <= current_price {
|
||||
state.supply_in_profit += sats;
|
||||
state.invested_capital_in_profit += invested_capital;
|
||||
state.investor_cap_in_profit += investor_cap;
|
||||
if price < current_price {
|
||||
let diff = current_price.checked_sub(price).unwrap();
|
||||
state.unrealized_profit += diff * sats;
|
||||
let diff = (current_price - price).as_u128();
|
||||
state.unrealized_profit += diff * sats_u128;
|
||||
}
|
||||
} else {
|
||||
state.supply_in_loss += sats;
|
||||
state.invested_capital_in_loss += invested_capital;
|
||||
let diff = price.checked_sub(current_price).unwrap();
|
||||
state.unrealized_loss += diff * sats;
|
||||
state.investor_cap_in_loss += investor_cap;
|
||||
let diff = (price - current_price).as_u128();
|
||||
state.unrealized_loss += diff * sats_u128;
|
||||
}
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
|
||||
/// Compute final UnrealizedState directly (not cached).
|
||||
/// Used for date_state which doesn't use the cache.
|
||||
pub fn compute_full_standalone(
|
||||
current_price: CentsUnsignedCompact,
|
||||
cost_basis_data: &CostBasisData,
|
||||
) -> UnrealizedState {
|
||||
Self::compute_raw(current_price, cost_basis_data).to_output()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{
|
||||
DateIndex, EmptyAddressData, EmptyAddressIndex, Height, LoadedAddressData, LoadedAddressIndex,
|
||||
DateIndex, EmptyAddressData, EmptyAddressIndex, FundedAddressData, FundedAddressIndex, Height,
|
||||
SupplyState, Version,
|
||||
};
|
||||
use tracing::info;
|
||||
use tracing::{debug, info};
|
||||
use vecdb::{
|
||||
AnyVec, BytesVec, Database, Exit, GenericStoredVec, ImportableVec, IterableCloneableVec,
|
||||
LazyVecFrom1, PAGE_SIZE, Stamp, TypedVecIterator, VecIndex,
|
||||
@@ -25,12 +25,12 @@ use crate::{
|
||||
use super::{
|
||||
AddressCohorts, AddressesDataVecs, AnyAddressIndexesVecs, UTXOCohorts,
|
||||
address::{
|
||||
AddrCountVecs, AddressActivityVecs, GrowthRateVecs, NewAddrCountVecs, TotalAddrCountVecs,
|
||||
AddrCountsVecs, AddressActivityVecs, GrowthRateVecs, NewAddrCountVecs, TotalAddrCountVecs,
|
||||
},
|
||||
compute::aggregates,
|
||||
};
|
||||
|
||||
const VERSION: Version = Version::new(21);
|
||||
const VERSION: Version = Version::new(22);
|
||||
|
||||
/// Main struct holding all computed vectors and state for stateful computation.
|
||||
#[derive(Clone, Traversable)]
|
||||
@@ -38,14 +38,14 @@ pub struct Vecs {
|
||||
#[traversable(skip)]
|
||||
db: Database,
|
||||
|
||||
pub chain_state: BytesVec<Height, SupplyState>,
|
||||
pub supply_state: BytesVec<Height, SupplyState>,
|
||||
pub any_address_indexes: AnyAddressIndexesVecs,
|
||||
pub addresses_data: AddressesDataVecs,
|
||||
pub utxo_cohorts: UTXOCohorts,
|
||||
pub address_cohorts: AddressCohorts,
|
||||
|
||||
pub addr_count: AddrCountVecs,
|
||||
pub empty_addr_count: AddrCountVecs,
|
||||
pub addr_count: AddrCountsVecs,
|
||||
pub empty_addr_count: AddrCountsVecs,
|
||||
pub address_activity: AddressActivityVecs,
|
||||
|
||||
/// Total addresses ever seen (addr_count + empty_addr_count) - lazy, global + per-type
|
||||
@@ -55,8 +55,8 @@ pub struct Vecs {
|
||||
/// Growth rate (new / addr_count) - lazy ratio with distribution stats, global + per-type
|
||||
pub growth_rate: GrowthRateVecs,
|
||||
|
||||
pub loadedaddressindex:
|
||||
LazyVecFrom1<LoadedAddressIndex, LoadedAddressIndex, LoadedAddressIndex, LoadedAddressData>,
|
||||
pub fundedaddressindex:
|
||||
LazyVecFrom1<FundedAddressIndex, FundedAddressIndex, FundedAddressIndex, FundedAddressData>,
|
||||
pub emptyaddressindex:
|
||||
LazyVecFrom1<EmptyAddressIndex, EmptyAddressIndex, EmptyAddressIndex, EmptyAddressData>,
|
||||
}
|
||||
@@ -92,8 +92,8 @@ impl Vecs {
|
||||
)?;
|
||||
|
||||
// Create address data BytesVecs first so we can also use them for identity mappings
|
||||
let loadedaddressindex_to_loadedaddressdata = BytesVec::forced_import_with(
|
||||
vecdb::ImportOptions::new(&db, "loadedaddressdata", version)
|
||||
let fundedaddressindex_to_fundedaddressdata = BytesVec::forced_import_with(
|
||||
vecdb::ImportOptions::new(&db, "fundedaddressdata", version)
|
||||
.with_saved_stamped_changes(SAVED_STAMPED_CHANGES),
|
||||
)?;
|
||||
let emptyaddressindex_to_emptyaddressdata = BytesVec::forced_import_with(
|
||||
@@ -102,10 +102,10 @@ impl Vecs {
|
||||
)?;
|
||||
|
||||
// Identity mappings for traversable
|
||||
let loadedaddressindex = LazyVecFrom1::init(
|
||||
"loadedaddressindex",
|
||||
let fundedaddressindex = LazyVecFrom1::init(
|
||||
"fundedaddressindex",
|
||||
version,
|
||||
loadedaddressindex_to_loadedaddressdata.boxed_clone(),
|
||||
fundedaddressindex_to_fundedaddressdata.boxed_clone(),
|
||||
|index, _| Some(index),
|
||||
);
|
||||
let emptyaddressindex = LazyVecFrom1::init(
|
||||
@@ -115,9 +115,9 @@ impl Vecs {
|
||||
|index, _| Some(index),
|
||||
);
|
||||
|
||||
let addr_count = AddrCountVecs::forced_import(&db, "addr_count", version, indexes)?;
|
||||
let addr_count = AddrCountsVecs::forced_import(&db, "addr_count", version, indexes)?;
|
||||
let empty_addr_count =
|
||||
AddrCountVecs::forced_import(&db, "empty_addr_count", version, indexes)?;
|
||||
AddrCountsVecs::forced_import(&db, "empty_addr_count", version, indexes)?;
|
||||
let address_activity =
|
||||
AddressActivityVecs::forced_import(&db, "address_activity", version, indexes)?;
|
||||
|
||||
@@ -139,8 +139,8 @@ impl Vecs {
|
||||
GrowthRateVecs::forced_import(&db, version, indexes, &new_addr_count, &addr_count)?;
|
||||
|
||||
let this = Self {
|
||||
chain_state: BytesVec::forced_import_with(
|
||||
vecdb::ImportOptions::new(&db, "chain", version)
|
||||
supply_state: BytesVec::forced_import_with(
|
||||
vecdb::ImportOptions::new(&db, "supply_state", version)
|
||||
.with_saved_stamped_changes(SAVED_STAMPED_CHANGES),
|
||||
)?,
|
||||
|
||||
@@ -156,10 +156,10 @@ impl Vecs {
|
||||
|
||||
any_address_indexes: AnyAddressIndexesVecs::forced_import(&db, version)?,
|
||||
addresses_data: AddressesDataVecs {
|
||||
loaded: loadedaddressindex_to_loadedaddressdata,
|
||||
funded: fundedaddressindex_to_fundedaddressdata,
|
||||
empty: emptyaddressindex_to_emptyaddressdata,
|
||||
},
|
||||
loadedaddressindex,
|
||||
fundedaddressindex,
|
||||
emptyaddressindex,
|
||||
|
||||
db,
|
||||
@@ -197,7 +197,7 @@ impl Vecs {
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
// 1. Find minimum height we have data for across stateful vecs
|
||||
let current_height = Height::from(self.chain_state.len());
|
||||
let current_height = Height::from(self.supply_state.len());
|
||||
let height_based_min = self.min_stateful_height_len();
|
||||
let dateindex_min = self.min_stateful_dateindex_len();
|
||||
let min_stateful = adjust_for_dateindex_gap(height_based_min, dateindex_min, indexes)?;
|
||||
@@ -219,7 +219,7 @@ impl Vecs {
|
||||
let stamp = Stamp::from(height);
|
||||
|
||||
// Rollback BytesVec state and capture results for validation
|
||||
let chain_state_rollback = self.chain_state.rollback_before(stamp);
|
||||
let chain_state_rollback = self.supply_state.rollback_before(stamp);
|
||||
|
||||
// Validate all rollbacks and imports are consistent
|
||||
let recovered = recover_state(
|
||||
@@ -234,14 +234,20 @@ impl Vecs {
|
||||
if recovered.starting_height.is_zero() {
|
||||
info!("State recovery validation failed, falling back to fresh start");
|
||||
}
|
||||
debug!(
|
||||
"recover_state completed, starting_height={}",
|
||||
recovered.starting_height
|
||||
);
|
||||
recovered.starting_height
|
||||
}
|
||||
StartMode::Fresh => Height::ZERO,
|
||||
};
|
||||
|
||||
debug!("recovered_height={}", recovered_height);
|
||||
|
||||
// Fresh start: reset all state
|
||||
let (starting_height, mut chain_state) = if recovered_height.is_zero() {
|
||||
self.chain_state.reset()?;
|
||||
self.supply_state.reset()?;
|
||||
self.addr_count.reset_height()?;
|
||||
self.empty_addr_count.reset_height()?;
|
||||
self.address_activity.reset_height()?;
|
||||
@@ -256,23 +262,27 @@ impl Vecs {
|
||||
(Height::ZERO, vec![])
|
||||
} else {
|
||||
// Recover chain_state from stored values
|
||||
debug!("recovering chain_state from stored values");
|
||||
let height_to_timestamp = &blocks.time.timestamp_monotonic;
|
||||
let height_to_price = price.map(|p| &p.usd.split.close.height);
|
||||
let height_to_price = price.map(|p| &p.cents.split.height.close);
|
||||
|
||||
let mut height_to_timestamp_iter = height_to_timestamp.into_iter();
|
||||
let mut height_to_price_iter = height_to_price.map(|v| v.into_iter());
|
||||
let mut chain_state_iter = self.chain_state.into_iter();
|
||||
let mut chain_state_iter = self.supply_state.into_iter();
|
||||
|
||||
debug!("building supply_state vec for {} heights", recovered_height);
|
||||
let chain_state = (0..recovered_height.to_usize())
|
||||
.map(|h| {
|
||||
let h = Height::from(h);
|
||||
let price = height_to_price_iter.as_mut().map(|v| *v.get_unwrap(h));
|
||||
BlockState {
|
||||
supply: chain_state_iter.get_unwrap(h),
|
||||
price: height_to_price_iter.as_mut().map(|v| *v.get_unwrap(h)),
|
||||
price,
|
||||
timestamp: height_to_timestamp_iter.get_unwrap(h),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
debug!("chain_state vec built");
|
||||
|
||||
(recovered_height, chain_state)
|
||||
};
|
||||
@@ -292,16 +302,23 @@ impl Vecs {
|
||||
}
|
||||
|
||||
// 2b. Validate computed versions
|
||||
debug!("validating computed versions");
|
||||
let base_version = VERSION;
|
||||
self.utxo_cohorts.validate_computed_versions(base_version)?;
|
||||
self.address_cohorts
|
||||
.validate_computed_versions(base_version)?;
|
||||
debug!("computed versions validated");
|
||||
|
||||
// 3. Get last height from indexer
|
||||
let last_height = Height::from(indexer.vecs.blocks.blockhash.len().saturating_sub(1));
|
||||
debug!(
|
||||
"last_height={}, starting_height={}",
|
||||
last_height, starting_height
|
||||
);
|
||||
|
||||
// 4. Process blocks
|
||||
if starting_height <= last_height {
|
||||
debug!("calling process_blocks");
|
||||
process_blocks(
|
||||
self,
|
||||
indexer,
|
||||
@@ -400,7 +417,7 @@ impl Vecs {
|
||||
self.utxo_cohorts
|
||||
.min_separate_stateful_height_len()
|
||||
.min(self.address_cohorts.min_separate_stateful_height_len())
|
||||
.min(Height::from(self.chain_state.len()))
|
||||
.min(Height::from(self.supply_state.len()))
|
||||
.min(self.any_address_indexes.min_stamped_height())
|
||||
.min(self.addresses_data.min_stamped_height())
|
||||
.min(Height::from(self.addr_count.min_stateful_height()))
|
||||
|
||||
@@ -10,7 +10,7 @@ use vecdb::{BinaryTransform, IterableBoxedVec, IterableCloneableVec, LazyVecFrom
|
||||
use crate::internal::{
|
||||
ComputedFromHeightLast, ComputedFromHeightSum, ComputedFromDateLast, ComputedVecValue,
|
||||
LazyBinaryComputedFromHeightLast, LazyBinaryComputedFromHeightSum, LazyBinaryTransformLast,
|
||||
LazyDateDerivedLast, LazyDateDerivedSumCum, NumericValue,
|
||||
LazyDateDerivedLast, LazyDateDerivedSumCum, LazyFromDateLast, LazyFromHeightLast, NumericValue,
|
||||
};
|
||||
|
||||
const VERSION: Version = Version::ZERO;
|
||||
@@ -223,6 +223,45 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_lazy_height_and_dateindex_last<F, S1SourceT>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source1: &LazyFromHeightLast<S1T, S1SourceT>,
|
||||
source2: &ComputedFromDateLast<S2T>,
|
||||
) -> Self
|
||||
where
|
||||
F: BinaryTransform<S1T, S2T, T>,
|
||||
S1SourceT: ComputedVecValue + JsonSchema,
|
||||
{
|
||||
let v = version + VERSION;
|
||||
|
||||
macro_rules! period {
|
||||
($p:ident) => {
|
||||
LazyBinaryTransformLast::from_vecs::<F>(
|
||||
name,
|
||||
v,
|
||||
source1.$p.boxed_clone(),
|
||||
source2.$p.boxed_clone(),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
Self {
|
||||
dateindex: LazyVecFrom2::transformed::<F>(
|
||||
name,
|
||||
v,
|
||||
source1.dateindex.boxed_clone(),
|
||||
source2.dateindex.boxed_clone(),
|
||||
),
|
||||
weekindex: period!(weekindex),
|
||||
monthindex: period!(monthindex),
|
||||
quarterindex: period!(quarterindex),
|
||||
semesterindex: period!(semesterindex),
|
||||
yearindex: period!(yearindex),
|
||||
decadeindex: period!(decadeindex),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_dateindex_and_height_last<F: BinaryTransform<S1T, S2T, T>>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
@@ -753,4 +792,81 @@ where
|
||||
decadeindex: period!(decadeindex),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from a ComputedFromDateLast and a LazyFromDateLast.
|
||||
pub fn from_computed_and_lazy_last<F, S2SourceT>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source1: &ComputedFromDateLast<S1T>,
|
||||
source2: &LazyFromDateLast<S2T, S2SourceT>,
|
||||
) -> Self
|
||||
where
|
||||
F: BinaryTransform<S1T, S2T, T>,
|
||||
S2SourceT: ComputedVecValue + JsonSchema,
|
||||
{
|
||||
let v = version + VERSION;
|
||||
|
||||
macro_rules! period {
|
||||
($p:ident) => {
|
||||
LazyBinaryTransformLast::from_vecs::<F>(
|
||||
name,
|
||||
v,
|
||||
source1.rest.$p.boxed_clone(),
|
||||
source2.$p.boxed_clone(),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
Self {
|
||||
dateindex: LazyVecFrom2::transformed::<F>(
|
||||
name,
|
||||
v,
|
||||
source1.dateindex.boxed_clone(),
|
||||
source2.dateindex.boxed_clone(),
|
||||
),
|
||||
weekindex: period!(weekindex),
|
||||
monthindex: period!(monthindex),
|
||||
quarterindex: period!(quarterindex),
|
||||
semesterindex: period!(semesterindex),
|
||||
yearindex: period!(yearindex),
|
||||
decadeindex: period!(decadeindex),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from a ComputedFromDateLast and a LazyDateDerivedLast.
|
||||
pub fn from_computed_and_derived_last<F: BinaryTransform<S1T, S2T, T>>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source1: &ComputedFromDateLast<S1T>,
|
||||
dateindex_source2: IterableBoxedVec<DateIndex, S2T>,
|
||||
source2: &LazyDateDerivedLast<S2T>,
|
||||
) -> Self {
|
||||
let v = version + VERSION;
|
||||
|
||||
macro_rules! period {
|
||||
($p:ident) => {
|
||||
LazyBinaryTransformLast::from_lazy_last::<F, _, _, _, _>(
|
||||
name,
|
||||
v,
|
||||
&source1.$p,
|
||||
&source2.$p,
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
Self {
|
||||
dateindex: LazyVecFrom2::transformed::<F>(
|
||||
name,
|
||||
v,
|
||||
source1.dateindex.boxed_clone(),
|
||||
dateindex_source2,
|
||||
),
|
||||
weekindex: period!(weekindex),
|
||||
monthindex: period!(monthindex),
|
||||
quarterindex: period!(quarterindex),
|
||||
semesterindex: period!(semesterindex),
|
||||
yearindex: period!(yearindex),
|
||||
decadeindex: period!(decadeindex),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ use brk_types::{
|
||||
use schemars::JsonSchema;
|
||||
use vecdb::{BinaryTransform, IterableCloneableVec};
|
||||
|
||||
use crate::internal::{ComputedVecValue, ComputedHeightDerivedSum, LazyBinaryTransformSum, NumericValue};
|
||||
use crate::internal::{
|
||||
ComputedFromHeightSumCum, ComputedHeightDerivedSum, ComputedVecValue, LazyBinaryTransformSum,
|
||||
LazyFromHeightLast, NumericValue,
|
||||
};
|
||||
|
||||
const VERSION: Version = Version::ZERO;
|
||||
|
||||
@@ -58,4 +61,74 @@ where
|
||||
decadeindex: period!(decadeindex),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from two LazyBinaryFromDateSum sources.
|
||||
pub fn from_binary<F, S1aT, S1bT, S2aT, S2bT>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source1: &LazyBinaryFromDateSum<S1T, S1aT, S1bT>,
|
||||
source2: &LazyBinaryFromDateSum<S2T, S2aT, S2bT>,
|
||||
) -> Self
|
||||
where
|
||||
F: BinaryTransform<S1T, S2T, T>,
|
||||
S1aT: ComputedVecValue + JsonSchema,
|
||||
S1bT: ComputedVecValue + JsonSchema,
|
||||
S2aT: ComputedVecValue + JsonSchema,
|
||||
S2bT: ComputedVecValue + JsonSchema,
|
||||
{
|
||||
let v = version + VERSION;
|
||||
|
||||
macro_rules! period {
|
||||
($p:ident) => {
|
||||
LazyBinaryTransformSum::from_boxed::<F>(name, v, source1.$p.boxed_clone(), source2.$p.boxed_clone())
|
||||
};
|
||||
}
|
||||
|
||||
Self {
|
||||
dateindex: period!(dateindex),
|
||||
weekindex: period!(weekindex),
|
||||
monthindex: period!(monthindex),
|
||||
quarterindex: period!(quarterindex),
|
||||
semesterindex: period!(semesterindex),
|
||||
yearindex: period!(yearindex),
|
||||
decadeindex: period!(decadeindex),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from a SumCum source (using only sum) and a LazyLast source.
|
||||
pub fn from_sumcum_lazy_last<F, S2ST>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source1: &ComputedFromHeightSumCum<S1T>,
|
||||
source2: &LazyFromHeightLast<S2T, S2ST>,
|
||||
) -> Self
|
||||
where
|
||||
F: BinaryTransform<S1T, S2T, T>,
|
||||
S2ST: ComputedVecValue + JsonSchema,
|
||||
{
|
||||
let v = version + VERSION;
|
||||
|
||||
// source1 has SumCum pattern with .dateindex.sum, .weekindex.sum, etc.
|
||||
// source2 has Last pattern via deref chain: .dates.dateindex, .dates.weekindex, etc.
|
||||
macro_rules! period {
|
||||
($p:ident) => {
|
||||
LazyBinaryTransformSum::from_boxed::<F>(
|
||||
name,
|
||||
v,
|
||||
source1.$p.sum.boxed_clone(),
|
||||
source2.dates.$p.boxed_clone(),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
Self {
|
||||
dateindex: period!(dateindex),
|
||||
weekindex: period!(weekindex),
|
||||
monthindex: period!(monthindex),
|
||||
quarterindex: period!(quarterindex),
|
||||
semesterindex: period!(semesterindex),
|
||||
yearindex: period!(yearindex),
|
||||
decadeindex: period!(decadeindex),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use vecdb::{BinaryTransform, IterableCloneableVec};
|
||||
use crate::internal::{
|
||||
ComputedFromHeightLast, ComputedFromHeightSumCum, ComputedHeightDerivedLast,
|
||||
ComputedHeightDerivedSumCum, ComputedVecValue, LazyBinaryTransformSumCum, LazyDateDerivedFull,
|
||||
LazyDateDerivedSumCum, NumericValue, SumCum,
|
||||
LazyDateDerivedSumCum, LazyFromHeightLast, NumericValue, SumCum,
|
||||
};
|
||||
|
||||
const VERSION: Version = Version::ZERO;
|
||||
@@ -278,4 +278,47 @@ where
|
||||
decadeindex: period!(decadeindex),
|
||||
}
|
||||
}
|
||||
|
||||
// --- Methods accepting SumCum + LazyLast sources ---
|
||||
|
||||
pub fn from_computed_lazy_last<F, S2ST>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source1: &ComputedFromHeightSumCum<S1T>,
|
||||
source2: &LazyFromHeightLast<S2T, S2ST>,
|
||||
) -> Self
|
||||
where
|
||||
F: BinaryTransform<S1T, S2T, T>,
|
||||
S1T: PartialOrd,
|
||||
S2T: NumericValue,
|
||||
S2ST: ComputedVecValue + JsonSchema,
|
||||
{
|
||||
let v = version + VERSION;
|
||||
|
||||
macro_rules! period {
|
||||
($p:ident) => {
|
||||
LazyBinaryTransformSumCum::from_sources_last_sum_raw::<F>(
|
||||
name, v,
|
||||
source1.rest.$p.sum.boxed_clone(),
|
||||
source1.rest.$p.cumulative.boxed_clone(),
|
||||
source2.rest.dates.$p.boxed_clone(),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
Self {
|
||||
dateindex: LazyBinaryTransformSumCum::from_sources_last_sum_raw::<F>(
|
||||
name, v,
|
||||
source1.dateindex.boxed_sum(),
|
||||
source1.dateindex.boxed_cumulative(),
|
||||
source2.rest.dates.dateindex.boxed_clone(),
|
||||
),
|
||||
weekindex: period!(weekindex),
|
||||
monthindex: period!(monthindex),
|
||||
quarterindex: period!(quarterindex),
|
||||
semesterindex: period!(semesterindex),
|
||||
yearindex: period!(yearindex),
|
||||
decadeindex: period!(decadeindex),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use schemars::JsonSchema;
|
||||
use vecdb::{BinaryTransform, IterableCloneableVec, LazyVecFrom1};
|
||||
|
||||
use super::{ComputedFromDateLast, LazyBinaryFromDateLast};
|
||||
use crate::internal::{ComputedFromHeightLast, ComputedVecValue, DollarsToSatsFract, LazyTransformLast, NumericValue};
|
||||
use crate::internal::{ComputedFromHeightLast, ComputedVecValue, DollarsToSatsFract, LazyFromHeightLast, LazyTransformLast, NumericValue};
|
||||
|
||||
/// Lazy binary price with both USD and sats representations.
|
||||
///
|
||||
@@ -71,6 +71,23 @@ where
|
||||
Self::from_dollars(name, version, dollars)
|
||||
}
|
||||
|
||||
/// Create from lazy height-based price and dateindex-based ratio sources.
|
||||
pub fn from_lazy_height_and_dateindex_last<F, S1SourceT>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source1: &LazyFromHeightLast<S1T, S1SourceT>,
|
||||
source2: &ComputedFromDateLast<S2T>,
|
||||
) -> Self
|
||||
where
|
||||
F: BinaryTransform<S1T, S2T, Dollars>,
|
||||
S1SourceT: ComputedVecValue + JsonSchema,
|
||||
{
|
||||
let dollars = LazyBinaryFromDateLast::from_lazy_height_and_dateindex_last::<F, S1SourceT>(
|
||||
name, version, source1, source2,
|
||||
);
|
||||
Self::from_dollars(name, version, dollars)
|
||||
}
|
||||
|
||||
/// Create from two computed dateindex sources.
|
||||
pub fn from_computed_both_last<F: BinaryTransform<S1T, S2T, Dollars>>(
|
||||
name: &str,
|
||||
|
||||
@@ -19,6 +19,8 @@ mod price;
|
||||
mod ratio;
|
||||
mod stddev;
|
||||
mod unary_last;
|
||||
mod value_change;
|
||||
mod value_change_derived;
|
||||
mod value_derived_last;
|
||||
mod value_last;
|
||||
mod value_lazy_last;
|
||||
@@ -44,6 +46,8 @@ pub use price::*;
|
||||
pub use ratio::*;
|
||||
pub use stddev::*;
|
||||
pub use unary_last::*;
|
||||
pub use value_change::*;
|
||||
pub use value_change_derived::*;
|
||||
pub use value_derived_last::*;
|
||||
pub use value_last::*;
|
||||
pub use value_lazy_last::*;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::{Traversable, TreeNode};
|
||||
use brk_types::{DateIndex, Dollars, Version};
|
||||
use brk_types::{DateIndex, Dollars, StoredF32, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{
|
||||
AnyExportableVec, AnyStoredVec, AnyVec, Database, EagerVec, Exit, GenericStoredVec, PcoVec,
|
||||
@@ -15,28 +15,77 @@ pub const PERCENTILES: [u8; 19] = [
|
||||
];
|
||||
pub const PERCENTILES_LEN: usize = PERCENTILES.len();
|
||||
|
||||
/// Compute spot percentile rank by interpolating within percentile bands.
|
||||
/// Returns a value between 0 and 100 indicating where spot sits in the distribution.
|
||||
pub fn compute_spot_percentile_rank(percentile_prices: &[Dollars; PERCENTILES_LEN], spot: Dollars) -> StoredF32 {
|
||||
if spot.is_nan() || percentile_prices[0].is_nan() {
|
||||
return StoredF32::NAN;
|
||||
}
|
||||
|
||||
let spot_f64 = f64::from(spot);
|
||||
|
||||
// Below lowest percentile (p5) - extrapolate towards 0
|
||||
let p5 = f64::from(percentile_prices[0]);
|
||||
if spot_f64 <= p5 {
|
||||
if p5 == 0.0 {
|
||||
return StoredF32::from(0.0);
|
||||
}
|
||||
// Linear extrapolation: rank = 5 * (spot / p5)
|
||||
return StoredF32::from((5.0 * spot_f64 / p5).max(0.0));
|
||||
}
|
||||
|
||||
// Above highest percentile (p95) - extrapolate towards 100
|
||||
let p95 = f64::from(percentile_prices[PERCENTILES_LEN - 1]);
|
||||
let p90 = f64::from(percentile_prices[PERCENTILES_LEN - 2]);
|
||||
if spot_f64 >= p95 {
|
||||
if p95 == p90 {
|
||||
return StoredF32::from(100.0);
|
||||
}
|
||||
// Linear extrapolation using p90-p95 slope
|
||||
let slope = 5.0 / (p95 - p90);
|
||||
return StoredF32::from((95.0 + (spot_f64 - p95) * slope).min(100.0));
|
||||
}
|
||||
|
||||
// Find the band containing spot and interpolate
|
||||
for i in 0..PERCENTILES_LEN - 1 {
|
||||
let lower = f64::from(percentile_prices[i]);
|
||||
let upper = f64::from(percentile_prices[i + 1]);
|
||||
|
||||
if spot_f64 >= lower && spot_f64 <= upper {
|
||||
let lower_pct = f64::from(PERCENTILES[i]);
|
||||
let upper_pct = f64::from(PERCENTILES[i + 1]);
|
||||
|
||||
if upper == lower {
|
||||
return StoredF32::from(lower_pct);
|
||||
}
|
||||
|
||||
// Linear interpolation
|
||||
let ratio = (spot_f64 - lower) / (upper - lower);
|
||||
return StoredF32::from(lower_pct + ratio * (upper_pct - lower_pct));
|
||||
}
|
||||
}
|
||||
|
||||
StoredF32::NAN
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CostBasisPercentiles {
|
||||
pub struct PercentilesVecs {
|
||||
pub vecs: [Option<Price>; PERCENTILES_LEN],
|
||||
}
|
||||
|
||||
const VERSION: Version = Version::ZERO;
|
||||
|
||||
impl CostBasisPercentiles {
|
||||
impl PercentilesVecs {
|
||||
pub fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
prefix: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
compute: bool,
|
||||
) -> Result<Self> {
|
||||
let vecs = PERCENTILES.map(|p| {
|
||||
compute.then(|| {
|
||||
let metric_name = if name.is_empty() {
|
||||
format!("cost_basis_pct{p:02}")
|
||||
} else {
|
||||
format!("{name}_cost_basis_pct{p:02}")
|
||||
};
|
||||
let metric_name = format!("{prefix}_pct{p:02}");
|
||||
Price::forced_import(db, &metric_name, version + VERSION, indexes).unwrap()
|
||||
})
|
||||
});
|
||||
@@ -88,7 +137,7 @@ impl CostBasisPercentiles {
|
||||
}
|
||||
}
|
||||
|
||||
impl CostBasisPercentiles {
|
||||
impl PercentilesVecs {
|
||||
pub fn write(&mut self) -> Result<()> {
|
||||
for vec in self.vecs.iter_mut().flatten() {
|
||||
vec.dateindex.write()?;
|
||||
@@ -115,7 +164,7 @@ impl CostBasisPercentiles {
|
||||
}
|
||||
}
|
||||
|
||||
impl Traversable for CostBasisPercentiles {
|
||||
impl Traversable for PercentilesVecs {
|
||||
fn to_tree_node(&self) -> TreeNode {
|
||||
TreeNode::Branch(
|
||||
PERCENTILES
|
||||
|
||||
@@ -17,7 +17,8 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{ComputedFromDateLast, Price};
|
||||
use crate::internal::ComputedFromHeightLast;
|
||||
use crate::internal::{ComputedFromHeightLast, ComputedVecValue, LazyFromHeightLast};
|
||||
use schemars::JsonSchema;
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct ComputedFromDateRatio {
|
||||
@@ -56,7 +57,6 @@ impl ComputedFromDateRatio {
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
extended: bool,
|
||||
price_vecs: Option<&price::Vecs>,
|
||||
) -> Result<Self> {
|
||||
let v = version + VERSION;
|
||||
|
||||
@@ -81,7 +81,8 @@ impl ComputedFromDateRatio {
|
||||
v,
|
||||
indexes,
|
||||
StandardDeviationVecsOptions::default().add_all(),
|
||||
price_vecs,
|
||||
metric_price,
|
||||
price.as_ref().map(|p| &p.dollars),
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
@@ -142,6 +143,82 @@ impl ComputedFromDateRatio {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn forced_import_from_lazy<S1T: ComputedVecValue + JsonSchema>(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
metric_price: &LazyFromHeightLast<Dollars, S1T>,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
extended: bool,
|
||||
) -> Result<Self> {
|
||||
let v = version + VERSION;
|
||||
|
||||
macro_rules! import {
|
||||
($suffix:expr) => {
|
||||
ComputedFromDateLast::forced_import(db, &format!("{name}_{}", $suffix), v, indexes)
|
||||
.unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! import_sd {
|
||||
($suffix:expr, $days:expr) => {
|
||||
ComputedFromDateStdDev::forced_import_from_lazy(
|
||||
db,
|
||||
&format!("{name}_{}", $suffix),
|
||||
$days,
|
||||
v,
|
||||
indexes,
|
||||
StandardDeviationVecsOptions::default().add_all(),
|
||||
Some(metric_price),
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
let ratio_pct99 = extended.then(|| import!("ratio_pct99"));
|
||||
let ratio_pct98 = extended.then(|| import!("ratio_pct98"));
|
||||
let ratio_pct95 = extended.then(|| import!("ratio_pct95"));
|
||||
let ratio_pct5 = extended.then(|| import!("ratio_pct5"));
|
||||
let ratio_pct2 = extended.then(|| import!("ratio_pct2"));
|
||||
let ratio_pct1 = extended.then(|| import!("ratio_pct1"));
|
||||
|
||||
macro_rules! lazy_usd {
|
||||
($ratio:expr, $suffix:expr) => {
|
||||
$ratio.as_ref().map(|r| {
|
||||
LazyBinaryPrice::from_lazy_height_and_dateindex_last::<PriceTimesRatio, S1T>(
|
||||
&format!("{name}_{}", $suffix),
|
||||
v,
|
||||
metric_price,
|
||||
r,
|
||||
)
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
ratio: import!("ratio"),
|
||||
ratio_1w_sma: extended.then(|| import!("ratio_1w_sma")),
|
||||
ratio_1m_sma: extended.then(|| import!("ratio_1m_sma")),
|
||||
ratio_sd: extended.then(|| import_sd!("ratio", usize::MAX)),
|
||||
ratio_1y_sd: extended.then(|| import_sd!("ratio_1y", 365)),
|
||||
ratio_2y_sd: extended.then(|| import_sd!("ratio_2y", 2 * 365)),
|
||||
ratio_4y_sd: extended.then(|| import_sd!("ratio_4y", 4 * 365)),
|
||||
ratio_pct99_usd: lazy_usd!(&ratio_pct99, "ratio_pct99_usd"),
|
||||
ratio_pct98_usd: lazy_usd!(&ratio_pct98, "ratio_pct98_usd"),
|
||||
ratio_pct95_usd: lazy_usd!(&ratio_pct95, "ratio_pct95_usd"),
|
||||
ratio_pct5_usd: lazy_usd!(&ratio_pct5, "ratio_pct5_usd"),
|
||||
ratio_pct2_usd: lazy_usd!(&ratio_pct2, "ratio_pct2_usd"),
|
||||
ratio_pct1_usd: lazy_usd!(&ratio_pct1, "ratio_pct1_usd"),
|
||||
price: None,
|
||||
ratio_pct99,
|
||||
ratio_pct98,
|
||||
ratio_pct95,
|
||||
ratio_pct5,
|
||||
ratio_pct2,
|
||||
ratio_pct1,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn compute_all<F>(
|
||||
&mut self,
|
||||
price: &price::Vecs,
|
||||
|
||||
@@ -2,15 +2,19 @@ use std::mem;
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Close, Date, DateIndex, Dollars, StoredF32, Version};
|
||||
use brk_types::{Date, DateIndex, Dollars, StoredF32, Version};
|
||||
use schemars::JsonSchema;
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, CollectableVec, Database, EagerVec, Exit, GenericStoredVec, IterableVec,
|
||||
PcoVec, VecIndex,
|
||||
};
|
||||
|
||||
use crate::{ComputeIndexes, indexes, price};
|
||||
use crate::{ComputeIndexes, indexes};
|
||||
|
||||
use crate::internal::{ClosePriceTimesRatio, ComputedFromDateLast, LazyBinaryPrice};
|
||||
use crate::internal::{
|
||||
ComputedFromDateLast, ComputedFromHeightLast, ComputedVecValue, LazyBinaryPrice,
|
||||
LazyFromHeightLast, PriceTimesRatio,
|
||||
};
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct ComputedFromDateStdDev {
|
||||
@@ -35,19 +39,19 @@ pub struct ComputedFromDateStdDev {
|
||||
pub m2_5sd: Option<ComputedFromDateLast<StoredF32>>,
|
||||
pub m3sd: Option<ComputedFromDateLast<StoredF32>>,
|
||||
|
||||
pub _0sd_usd: Option<LazyBinaryPrice<Close<Dollars>, StoredF32>>,
|
||||
pub p0_5sd_usd: Option<LazyBinaryPrice<Close<Dollars>, StoredF32>>,
|
||||
pub p1sd_usd: Option<LazyBinaryPrice<Close<Dollars>, StoredF32>>,
|
||||
pub p1_5sd_usd: Option<LazyBinaryPrice<Close<Dollars>, StoredF32>>,
|
||||
pub p2sd_usd: Option<LazyBinaryPrice<Close<Dollars>, StoredF32>>,
|
||||
pub p2_5sd_usd: Option<LazyBinaryPrice<Close<Dollars>, StoredF32>>,
|
||||
pub p3sd_usd: Option<LazyBinaryPrice<Close<Dollars>, StoredF32>>,
|
||||
pub m0_5sd_usd: Option<LazyBinaryPrice<Close<Dollars>, StoredF32>>,
|
||||
pub m1sd_usd: Option<LazyBinaryPrice<Close<Dollars>, StoredF32>>,
|
||||
pub m1_5sd_usd: Option<LazyBinaryPrice<Close<Dollars>, StoredF32>>,
|
||||
pub m2sd_usd: Option<LazyBinaryPrice<Close<Dollars>, StoredF32>>,
|
||||
pub m2_5sd_usd: Option<LazyBinaryPrice<Close<Dollars>, StoredF32>>,
|
||||
pub m3sd_usd: Option<LazyBinaryPrice<Close<Dollars>, StoredF32>>,
|
||||
pub _0sd_usd: Option<LazyBinaryPrice<Dollars, StoredF32>>,
|
||||
pub p0_5sd_usd: Option<LazyBinaryPrice<Dollars, StoredF32>>,
|
||||
pub p1sd_usd: Option<LazyBinaryPrice<Dollars, StoredF32>>,
|
||||
pub p1_5sd_usd: Option<LazyBinaryPrice<Dollars, StoredF32>>,
|
||||
pub p2sd_usd: Option<LazyBinaryPrice<Dollars, StoredF32>>,
|
||||
pub p2_5sd_usd: Option<LazyBinaryPrice<Dollars, StoredF32>>,
|
||||
pub p3sd_usd: Option<LazyBinaryPrice<Dollars, StoredF32>>,
|
||||
pub m0_5sd_usd: Option<LazyBinaryPrice<Dollars, StoredF32>>,
|
||||
pub m1sd_usd: Option<LazyBinaryPrice<Dollars, StoredF32>>,
|
||||
pub m1_5sd_usd: Option<LazyBinaryPrice<Dollars, StoredF32>>,
|
||||
pub m2sd_usd: Option<LazyBinaryPrice<Dollars, StoredF32>>,
|
||||
pub m2_5sd_usd: Option<LazyBinaryPrice<Dollars, StoredF32>>,
|
||||
pub m3sd_usd: Option<LazyBinaryPrice<Dollars, StoredF32>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -103,9 +107,10 @@ impl ComputedFromDateStdDev {
|
||||
parent_version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
options: StandardDeviationVecsOptions,
|
||||
price_vecs: Option<&price::Vecs>,
|
||||
metric_price: Option<&ComputedFromHeightLast<Dollars>>,
|
||||
date_price: Option<&ComputedFromDateLast<Dollars>>,
|
||||
) -> Result<Self> {
|
||||
let version = parent_version + Version::ONE;
|
||||
let version = parent_version + Version::TWO;
|
||||
|
||||
macro_rules! import {
|
||||
($suffix:expr) => {
|
||||
@@ -133,20 +138,33 @@ impl ComputedFromDateStdDev {
|
||||
let m2_5sd = options.bands().then(|| import!("m2_5sd"));
|
||||
let m3sd = options.bands().then(|| import!("m3sd"));
|
||||
|
||||
// Create USD bands using the metric price (the denominator of the ratio).
|
||||
// This converts ratio bands back to USD: usd_band = metric_price * ratio_band
|
||||
macro_rules! lazy_usd {
|
||||
($band:expr, $suffix:expr) => {
|
||||
price_vecs
|
||||
.map(|p| &p.usd.split.close)
|
||||
.zip($band.as_ref())
|
||||
.filter(|_| options.price_bands())
|
||||
.map(|(p, b)| {
|
||||
LazyBinaryPrice::from_computed_both_last::<ClosePriceTimesRatio>(
|
||||
if !options.price_bands() {
|
||||
None
|
||||
} else if let Some(mp) = metric_price {
|
||||
$band.as_ref().map(|b| {
|
||||
LazyBinaryPrice::from_height_and_dateindex_last::<PriceTimesRatio>(
|
||||
&format!("{name}_{}", $suffix),
|
||||
version,
|
||||
p,
|
||||
mp,
|
||||
b,
|
||||
)
|
||||
})
|
||||
} else if let Some(dp) = date_price {
|
||||
$band.as_ref().map(|b| {
|
||||
LazyBinaryPrice::from_computed_both_last::<PriceTimesRatio>(
|
||||
&format!("{name}_{}", $suffix),
|
||||
version,
|
||||
dp,
|
||||
b,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -395,4 +413,91 @@ impl ComputedFromDateStdDev {
|
||||
) -> impl Iterator<Item = &mut EagerVec<PcoVec<DateIndex, StoredF32>>> {
|
||||
self.mut_stateful_computed().map(|c| &mut c.dateindex)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn forced_import_from_lazy<S1T: ComputedVecValue + JsonSchema>(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
days: usize,
|
||||
parent_version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
options: StandardDeviationVecsOptions,
|
||||
metric_price: Option<&LazyFromHeightLast<Dollars, S1T>>,
|
||||
) -> Result<Self> {
|
||||
let version = parent_version + Version::TWO;
|
||||
|
||||
macro_rules! import {
|
||||
($suffix:expr) => {
|
||||
ComputedFromDateLast::forced_import(
|
||||
db,
|
||||
&format!("{name}_{}", $suffix),
|
||||
version,
|
||||
indexes,
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
let sma_vec = Some(import!("sma"));
|
||||
let p0_5sd = options.bands().then(|| import!("p0_5sd"));
|
||||
let p1sd = options.bands().then(|| import!("p1sd"));
|
||||
let p1_5sd = options.bands().then(|| import!("p1_5sd"));
|
||||
let p2sd = options.bands().then(|| import!("p2sd"));
|
||||
let p2_5sd = options.bands().then(|| import!("p2_5sd"));
|
||||
let p3sd = options.bands().then(|| import!("p3sd"));
|
||||
let m0_5sd = options.bands().then(|| import!("m0_5sd"));
|
||||
let m1sd = options.bands().then(|| import!("m1sd"));
|
||||
let m1_5sd = options.bands().then(|| import!("m1_5sd"));
|
||||
let m2sd = options.bands().then(|| import!("m2sd"));
|
||||
let m2_5sd = options.bands().then(|| import!("m2_5sd"));
|
||||
let m3sd = options.bands().then(|| import!("m3sd"));
|
||||
|
||||
macro_rules! lazy_usd {
|
||||
($band:expr, $suffix:expr) => {
|
||||
metric_price
|
||||
.zip($band.as_ref())
|
||||
.filter(|_| options.price_bands())
|
||||
.map(|(mp, b)| {
|
||||
LazyBinaryPrice::from_lazy_height_and_dateindex_last::<PriceTimesRatio, S1T>(
|
||||
&format!("{name}_{}", $suffix),
|
||||
version,
|
||||
mp,
|
||||
b,
|
||||
)
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
days,
|
||||
sd: import!("sd"),
|
||||
zscore: options.zscore().then(|| import!("zscore")),
|
||||
_0sd_usd: lazy_usd!(&sma_vec, "0sd_usd"),
|
||||
p0_5sd_usd: lazy_usd!(&p0_5sd, "p0_5sd_usd"),
|
||||
p1sd_usd: lazy_usd!(&p1sd, "p1sd_usd"),
|
||||
p1_5sd_usd: lazy_usd!(&p1_5sd, "p1_5sd_usd"),
|
||||
p2sd_usd: lazy_usd!(&p2sd, "p2sd_usd"),
|
||||
p2_5sd_usd: lazy_usd!(&p2_5sd, "p2_5sd_usd"),
|
||||
p3sd_usd: lazy_usd!(&p3sd, "p3sd_usd"),
|
||||
m0_5sd_usd: lazy_usd!(&m0_5sd, "m0_5sd_usd"),
|
||||
m1sd_usd: lazy_usd!(&m1sd, "m1sd_usd"),
|
||||
m1_5sd_usd: lazy_usd!(&m1_5sd, "m1_5sd_usd"),
|
||||
m2sd_usd: lazy_usd!(&m2sd, "m2sd_usd"),
|
||||
m2_5sd_usd: lazy_usd!(&m2_5sd, "m2_5sd_usd"),
|
||||
m3sd_usd: lazy_usd!(&m3sd, "m3sd_usd"),
|
||||
sma: sma_vec,
|
||||
p0_5sd,
|
||||
p1sd,
|
||||
p1_5sd,
|
||||
p2sd,
|
||||
p2_5sd,
|
||||
p3sd,
|
||||
m0_5sd,
|
||||
m1sd,
|
||||
m1_5sd,
|
||||
m2sd,
|
||||
m2_5sd,
|
||||
m3sd,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
//! Change values from DateIndex - stores signed sats (changes can be negative).
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{DateIndex, Dollars, Sats, SatsSigned, Version};
|
||||
use vecdb::{CollectableVec, Database, EagerVec, Exit, ImportableVec, IterableCloneableVec, PcoVec};
|
||||
|
||||
use crate::{ComputeIndexes, indexes, price};
|
||||
|
||||
use super::LazyValueChangeDateDerived;
|
||||
|
||||
const VERSION: Version = Version::ZERO;
|
||||
|
||||
/// Change values indexed by date - uses signed sats since changes can be negative.
|
||||
#[derive(Clone, Traversable)]
|
||||
#[traversable(merge)]
|
||||
pub struct ValueChangeFromDate {
|
||||
#[traversable(rename = "sats")]
|
||||
pub sats: EagerVec<PcoVec<DateIndex, SatsSigned>>,
|
||||
#[traversable(flatten)]
|
||||
pub rest: LazyValueChangeDateDerived,
|
||||
}
|
||||
|
||||
impl ValueChangeFromDate {
|
||||
pub fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
compute_dollars: bool,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
let sats = EagerVec::forced_import(db, name, version + VERSION)?;
|
||||
|
||||
let rest = LazyValueChangeDateDerived::from_source(
|
||||
db,
|
||||
name,
|
||||
sats.boxed_clone(),
|
||||
version + VERSION,
|
||||
compute_dollars,
|
||||
indexes,
|
||||
)?;
|
||||
|
||||
Ok(Self { sats, rest })
|
||||
}
|
||||
|
||||
/// Compute N-day change from unsigned sats source and optional dollars source.
|
||||
pub fn compute_change(
|
||||
&mut self,
|
||||
starting_dateindex: DateIndex,
|
||||
sats_source: &impl CollectableVec<DateIndex, Sats>,
|
||||
dollars_source: Option<&impl CollectableVec<DateIndex, Dollars>>,
|
||||
period: usize,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.sats
|
||||
.compute_change(starting_dateindex, sats_source, period, exit)?;
|
||||
|
||||
if let (Some(dollars), Some(source)) = (self.rest.dollars.as_mut(), dollars_source) {
|
||||
dollars
|
||||
.dateindex
|
||||
.compute_change(starting_dateindex, source, period, exit)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute dollars from price after sats change is computed.
|
||||
pub fn compute_dollars_from_price(
|
||||
&mut self,
|
||||
price: Option<&price::Vecs>,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.rest
|
||||
.compute_dollars_from_price(price, starting_indexes, exit)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//! Lazy derived values for change (bitcoin from sats, period aggregations).
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Bitcoin, DateIndex, Dollars, SatsSigned, Version};
|
||||
use vecdb::{Database, Exit, IterableBoxedVec};
|
||||
|
||||
use crate::{
|
||||
ComputeIndexes, indexes,
|
||||
internal::{ComputedFromDateLast, LazyDateDerivedLast, LazyFromDateLast, SatsSignedToBitcoin},
|
||||
price,
|
||||
traits::ComputeFromBitcoin,
|
||||
utils::OptionExt,
|
||||
};
|
||||
|
||||
const VERSION: Version = Version::ZERO;
|
||||
|
||||
/// Lazy derived values for change (bitcoin from sats, period aggregations).
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct LazyValueChangeDateDerived {
|
||||
pub sats: LazyDateDerivedLast<SatsSigned>,
|
||||
pub bitcoin: LazyFromDateLast<Bitcoin, SatsSigned>,
|
||||
pub dollars: Option<ComputedFromDateLast<Dollars>>,
|
||||
}
|
||||
|
||||
impl LazyValueChangeDateDerived {
|
||||
pub fn from_source(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
source: IterableBoxedVec<DateIndex, SatsSigned>,
|
||||
version: Version,
|
||||
compute_dollars: bool,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
let sats =
|
||||
LazyDateDerivedLast::from_source(name, version + VERSION, source.clone(), indexes);
|
||||
|
||||
let bitcoin = LazyFromDateLast::from_derived::<SatsSignedToBitcoin>(
|
||||
&format!("{name}_btc"),
|
||||
version + VERSION,
|
||||
source,
|
||||
&sats,
|
||||
);
|
||||
|
||||
let dollars = compute_dollars
|
||||
.then(|| {
|
||||
ComputedFromDateLast::forced_import(
|
||||
db,
|
||||
&format!("{name}_usd"),
|
||||
version + VERSION,
|
||||
indexes,
|
||||
)
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
Ok(Self {
|
||||
sats,
|
||||
bitcoin,
|
||||
dollars,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn compute_dollars_from_price(
|
||||
&mut self,
|
||||
price: Option<&price::Vecs>,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
if let Some(dollars) = self.dollars.as_mut() {
|
||||
let dateindex_to_bitcoin = &*self.bitcoin.dateindex;
|
||||
let dateindex_to_price_close = &price.u().usd.split.close.dateindex;
|
||||
|
||||
dollars.compute_all(starting_indexes, exit, |v| {
|
||||
v.compute_from_bitcoin(
|
||||
starting_indexes.dateindex,
|
||||
dateindex_to_bitcoin,
|
||||
dateindex_to_price_close,
|
||||
exit,
|
||||
)
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{DateIndex, Sats, Version};
|
||||
use brk_types::{DateIndex, Dollars, Sats, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{Database, EagerVec, Exit, ImportableVec, IterableCloneableVec, PcoVec};
|
||||
use vecdb::{CollectableVec, Database, EagerVec, Exit, ImportableVec, IterableCloneableVec, PcoVec};
|
||||
|
||||
use crate::{ComputeIndexes, indexes, price};
|
||||
|
||||
use super::LazyValueDateDerivedLast;
|
||||
use super::{ComputedFromDateLast, LazyValueDateDerivedLast};
|
||||
|
||||
#[derive(Clone, Deref, DerefMut, Traversable)]
|
||||
#[traversable(merge)]
|
||||
@@ -70,7 +70,7 @@ impl ValueFromDateLast {
|
||||
|
||||
pub fn compute_dollars<F>(&mut self, compute: F) -> Result<()>
|
||||
where
|
||||
F: FnMut(&mut crate::internal::ComputedFromDateLast<brk_types::Dollars>) -> Result<()>,
|
||||
F: FnMut(&mut ComputedFromDateLast<Dollars>) -> Result<()>,
|
||||
{
|
||||
self.rest.compute_dollars(compute)
|
||||
}
|
||||
@@ -84,4 +84,63 @@ impl ValueFromDateLast {
|
||||
self.rest
|
||||
.compute_dollars_from_price(price, starting_indexes, exit)
|
||||
}
|
||||
|
||||
/// Compute both sats and dollars using provided closures.
|
||||
pub fn compute_both<S, D>(
|
||||
&mut self,
|
||||
compute_sats: S,
|
||||
compute_dollars: D,
|
||||
) -> Result<()>
|
||||
where
|
||||
S: FnOnce(&mut EagerVec<PcoVec<DateIndex, Sats>>) -> Result<()>,
|
||||
D: FnOnce(&mut ComputedFromDateLast<Dollars>) -> Result<()>,
|
||||
{
|
||||
compute_sats(&mut self.sats_dateindex)?;
|
||||
if let Some(dollars) = self.rest.dollars.as_mut() {
|
||||
compute_dollars(dollars)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute EMA for sats and optionally dollars from source vecs.
|
||||
pub fn compute_ema(
|
||||
&mut self,
|
||||
starting_dateindex: DateIndex,
|
||||
sats_source: &impl CollectableVec<DateIndex, Sats>,
|
||||
dollars_source: Option<&impl CollectableVec<DateIndex, Dollars>>,
|
||||
period: usize,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.sats_dateindex
|
||||
.compute_ema(starting_dateindex, sats_source, period, exit)?;
|
||||
|
||||
if let (Some(dollars), Some(source)) = (self.rest.dollars.as_mut(), dollars_source) {
|
||||
dollars
|
||||
.dateindex
|
||||
.compute_ema(starting_dateindex, source, period, exit)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute N-day change for sats and optionally dollars from source vecs.
|
||||
pub fn compute_change(
|
||||
&mut self,
|
||||
starting_dateindex: DateIndex,
|
||||
sats_source: &impl CollectableVec<DateIndex, Sats>,
|
||||
dollars_source: Option<&impl CollectableVec<DateIndex, Dollars>>,
|
||||
period: usize,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.sats_dateindex
|
||||
.compute_change(starting_dateindex, sats_source, period, exit)?;
|
||||
|
||||
if let (Some(dollars), Some(source)) = (self.rest.dollars.as_mut(), dollars_source) {
|
||||
dollars
|
||||
.dateindex
|
||||
.compute_change(starting_dateindex, source, period, exit)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use vecdb::{BinaryTransform, IterableBoxedVec, IterableCloneableVec, LazyVecFrom
|
||||
use crate::internal::{
|
||||
ComputedFromHeightLast, ComputedFromHeightSumCum, ComputedFromHeightAndDateLast, ComputedVecValue,
|
||||
LazyBinaryComputedFromHeightLast, LazyBinaryFromDateLast, LazyBinaryHeightDerivedLast,
|
||||
LazyBinaryTransformLast, LazyDateDerivedLast, NumericValue,
|
||||
LazyBinaryTransformLast, LazyDateDerivedLast, LazyFromHeightLast, NumericValue,
|
||||
};
|
||||
|
||||
#[derive(Clone, Deref, DerefMut, Traversable)]
|
||||
@@ -369,4 +369,31 @@ where
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from a ComputedFromHeightAndDateLast and a LazyFromHeightLast.
|
||||
pub fn from_computed_height_date_and_lazy_block_last<F, S2SourceT>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source1: &ComputedFromHeightAndDateLast<S1T>,
|
||||
source2: &LazyFromHeightLast<S2T, S2SourceT>,
|
||||
) -> Self
|
||||
where
|
||||
F: BinaryTransform<S1T, S2T, T>,
|
||||
S1T: PartialOrd,
|
||||
S2SourceT: ComputedVecValue + JsonSchema,
|
||||
{
|
||||
let v = version + VERSION;
|
||||
|
||||
Self {
|
||||
height: LazyVecFrom2::transformed::<F>(
|
||||
name,
|
||||
v,
|
||||
source1.height.boxed_clone(),
|
||||
source2.height.boxed_clone(),
|
||||
),
|
||||
rest: LazyBinaryHeightDerivedLast::from_computed_height_date_and_lazy_block_last::<F, _>(
|
||||
name, v, source1, source2,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ use schemars::JsonSchema;
|
||||
use vecdb::{BinaryTransform, IterableBoxedVec, IterableCloneableVec, LazyVecFrom2};
|
||||
|
||||
use crate::internal::{
|
||||
ComputedFromHeightSum, ComputedHeightDerivedSum, ComputedVecValue, LazyBinaryHeightDerivedSum,
|
||||
NumericValue,
|
||||
ComputedFromHeightSum, ComputedFromHeightSumCum, ComputedHeightDerivedSum, ComputedVecValue,
|
||||
LazyBinaryHeightDerivedSum, LazyFromHeightLast, NumericValue,
|
||||
};
|
||||
|
||||
const VERSION: Version = Version::ZERO;
|
||||
@@ -68,4 +68,63 @@ where
|
||||
rest: LazyBinaryHeightDerivedSum::from_derived::<F>(name, v, &source1.rest, &source2.rest),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from two LazyBinaryFromHeightSum sources.
|
||||
pub fn from_binary<F, S1aT, S1bT, S2aT, S2bT>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source1: &LazyBinaryFromHeightSum<S1T, S1aT, S1bT>,
|
||||
source2: &LazyBinaryFromHeightSum<S2T, S2aT, S2bT>,
|
||||
) -> Self
|
||||
where
|
||||
F: BinaryTransform<S1T, S2T, T>,
|
||||
S1aT: ComputedVecValue + JsonSchema,
|
||||
S1bT: ComputedVecValue + JsonSchema,
|
||||
S2aT: ComputedVecValue + JsonSchema,
|
||||
S2bT: ComputedVecValue + JsonSchema,
|
||||
{
|
||||
let v = version + VERSION;
|
||||
|
||||
Self {
|
||||
height: LazyVecFrom2::transformed::<F>(
|
||||
name,
|
||||
v,
|
||||
source1.height.boxed_clone(),
|
||||
source2.height.boxed_clone(),
|
||||
),
|
||||
rest: LazyBinaryHeightDerivedSum::from_binary::<F, _, _, _, _>(
|
||||
name,
|
||||
v,
|
||||
&source1.rest,
|
||||
&source2.rest,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from a SumCum source (using only sum) and a LazyLast source.
|
||||
/// Produces sum-only output (no cumulative).
|
||||
pub fn from_sumcum_lazy_last<F, S2ST>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
height_source1: IterableBoxedVec<Height, S1T>,
|
||||
height_source2: IterableBoxedVec<Height, S2T>,
|
||||
source1: &ComputedFromHeightSumCum<S1T>,
|
||||
source2: &LazyFromHeightLast<S2T, S2ST>,
|
||||
) -> Self
|
||||
where
|
||||
F: BinaryTransform<S1T, S2T, T>,
|
||||
S2ST: ComputedVecValue + JsonSchema,
|
||||
{
|
||||
let v = version + VERSION;
|
||||
|
||||
Self {
|
||||
height: LazyVecFrom2::transformed::<F>(name, v, height_source1, height_source2),
|
||||
rest: LazyBinaryHeightDerivedSum::from_sumcum_lazy_last::<F, S2ST>(
|
||||
name,
|
||||
v,
|
||||
source1,
|
||||
source2,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use vecdb::{BinaryTransform, IterableBoxedVec, IterableCloneableVec, LazyVecFrom
|
||||
|
||||
use crate::internal::{
|
||||
ComputedFromHeightLast, ComputedFromHeightSumCum, ComputedHeightDerivedLast, ComputedHeightDerivedSumCum,
|
||||
ComputedVecValue, LazyBinaryHeightDerivedSumCum, NumericValue,
|
||||
ComputedVecValue, LazyBinaryHeightDerivedSumCum, LazyFromHeightLast, NumericValue,
|
||||
};
|
||||
|
||||
#[derive(Clone, Deref, DerefMut, Traversable)]
|
||||
@@ -207,4 +207,33 @@ where
|
||||
rest: LazyBinaryHeightDerivedSumCum::from_computed_derived_last::<F>(name, v, source1, source2),
|
||||
}
|
||||
}
|
||||
|
||||
// --- Methods accepting SumCum + LazyLast sources ---
|
||||
|
||||
pub fn from_computed_lazy_last<F, S2ST>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
height_source1: IterableBoxedVec<Height, S1T>,
|
||||
height_source2: IterableBoxedVec<Height, S2T>,
|
||||
source1: &ComputedFromHeightSumCum<S1T>,
|
||||
source2: &LazyFromHeightLast<S2T, S2ST>,
|
||||
) -> Self
|
||||
where
|
||||
F: BinaryTransform<S1T, S2T, T>,
|
||||
S1T: PartialOrd,
|
||||
S2T: NumericValue,
|
||||
S2ST: ComputedVecValue + JsonSchema,
|
||||
{
|
||||
let v = version + VERSION;
|
||||
Self {
|
||||
height: LazyVecFrom2::transformed::<F>(name, v, height_source1, height_source2),
|
||||
height_cumulative: LazyVecFrom2::transformed::<F>(
|
||||
&format!("{name}_cumulative"),
|
||||
v,
|
||||
source1.height_cumulative.boxed_clone(),
|
||||
source2.height.boxed_clone(),
|
||||
),
|
||||
rest: LazyBinaryHeightDerivedSumCum::from_computed_lazy_last::<F, S2ST>(name, v, source1, source2),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
//! Lazy price wrapper for height-based metrics with both USD and sats representations.
|
||||
//! Derives both from a cents base metric.
|
||||
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{CentsUnsigned, Dollars, SatsFract, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::IterableCloneableVec;
|
||||
|
||||
use super::{ComputedFromHeightLast, LazyFromHeightLast};
|
||||
use crate::internal::{CentsUnsignedToDollars, CentsUnsignedToSatsFract};
|
||||
|
||||
/// Lazy price metric (height-based) with both USD and sats representations.
|
||||
/// Both are lazily derived from a cents base metric.
|
||||
///
|
||||
/// Derefs to the dollars metric, so existing code works unchanged.
|
||||
/// Access `.sats` for the sats exchange rate version.
|
||||
#[derive(Clone, Deref, DerefMut, Traversable)]
|
||||
#[traversable(merge)]
|
||||
pub struct LazyPriceFromCents {
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
#[traversable(flatten)]
|
||||
pub dollars: LazyFromHeightLast<Dollars, CentsUnsigned>,
|
||||
pub sats: LazyFromHeightLast<SatsFract, CentsUnsigned>,
|
||||
}
|
||||
|
||||
impl LazyPriceFromCents {
|
||||
pub fn from_computed(
|
||||
name: &str,
|
||||
version: Version,
|
||||
cents: &ComputedFromHeightLast<CentsUnsigned>,
|
||||
) -> Self {
|
||||
let dollars = LazyFromHeightLast::from_computed::<CentsUnsignedToDollars>(
|
||||
name,
|
||||
version,
|
||||
cents.height.boxed_clone(),
|
||||
cents,
|
||||
);
|
||||
let sats = LazyFromHeightLast::from_computed::<CentsUnsignedToSatsFract>(
|
||||
&format!("{name}_sats"),
|
||||
version,
|
||||
cents.height.boxed_clone(),
|
||||
cents,
|
||||
);
|
||||
Self { dollars, sats }
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ mod lazy_binary_computed_sum_cum;
|
||||
mod lazy_computed_full;
|
||||
mod lazy_computed_sum_cum;
|
||||
mod lazy_last;
|
||||
mod lazy_price_from_cents;
|
||||
mod lazy_sum;
|
||||
mod price;
|
||||
mod unary_last;
|
||||
@@ -51,6 +52,7 @@ pub use lazy_binary_computed_sum_cum::*;
|
||||
pub use lazy_computed_full::*;
|
||||
pub use lazy_computed_sum_cum::*;
|
||||
pub use lazy_last::*;
|
||||
pub use lazy_price_from_cents::*;
|
||||
pub use lazy_sum::*;
|
||||
pub use price::*;
|
||||
pub use unary_last::*;
|
||||
|
||||
@@ -8,7 +8,7 @@ use vecdb::{BinaryTransform, IterableCloneableVec};
|
||||
|
||||
use crate::internal::{
|
||||
ComputedFromHeightLast, ComputedFromHeightSumCum, ComputedFromHeightAndDateLast, ComputedVecValue,
|
||||
LazyBinaryFromDateLast, LazyBinaryTransformLast, NumericValue,
|
||||
LazyBinaryFromDateLast, LazyBinaryTransformLast, LazyFromHeightLast, NumericValue,
|
||||
};
|
||||
|
||||
#[derive(Clone, Deref, DerefMut, Traversable)]
|
||||
@@ -141,4 +141,34 @@ where
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from a ComputedFromHeightAndDateLast and a LazyFromHeightLast.
|
||||
pub fn from_computed_height_date_and_lazy_block_last<F, S2SourceT>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source1: &ComputedFromHeightAndDateLast<S1T>,
|
||||
source2: &LazyFromHeightLast<S2T, S2SourceT>,
|
||||
) -> Self
|
||||
where
|
||||
F: BinaryTransform<S1T, S2T, T>,
|
||||
S1T: PartialOrd,
|
||||
S2SourceT: ComputedVecValue + JsonSchema,
|
||||
{
|
||||
let v = version + VERSION;
|
||||
|
||||
Self {
|
||||
dates: LazyBinaryFromDateLast::from_computed_and_lazy_last::<F, _>(
|
||||
name,
|
||||
v,
|
||||
&source1.rest,
|
||||
&source2.rest.dates,
|
||||
),
|
||||
difficultyepoch: LazyBinaryTransformLast::from_vecs::<F>(
|
||||
name,
|
||||
v,
|
||||
source1.difficultyepoch.boxed_clone(),
|
||||
source2.rest.difficultyepoch.boxed_clone(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ use derive_more::{Deref, DerefMut};
|
||||
use schemars::JsonSchema;
|
||||
use vecdb::{BinaryTransform, IterableCloneableVec};
|
||||
|
||||
use crate::internal::{ComputedVecValue, ComputedHeightDerivedSum, LazyBinaryFromDateSum, LazyBinaryTransformSum, NumericValue};
|
||||
use crate::internal::{
|
||||
ComputedFromHeightSumCum, ComputedHeightDerivedSum, ComputedVecValue, LazyBinaryFromDateSum,
|
||||
LazyBinaryTransformSum, LazyFromHeightLast, NumericValue,
|
||||
};
|
||||
|
||||
const VERSION: Version = Version::ZERO;
|
||||
|
||||
@@ -48,4 +51,65 @@ where
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from two LazyBinaryHeightDerivedSum sources.
|
||||
pub fn from_binary<F, S1aT, S1bT, S2aT, S2bT>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source1: &LazyBinaryHeightDerivedSum<S1T, S1aT, S1bT>,
|
||||
source2: &LazyBinaryHeightDerivedSum<S2T, S2aT, S2bT>,
|
||||
) -> Self
|
||||
where
|
||||
F: BinaryTransform<S1T, S2T, T>,
|
||||
S1aT: ComputedVecValue + JsonSchema,
|
||||
S1bT: ComputedVecValue + JsonSchema,
|
||||
S2aT: ComputedVecValue + JsonSchema,
|
||||
S2bT: ComputedVecValue + JsonSchema,
|
||||
{
|
||||
let v = version + VERSION;
|
||||
|
||||
Self {
|
||||
dates: LazyBinaryFromDateSum::from_binary::<F, _, _, _, _>(
|
||||
name,
|
||||
v,
|
||||
&source1.dates,
|
||||
&source2.dates,
|
||||
),
|
||||
difficultyepoch: LazyBinaryTransformSum::from_boxed::<F>(
|
||||
name,
|
||||
v,
|
||||
source1.difficultyepoch.boxed_clone(),
|
||||
source2.difficultyepoch.boxed_clone(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from a SumCum source (using only sum) and a LazyLast source.
|
||||
pub fn from_sumcum_lazy_last<F, S2ST>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source1: &ComputedFromHeightSumCum<S1T>,
|
||||
source2: &LazyFromHeightLast<S2T, S2ST>,
|
||||
) -> Self
|
||||
where
|
||||
F: BinaryTransform<S1T, S2T, T>,
|
||||
S2ST: ComputedVecValue + JsonSchema,
|
||||
{
|
||||
let v = version + VERSION;
|
||||
|
||||
Self {
|
||||
dates: LazyBinaryFromDateSum::from_sumcum_lazy_last::<F, S2ST>(
|
||||
name,
|
||||
v,
|
||||
source1,
|
||||
source2,
|
||||
),
|
||||
difficultyepoch: LazyBinaryTransformSum::from_boxed::<F>(
|
||||
name,
|
||||
v,
|
||||
source1.difficultyepoch.sum.boxed_clone(),
|
||||
source2.difficultyepoch.boxed_clone(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use vecdb::{BinaryTransform, IterableCloneableVec};
|
||||
use crate::internal::{
|
||||
ComputedFromHeightLast, ComputedFromHeightSumCum, ComputedHeightDerivedLast, ComputedHeightDerivedSumCum,
|
||||
ComputedVecValue, LazyBinaryFromDateSumCum, LazyBinaryTransformSumCum, LazyFull, LazyDateDerivedFull,
|
||||
LazyDateDerivedSumCum, LazySumCum, NumericValue, SumCum,
|
||||
LazyDateDerivedSumCum, LazyFromHeightLast, LazySumCum, NumericValue, SumCum,
|
||||
};
|
||||
|
||||
const VERSION: Version = Version::ZERO;
|
||||
@@ -221,4 +221,32 @@ where
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// --- Methods accepting SumCum + LazyLast sources ---
|
||||
|
||||
pub fn from_computed_lazy_last<F, S2ST>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source1: &ComputedFromHeightSumCum<S1T>,
|
||||
source2: &LazyFromHeightLast<S2T, S2ST>,
|
||||
) -> Self
|
||||
where
|
||||
F: BinaryTransform<S1T, S2T, T>,
|
||||
S1T: PartialOrd,
|
||||
S2T: NumericValue,
|
||||
S2ST: ComputedVecValue + schemars::JsonSchema,
|
||||
{
|
||||
let v = version + VERSION;
|
||||
|
||||
Self {
|
||||
dates: LazyBinaryFromDateSumCum::from_computed_lazy_last::<F, S2ST>(name, v, source1, source2),
|
||||
difficultyepoch: LazyBinaryTransformSumCum::from_sources_last_sum_raw::<F>(
|
||||
name,
|
||||
v,
|
||||
source1.difficultyepoch.sum.boxed_clone(),
|
||||
source1.difficultyepoch.cumulative.boxed_clone(),
|
||||
source2.rest.difficultyepoch.boxed_clone(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
use brk_types::{CentsUnsigned, Dollars};
|
||||
use vecdb::UnaryTransform;
|
||||
|
||||
/// CentsUnsigned -> Dollars (convert cents to dollars for display)
|
||||
pub struct CentsUnsignedToDollars;
|
||||
|
||||
impl UnaryTransform<CentsUnsigned, Dollars> for CentsUnsignedToDollars {
|
||||
#[inline(always)]
|
||||
fn apply(cents: CentsUnsigned) -> Dollars {
|
||||
cents.into()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use brk_types::{CentsUnsigned, SatsFract};
|
||||
use vecdb::UnaryTransform;
|
||||
|
||||
/// CentsUnsigned -> SatsFract (exchange rate: sats per dollar at this price level)
|
||||
/// Formula: sats = 100_000_000 / dollars = 100_000_000 / (cents / 100) = 10_000_000_000 / cents
|
||||
pub struct CentsUnsignedToSatsFract;
|
||||
|
||||
impl UnaryTransform<CentsUnsigned, SatsFract> for CentsUnsignedToSatsFract {
|
||||
#[inline(always)]
|
||||
fn apply(cents: CentsUnsigned) -> SatsFract {
|
||||
let cents_f64 = cents.inner() as f64;
|
||||
if cents_f64 == 0.0 {
|
||||
SatsFract::NAN
|
||||
} else {
|
||||
// sats = 1 BTC * 100 / cents = 10_000_000_000 / cents
|
||||
SatsFract::new(SatsFract::SATS_PER_BTC * 100.0 / cents_f64)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
use brk_types::{Close, Dollars, StoredF32};
|
||||
use vecdb::BinaryTransform;
|
||||
|
||||
/// Close<Dollars> * StoredF32 -> Dollars (price × ratio)
|
||||
/// Same as PriceTimesRatio but accepts Close<Dollars> price source.
|
||||
pub struct ClosePriceTimesRatio;
|
||||
|
||||
impl BinaryTransform<Close<Dollars>, StoredF32, Dollars> for ClosePriceTimesRatio {
|
||||
#[inline(always)]
|
||||
fn apply(price: Close<Dollars>, ratio: StoredF32) -> Dollars {
|
||||
*price * ratio
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
mod close_price_times_ratio;
|
||||
mod cents_unsigned_to_dollars;
|
||||
mod cents_unsigned_to_sats_fract;
|
||||
mod close_price_times_sats;
|
||||
mod difference_f32;
|
||||
mod dollar_halve;
|
||||
mod dollar_identity;
|
||||
mod dollars_to_sats_fract;
|
||||
mod dollar_minus;
|
||||
mod dollar_plus;
|
||||
mod dollar_times_tenths;
|
||||
mod dollars_to_sats_fract;
|
||||
mod f32_identity;
|
||||
mod half_close_price_times_sats;
|
||||
mod ohlc;
|
||||
@@ -18,7 +19,6 @@ mod percentage_u32_f32;
|
||||
mod percentage_u64_f32;
|
||||
mod price_times_ratio;
|
||||
mod ratio32;
|
||||
mod ratio32_neg;
|
||||
mod ratio_f32;
|
||||
mod ratio_u64_f32;
|
||||
mod return_f32_tenths;
|
||||
@@ -40,15 +40,16 @@ mod volatility_sqrt365;
|
||||
mod volatility_sqrt7;
|
||||
mod weight_to_fullness;
|
||||
|
||||
pub use close_price_times_ratio::*;
|
||||
pub use cents_unsigned_to_dollars::*;
|
||||
pub use cents_unsigned_to_sats_fract::*;
|
||||
pub use close_price_times_sats::*;
|
||||
pub use difference_f32::*;
|
||||
pub use dollar_halve::*;
|
||||
pub use dollar_identity::*;
|
||||
pub use dollars_to_sats_fract::*;
|
||||
pub use dollar_minus::*;
|
||||
pub use dollar_plus::*;
|
||||
pub use dollar_times_tenths::*;
|
||||
pub use dollars_to_sats_fract::*;
|
||||
pub use f32_identity::*;
|
||||
pub use half_close_price_times_sats::*;
|
||||
pub use ohlc::*;
|
||||
@@ -59,10 +60,9 @@ pub use percentage_sats_f64::*;
|
||||
pub use percentage_u32_f32::*;
|
||||
pub use percentage_u64_f32::*;
|
||||
pub use price_times_ratio::*;
|
||||
pub use ratio32::*;
|
||||
pub use ratio32_neg::*;
|
||||
pub use ratio_f32::*;
|
||||
pub use ratio_u64_f32::*;
|
||||
pub use ratio32::*;
|
||||
pub use return_f32_tenths::*;
|
||||
pub use return_i8::*;
|
||||
pub use return_u16::*;
|
||||
@@ -77,7 +77,7 @@ pub use sat_to_bitcoin::*;
|
||||
pub use sats_times_close_price::*;
|
||||
pub use u16_to_years::*;
|
||||
pub use u64_plus::*;
|
||||
pub use volatility_sqrt7::*;
|
||||
pub use volatility_sqrt30::*;
|
||||
pub use volatility_sqrt365::*;
|
||||
pub use volatility_sqrt7::*;
|
||||
pub use weight_to_fullness::*;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
use brk_types::{Dollars, StoredF32};
|
||||
use vecdb::BinaryTransform;
|
||||
|
||||
/// (Dollars, Dollars) -> -StoredF32 (negated ratio)
|
||||
/// Computes -(a/b) directly to avoid lazy-from-lazy chains.
|
||||
pub struct NegRatio32;
|
||||
|
||||
impl BinaryTransform<Dollars, Dollars, StoredF32> for NegRatio32 {
|
||||
#[inline(always)]
|
||||
fn apply(numerator: Dollars, denominator: Dollars) -> StoredF32 {
|
||||
-StoredF32::from(numerator / denominator)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use brk_types::{Bitcoin, Sats};
|
||||
use brk_types::{Bitcoin, Sats, SatsSigned};
|
||||
use vecdb::UnaryTransform;
|
||||
|
||||
/// Sats -> Bitcoin (divide by 1e8)
|
||||
@@ -10,3 +10,13 @@ impl UnaryTransform<Sats, Bitcoin> for SatsToBitcoin {
|
||||
Bitcoin::from(sats)
|
||||
}
|
||||
}
|
||||
|
||||
/// SatsSigned -> Bitcoin (divide by 1e8, preserves sign)
|
||||
pub struct SatsSignedToBitcoin;
|
||||
|
||||
impl UnaryTransform<SatsSigned, Bitcoin> for SatsSignedToBitcoin {
|
||||
#[inline(always)]
|
||||
fn apply(sats: SatsSigned) -> Bitcoin {
|
||||
Bitcoin::from(sats)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ impl Vecs {
|
||||
compute_period_profitability(
|
||||
&mut self.period_days_in_profit,
|
||||
&mut self.period_days_in_loss,
|
||||
&mut self.period_max_drawdown,
|
||||
&mut self.period_min_return,
|
||||
&mut self.period_max_return,
|
||||
&self.period_returns,
|
||||
starting_indexes,
|
||||
@@ -95,7 +95,7 @@ impl Vecs {
|
||||
compute_period_profitability(
|
||||
&mut self.period_lump_sum_days_in_profit,
|
||||
&mut self.period_lump_sum_days_in_loss,
|
||||
&mut self.period_lump_sum_max_drawdown,
|
||||
&mut self.period_lump_sum_min_return,
|
||||
&mut self.period_lump_sum_max_return,
|
||||
&self.period_lump_sum_returns,
|
||||
starting_indexes,
|
||||
@@ -130,7 +130,7 @@ impl Vecs {
|
||||
compute_class_profitability(
|
||||
&mut self.class_days_in_profit,
|
||||
&mut self.class_days_in_loss,
|
||||
&mut self.class_max_drawdown,
|
||||
&mut self.class_min_return,
|
||||
&mut self.class_max_return,
|
||||
&self.class_returns,
|
||||
starting_indexes,
|
||||
@@ -144,16 +144,16 @@ impl Vecs {
|
||||
fn compute_period_profitability(
|
||||
days_in_profit: &mut ByDcaPeriod<ComputedFromDateLast<StoredU32>>,
|
||||
days_in_loss: &mut ByDcaPeriod<ComputedFromDateLast<StoredU32>>,
|
||||
max_drawdown: &mut ByDcaPeriod<ComputedFromDateLast<StoredF32>>,
|
||||
min_return: &mut ByDcaPeriod<ComputedFromDateLast<StoredF32>>,
|
||||
max_return: &mut ByDcaPeriod<ComputedFromDateLast<StoredF32>>,
|
||||
returns: &ByDcaPeriod<LazyBinaryFromDateLast<StoredF32, Close<Dollars>, Dollars>>,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
for ((((dip, dil), md), mr), (ret, days)) in days_in_profit
|
||||
for ((((dip, dil), minr), maxr), (ret, days)) in days_in_profit
|
||||
.iter_mut()
|
||||
.zip(days_in_loss.iter_mut())
|
||||
.zip(max_drawdown.iter_mut())
|
||||
.zip(min_return.iter_mut())
|
||||
.zip(max_return.iter_mut())
|
||||
.zip(returns.iter_with_days())
|
||||
{
|
||||
@@ -177,7 +177,7 @@ fn compute_period_profitability(
|
||||
)?)
|
||||
})?;
|
||||
|
||||
md.compute_all(starting_indexes, exit, |v| {
|
||||
minr.compute_all(starting_indexes, exit, |v| {
|
||||
Ok(v.compute_min(
|
||||
starting_indexes.dateindex,
|
||||
&ret.dateindex,
|
||||
@@ -186,7 +186,7 @@ fn compute_period_profitability(
|
||||
)?)
|
||||
})?;
|
||||
|
||||
mr.compute_all(starting_indexes, exit, |v| {
|
||||
maxr.compute_all(starting_indexes, exit, |v| {
|
||||
Ok(v.compute_max(
|
||||
starting_indexes.dateindex,
|
||||
&ret.dateindex,
|
||||
@@ -201,7 +201,7 @@ fn compute_period_profitability(
|
||||
fn compute_class_profitability(
|
||||
days_in_profit: &mut ByDcaClass<ComputedFromDateLast<StoredU32>>,
|
||||
days_in_loss: &mut ByDcaClass<ComputedFromDateLast<StoredU32>>,
|
||||
max_drawdown: &mut ByDcaClass<ComputedFromDateLast<StoredF32>>,
|
||||
min_return: &mut ByDcaClass<ComputedFromDateLast<StoredF32>>,
|
||||
max_return: &mut ByDcaClass<ComputedFromDateLast<StoredF32>>,
|
||||
returns: &ByDcaClass<LazyBinaryFromDateLast<StoredF32, Close<Dollars>, Dollars>>,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
@@ -209,10 +209,10 @@ fn compute_class_profitability(
|
||||
) -> Result<()> {
|
||||
let dateindexes = ByDcaClass::<()>::dateindexes();
|
||||
|
||||
for (((((dip, dil), md), mr), ret), from) in days_in_profit
|
||||
for (((((dip, dil), minr), maxr), ret), from) in days_in_profit
|
||||
.iter_mut()
|
||||
.zip(days_in_loss.iter_mut())
|
||||
.zip(max_drawdown.iter_mut())
|
||||
.zip(min_return.iter_mut())
|
||||
.zip(max_return.iter_mut())
|
||||
.zip(returns.iter())
|
||||
.zip(dateindexes)
|
||||
@@ -237,7 +237,7 @@ fn compute_class_profitability(
|
||||
)?)
|
||||
})?;
|
||||
|
||||
md.compute_all(starting_indexes, exit, |v| {
|
||||
minr.compute_all(starting_indexes, exit, |v| {
|
||||
Ok(v.compute_all_time_low_from(
|
||||
starting_indexes.dateindex,
|
||||
&ret.dateindex,
|
||||
@@ -246,7 +246,7 @@ fn compute_class_profitability(
|
||||
)?)
|
||||
})?;
|
||||
|
||||
mr.compute_all(starting_indexes, exit, |v| {
|
||||
maxr.compute_all(starting_indexes, exit, |v| {
|
||||
Ok(v.compute_all_time_high_from(
|
||||
starting_indexes.dateindex,
|
||||
&ret.dateindex,
|
||||
|
||||
@@ -67,10 +67,10 @@ impl Vecs {
|
||||
)
|
||||
})?;
|
||||
|
||||
let period_max_drawdown = ByDcaPeriod::try_new(|name, _days| {
|
||||
let period_min_return = ByDcaPeriod::try_new(|name, _days| {
|
||||
ComputedFromDateLast::forced_import(
|
||||
db,
|
||||
&format!("{name}_dca_max_drawdown"),
|
||||
&format!("{name}_dca_min_return"),
|
||||
version,
|
||||
indexes,
|
||||
)
|
||||
@@ -130,10 +130,10 @@ impl Vecs {
|
||||
)
|
||||
})?;
|
||||
|
||||
let period_lump_sum_max_drawdown = ByDcaPeriod::try_new(|name, _days| {
|
||||
let period_lump_sum_min_return = ByDcaPeriod::try_new(|name, _days| {
|
||||
ComputedFromDateLast::forced_import(
|
||||
db,
|
||||
&format!("{name}_lump_sum_max_drawdown"),
|
||||
&format!("{name}_lump_sum_min_return"),
|
||||
version,
|
||||
indexes,
|
||||
)
|
||||
@@ -189,10 +189,10 @@ impl Vecs {
|
||||
)
|
||||
})?;
|
||||
|
||||
let class_max_drawdown = ByDcaClass::try_new(|name, _year, _dateindex| {
|
||||
let class_min_return = ByDcaClass::try_new(|name, _year, _dateindex| {
|
||||
ComputedFromDateLast::forced_import(
|
||||
db,
|
||||
&format!("{name}_max_drawdown"),
|
||||
&format!("{name}_min_return"),
|
||||
version,
|
||||
indexes,
|
||||
)
|
||||
@@ -214,20 +214,20 @@ impl Vecs {
|
||||
period_cagr,
|
||||
period_days_in_profit,
|
||||
period_days_in_loss,
|
||||
period_max_drawdown,
|
||||
period_min_return,
|
||||
period_max_return,
|
||||
period_lump_sum_stack,
|
||||
period_lump_sum_returns,
|
||||
period_lump_sum_days_in_profit,
|
||||
period_lump_sum_days_in_loss,
|
||||
period_lump_sum_max_drawdown,
|
||||
period_lump_sum_min_return,
|
||||
period_lump_sum_max_return,
|
||||
class_stack,
|
||||
class_average_price,
|
||||
class_returns,
|
||||
class_days_in_profit,
|
||||
class_days_in_loss,
|
||||
class_max_drawdown,
|
||||
class_min_return,
|
||||
class_max_return,
|
||||
})
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user