Compare commits

...

35 Commits

Author SHA1 Message Date
nym21 2a79211aee release: v0.1.8 2026-02-13 17:08:34 +01:00
nym21 cd5334215a docs: update generated docs 2026-02-13 17:08:14 +01:00
nym21 dfcb04484b global: snapshot 2026-02-13 16:54:09 +01:00
nym21 d18c872072 global: snapshot 2026-02-13 15:25:13 +01:00
nym21 80b2c636b0 global: snapshot 2026-02-13 13:54:09 +01:00
nym21 b779edc0d6 global: snapshot 2026-02-12 22:52:57 +01:00
nym21 3bc20a0a46 website: snapshot 2026-02-11 12:42:21 +01:00
nym21 121928bc57 website: chart style changes 2026-02-11 12:22:32 +01:00
nym21 1d63b8901d website: fetch on focus + split zscore charts 2026-02-10 11:47:51 +01:00
nym21 474c430ad1 deps: upgrade 2026-02-09 22:55:46 +01:00
nym21 f968ae4fd4 clients: bump versions 2026-02-08 16:41:31 +01:00
nym21 aa61e327f6 release: v0.1.7 2026-02-07 22:51:30 +01:00
nym21 605a8b86b8 docs: update generated docs 2026-02-07 22:51:10 +01:00
nym21 ba60b7e4f6 computer: fixes 2026-02-07 22:38:25 +01:00
nym21 9cba9bfec4 computer: snapshot 2026-02-06 21:40:34 +01:00
nym21 ed10e21ee9 release: v0.1.6 2026-02-05 23:23:32 +01:00
nym21 9d8fcbe866 docs: update generated docs 2026-02-05 23:23:11 +01:00
nym21 afe4123a17 computer: distribution: feat cost basis distribution 2026-02-05 23:10:02 +01:00
nym21 bbba8f4373 website: safari fixes 2026-02-05 11:43:40 +01:00
nym21 897aab032e release: v0.1.5 2026-02-05 10:43:42 +01:00
nym21 5b2c83ae6e docs: update generated docs 2026-02-05 10:43:23 +01:00
nym21 dc15cceb1e website: chart improvements 2026-02-05 10:31:28 +01:00
nym21 b5c2d6ce9e changelog: updated 2026-02-05 00:34:56 +01:00
nym21 0eeda63abb clients: versions 2026-02-04 23:32:21 +01:00
nym21 d4933ae314 release: v0.1.4 2026-02-04 22:45:32 +01:00
nym21 53ffe0e06c docs: update generated docs 2026-02-04 22:45:13 +01:00
nym21 0433e3b256 global: snapshot 2026-02-04 22:27:44 +01:00
nym21 9b409799c8 website: snapshot 2026-02-04 20:30:56 +01:00
nym21 dd96709d18 website: snapshot 2026-02-04 18:33:25 +01:00
nym21 3818a72045 website: snapshot 2026-02-04 17:48:06 +01:00
nym21 0437ce1bb4 website: snapshot 2026-02-04 17:26:35 +01:00
nym21 0d5d7da70f website: snapshot 2026-02-03 23:43:52 +01:00
nym21 277a0eb6a7 website: snapshot 2026-02-03 11:03:51 +01:00
nym21 c02fc37491 website: snapshot 2026-02-03 10:00:36 +01:00
nym21 1d440be352 clients: bump versions 2026-02-03 00:49:56 +01:00
572 changed files with 26171 additions and 14460 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ name: Check outdated dependencies
on:
schedule:
- cron: '0 9 * * *'
- cron: "0 9 * * 1"
workflow_dispatch:
jobs:
+1
View File
@@ -24,6 +24,7 @@ _*
/oracle*
/playground
/*.txt
/*.csv
# Logs
*.log*
Generated
+274 -85
View File
@@ -86,9 +86,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.100"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
[[package]]
name = "arrayvec"
@@ -98,9 +98,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-compression"
version = "0.4.37"
version = "0.4.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d10e4f991a553474232bc0a31799f6d24b034a84c0971d80d2e2f78b2e576e40"
checksum = "68650b7df54f0293fd061972a0fb05aaf4fc0879d3b3d21a638a182c5c543b9f"
dependencies = [
"compression-codecs",
"compression-core",
@@ -334,7 +334,7 @@ dependencies = [
[[package]]
name = "brk"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"brk_bencher",
"brk_bindgen",
@@ -358,7 +358,7 @@ dependencies = [
[[package]]
name = "brk_alloc"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"libmimalloc-sys",
"mimalloc",
@@ -366,7 +366,7 @@ dependencies = [
[[package]]
name = "brk_bencher"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"brk_error",
"brk_logger",
@@ -376,14 +376,14 @@ dependencies = [
[[package]]
name = "brk_bencher_visualizer"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"plotters",
]
[[package]]
name = "brk_bindgen"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"brk_cohort",
"brk_query",
@@ -396,7 +396,7 @@ dependencies = [
[[package]]
name = "brk_cli"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"anyhow",
"brk_alloc",
@@ -423,7 +423,7 @@ dependencies = [
[[package]]
name = "brk_client"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"brk_cohort",
"brk_types",
@@ -434,7 +434,7 @@ dependencies = [
[[package]]
name = "brk_cohort"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"brk_error",
"brk_traversable",
@@ -446,7 +446,7 @@ dependencies = [
[[package]]
name = "brk_computer"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"bitcoin",
"brk_alloc",
@@ -457,6 +457,7 @@ dependencies = [
"brk_indexer",
"brk_iterator",
"brk_logger",
"brk_oracle",
"brk_reader",
"brk_rpc",
"brk_store",
@@ -476,13 +477,14 @@ dependencies = [
[[package]]
name = "brk_error"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"bitcoin",
"bitcoincore-rpc",
"fjall",
"jiff",
"minreq",
"pco",
"serde_json",
"thiserror",
"tokio",
@@ -491,7 +493,7 @@ dependencies = [
[[package]]
name = "brk_fetcher"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"brk_error",
"brk_logger",
@@ -503,7 +505,7 @@ dependencies = [
[[package]]
name = "brk_indexer"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"bitcoin",
"brk_alloc",
@@ -528,7 +530,7 @@ dependencies = [
[[package]]
name = "brk_iterator"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"brk_error",
"brk_reader",
@@ -538,7 +540,7 @@ dependencies = [
[[package]]
name = "brk_logger"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"jiff",
"owo-colors",
@@ -549,7 +551,7 @@ dependencies = [
[[package]]
name = "brk_mempool"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"brk_error",
"brk_logger",
@@ -562,9 +564,19 @@ dependencies = [
"tracing",
]
[[package]]
name = "brk_oracle"
version = "0.1.8"
dependencies = [
"brk_indexer",
"brk_types",
"serde_json",
"vecdb",
]
[[package]]
name = "brk_query"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"bitcoin",
"brk_computer",
@@ -584,7 +596,7 @@ dependencies = [
[[package]]
name = "brk_reader"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"bitcoin",
"brk_error",
@@ -599,7 +611,7 @@ dependencies = [
[[package]]
name = "brk_rpc"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"bitcoin",
"bitcoincore-rpc",
@@ -612,7 +624,7 @@ dependencies = [
[[package]]
name = "brk_server"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"aide",
"axum",
@@ -644,7 +656,7 @@ dependencies = [
[[package]]
name = "brk_store"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"brk_error",
"brk_types",
@@ -655,7 +667,7 @@ dependencies = [
[[package]]
name = "brk_traversable"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"brk_traversable_derive",
"brk_types",
@@ -668,7 +680,7 @@ dependencies = [
[[package]]
name = "brk_traversable_derive"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"proc-macro2",
"quote",
@@ -677,7 +689,7 @@ dependencies = [
[[package]]
name = "brk_types"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"bitcoin",
"brk_error",
@@ -686,7 +698,9 @@ dependencies = [
"indexmap",
"itoa",
"jiff",
"pco",
"rapidhash",
"rustc-hash",
"ryu",
"schemars",
"serde",
@@ -698,7 +712,7 @@ dependencies = [
[[package]]
name = "brk_website"
version = "0.1.3"
version = "0.1.8"
dependencies = [
"axum",
"brk_logger",
@@ -758,21 +772,21 @@ 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"
version = "0.10.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dda4398f387cc6395a3e93b3867cd9abda914c97a0b344d1eefb2e5c51785fca"
checksum = "1c53ba0f290bfc610084c05582d9c5d421662128fc69f4bf236707af6fd321b9"
[[package]]
name = "cc"
version = "1.2.55"
version = "1.2.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -1028,9 +1042,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "ctrlc"
version = "3.5.1"
version = "3.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790"
checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
dependencies = [
"dispatch2",
"nix",
@@ -1135,9 +1149,9 @@ dependencies = [
[[package]]
name = "dtype_dispatch"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63cb69518c750a905135325cb009ffb24e1dff48cfc7bee026cd8e7e90d14f26"
checksum = "ab23e69df104e2fd85ee63a533a22d2132ef5975dc6b36f9f3e5a7305e4a8ed7"
[[package]]
name = "dwrote"
@@ -1241,9 +1255,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",
@@ -1264,6 +1278,12 @@ dependencies = [
"spin",
]
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "font-kit"
version = "0.14.3"
@@ -1448,6 +1468,19 @@ dependencies = [
"wasip2",
]
[[package]]
name = "getrandom"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
"wasip3",
]
[[package]]
name = "gif"
version = "0.12.0"
@@ -1487,6 +1520,15 @@ version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
@@ -1700,6 +1742,12 @@ dependencies = [
"zerovec",
]
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "idna"
version = "1.1.0"
@@ -1809,9 +1857,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jiff"
version = "0.2.18"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50"
checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543"
dependencies = [
"jiff-static",
"log",
@@ -1823,9 +1871,9 @@ dependencies = [
[[package]]
name = "jiff-static"
version = "0.2.18"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78"
checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5"
dependencies = [
"proc-macro2",
"quote",
@@ -1876,6 +1924,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "lexopt"
version = "0.3.1"
@@ -2011,9 +2065,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "memchr"
version = "2.7.6"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "memmap2"
@@ -2081,9 +2135,9 @@ dependencies = [
[[package]]
name = "nix"
version = "0.30.1"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66"
dependencies = [
"bitflags 2.10.0",
"cfg-if",
@@ -2213,9 +2267,9 @@ dependencies = [
[[package]]
name = "pco"
version = "1.0.0"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4acf50a078a796b341d327c18a0d3b2d2ca7dd630f04958558608169e2686e87"
checksum = "e89d71ab3c07ed898defa4915bdc2a963131d811a1eab0eeacfac65c94cdeae8"
dependencies = [
"better_io",
"dtype_dispatch",
@@ -2339,6 +2393,16 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
@@ -2425,18 +2489,18 @@ dependencies = [
[[package]]
name = "rapidhash"
version = "4.2.1"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d8b5b858a440a0bc02625b62dd95131b9201aa9f69f411195dd4a7cfb1de3d7"
checksum = "84816e4c99c467e92cf984ee6328caa976dfecd33a673544489d79ca2caaefe5"
dependencies = [
"rustversion",
]
[[package]]
name = "rawdb"
version = "0.6.5"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89f7fffcea393bd56abe3f7f691b88d5cb321e0b46b9caf1064bbcf2ec22180b"
checksum = "a66c17743b9a7e6a3bb8edb10fef25c62516e281b723ea38d7c1feea2035c75d"
dependencies = [
"libc",
"log",
@@ -2509,9 +2573,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",
@@ -2521,9 +2585,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",
@@ -2532,9 +2596,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"
@@ -2623,9 +2687,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.22"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "same-file"
@@ -2913,9 +2977,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.114"
version = "2.0.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12"
dependencies = [
"proc-macro2",
"quote",
@@ -2941,12 +3005,12 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.24.0"
version = "3.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"getrandom 0.4.1",
"once_cell",
"rustix",
"windows-sys 0.61.2",
@@ -3031,9 +3095,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.11+spec-1.1.0"
version = "1.0.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46"
checksum = "bbe30f93627849fa362d4a602212d41bb237dc2bd0f8ba0b2ce785012e124220"
dependencies = [
"indexmap",
"serde_core",
@@ -3046,18 +3110,18 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.7.5+spec-1.1.0"
version = "1.0.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.0.6+spec-1.1.0"
version = "1.0.8+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc"
dependencies = [
"winnow",
]
@@ -3199,9 +3263,9 @@ checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c"
[[package]]
name = "unicode-ident"
version = "1.0.22"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e"
[[package]]
name = "unicode-segmentation"
@@ -3254,9 +3318,9 @@ checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23"
[[package]]
name = "vecdb"
version = "0.6.5"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92a5c013b67f59b479557f9ee47b365e3802b33348b5000c9f21381b5249669d"
checksum = "16459a73939ec1c7ddb5c2f4264916f7bb96c88287b15dcce29cd95c16d2f6c0"
dependencies = [
"ctrlc",
"log",
@@ -3275,9 +3339,9 @@ dependencies = [
[[package]]
name = "vecdb_derive"
version = "0.6.5"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d90acf14f49129f0f205bc45331e2608dcb09c55d218e7fdbfdbf4b7c31b9433"
checksum = "e1845265e89f36a22175ebef07dc1340050ef3ec54aa9f9c84859d4dda0a3a03"
dependencies = [
"quote",
"syn",
@@ -3314,6 +3378,15 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.108"
@@ -3359,6 +3432,40 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags 2.10.0",
"hashbrown 0.15.5",
"indexmap",
"semver",
]
[[package]]
name = "web-sys"
version = "0.3.85"
@@ -3647,6 +3754,88 @@ name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags 2.10.0",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]]
name = "writeable"
@@ -3696,18 +3885,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.37"
version = "0.8.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac"
checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.37"
version = "0.8.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0"
checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517"
dependencies = [
"proc-macro2",
"quote",
@@ -3770,9 +3959,9 @@ dependencies = [
[[package]]
name = "zmij"
version = "1.0.19"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zstd"
+28 -26
View File
@@ -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.3"
package.version = "0.1.8"
package.homepage = "https://bitcoinresearchkit.org"
package.repository = "https://github.com/bitcoinresearchkit/brk"
package.readme = "README.md"
@@ -40,37 +40,39 @@ 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.3", path = "crates/brk_alloc" }
brk_bencher = { version = "0.1.3", path = "crates/brk_bencher" }
brk_bindgen = { version = "0.1.3", path = "crates/brk_bindgen" }
brk_cli = { version = "0.1.3", path = "crates/brk_cli" }
brk_client = { version = "0.1.3", path = "crates/brk_client" }
brk_cohort = { version = "0.1.3", path = "crates/brk_cohort" }
brk_computer = { version = "0.1.3", path = "crates/brk_computer" }
brk_error = { version = "0.1.3", path = "crates/brk_error" }
brk_fetcher = { version = "0.1.3", path = "crates/brk_fetcher" }
brk_indexer = { version = "0.1.3", path = "crates/brk_indexer" }
brk_iterator = { version = "0.1.3", path = "crates/brk_iterator" }
brk_logger = { version = "0.1.3", path = "crates/brk_logger" }
brk_mempool = { version = "0.1.3", path = "crates/brk_mempool" }
brk_query = { version = "0.1.3", path = "crates/brk_query", features = ["tokio"] }
brk_reader = { version = "0.1.3", path = "crates/brk_reader" }
brk_rpc = { version = "0.1.3", path = "crates/brk_rpc" }
brk_server = { version = "0.1.3", path = "crates/brk_server" }
brk_store = { version = "0.1.3", path = "crates/brk_store" }
brk_traversable = { version = "0.1.3", path = "crates/brk_traversable", features = ["pco", "derive"] }
brk_traversable_derive = { version = "0.1.3", path = "crates/brk_traversable_derive" }
brk_types = { version = "0.1.3", path = "crates/brk_types" }
brk_website = { version = "0.1.3", path = "crates/brk_website" }
byteview = "0.10.0"
brk_alloc = { version = "0.1.8", path = "crates/brk_alloc" }
brk_bencher = { version = "0.1.8", path = "crates/brk_bencher" }
brk_bindgen = { version = "0.1.8", path = "crates/brk_bindgen" }
brk_cli = { version = "0.1.8", path = "crates/brk_cli" }
brk_client = { version = "0.1.8", path = "crates/brk_client" }
brk_cohort = { version = "0.1.8", path = "crates/brk_cohort" }
brk_computer = { version = "0.1.8", path = "crates/brk_computer" }
brk_error = { version = "0.1.8", path = "crates/brk_error" }
brk_fetcher = { version = "0.1.8", path = "crates/brk_fetcher" }
brk_indexer = { version = "0.1.8", path = "crates/brk_indexer" }
brk_iterator = { version = "0.1.8", path = "crates/brk_iterator" }
brk_logger = { version = "0.1.8", path = "crates/brk_logger" }
brk_oracle = { version = "0.1.8", path = "crates/brk_oracle" }
brk_mempool = { version = "0.1.8", path = "crates/brk_mempool" }
brk_query = { version = "0.1.8", path = "crates/brk_query", features = ["tokio"] }
brk_reader = { version = "0.1.8", path = "crates/brk_reader" }
brk_rpc = { version = "0.1.8", path = "crates/brk_rpc" }
brk_server = { version = "0.1.8", path = "crates/brk_server" }
brk_store = { version = "0.1.8", path = "crates/brk_store" }
brk_traversable = { version = "0.1.8", path = "crates/brk_traversable", features = ["pco", "derive"] }
brk_traversable_derive = { version = "0.1.8", path = "crates/brk_traversable_derive" }
brk_types = { version = "0.1.8", path = "crates/brk_types" }
brk_website = { version = "0.1.8", path = "crates/brk_website" }
byteview = "0.10.1"
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 }
jiff = { version = "0.2.20", 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"
pco = "1.0.1"
rayon = "1.11.0"
rustc-hash = "2.1.1"
schemars = { version = "1.2.1", features = ["indexmap2"] }
@@ -83,7 +85,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.5", 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]
@@ -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;
}}
@@ -10,7 +10,7 @@ pub fn js_type_to_rust(js_type: &str) -> String {
"integer" => "i64".to_string(),
"number" => "f64".to_string(),
"boolean" => "bool".to_string(),
"*" => "serde_json::Value".to_string(),
"*" | "Object" => "serde_json::Value".to_string(),
other => other.to_string(),
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ owo-colors = { workspace = true }
tracing = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
toml = "0.9.11"
toml = "1.0.1"
vecdb = { workspace = true }
[[bin]]
+234 -91
View File
@@ -858,7 +858,7 @@ impl<T: DeserializeOwned> MetricPattern<T> for MetricPattern32<T> { fn get(&self
// Reusable pattern structs
/// Pattern struct for repeated tree structure.
pub struct AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern {
pub struct AdjustedCapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern {
pub adjusted_sopr: MetricPattern6<StoredF64>,
pub adjusted_sopr_30d_ema: MetricPattern6<StoredF64>,
pub adjusted_sopr_7d_ema: MetricPattern6<StoredF64>,
@@ -872,6 +872,7 @@ pub struct AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSe
pub investor_price_extra: RatioPattern,
pub loss_value_created: MetricPattern1<Dollars>,
pub loss_value_destroyed: MetricPattern1<Dollars>,
pub lower_price_band: DollarsSatsPattern,
pub mvrv: MetricPattern4<StoredF32>,
pub neg_realized_loss: CumulativeSumPattern2<Dollars>,
pub net_realized_pnl: CumulativeSumPattern<Dollars>,
@@ -910,11 +911,12 @@ pub struct AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSe
pub sopr_30d_ema: MetricPattern6<StoredF64>,
pub sopr_7d_ema: MetricPattern6<StoredF64>,
pub total_realized_pnl: MetricPattern1<Dollars>,
pub upper_price_band: DollarsSatsPattern,
pub value_created: MetricPattern1<Dollars>,
pub value_destroyed: MetricPattern1<Dollars>,
}
impl AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern {
impl AdjustedCapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
@@ -931,6 +933,7 @@ impl AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSopr
investor_price_extra: RatioPattern::new(client.clone(), _m(&acc, "investor_price_ratio")),
loss_value_created: MetricPattern1::new(client.clone(), _m(&acc, "loss_value_created")),
loss_value_destroyed: MetricPattern1::new(client.clone(), _m(&acc, "loss_value_destroyed")),
lower_price_band: DollarsSatsPattern::new(client.clone(), _m(&acc, "lower_price_band")),
mvrv: MetricPattern4::new(client.clone(), _m(&acc, "mvrv")),
neg_realized_loss: CumulativeSumPattern2::new(client.clone(), _m(&acc, "neg_realized_loss")),
net_realized_pnl: CumulativeSumPattern::new(client.clone(), _m(&acc, "net_realized_pnl")),
@@ -969,6 +972,7 @@ impl AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSopr
sopr_30d_ema: MetricPattern6::new(client.clone(), _m(&acc, "sopr_30d_ema")),
sopr_7d_ema: MetricPattern6::new(client.clone(), _m(&acc, "sopr_7d_ema")),
total_realized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "total_realized_pnl")),
upper_price_band: DollarsSatsPattern::new(client.clone(), _m(&acc, "upper_price_band")),
value_created: MetricPattern1::new(client.clone(), _m(&acc, "value_created")),
value_destroyed: MetricPattern1::new(client.clone(), _m(&acc, "value_destroyed")),
}
@@ -976,7 +980,7 @@ impl AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSopr
}
/// Pattern struct for repeated tree structure.
pub struct AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern2 {
pub struct AdjustedCapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern2 {
pub adjusted_sopr: MetricPattern6<StoredF64>,
pub adjusted_sopr_30d_ema: MetricPattern6<StoredF64>,
pub adjusted_sopr_7d_ema: MetricPattern6<StoredF64>,
@@ -990,6 +994,7 @@ pub struct AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSe
pub investor_price_extra: RatioPattern2,
pub loss_value_created: MetricPattern1<Dollars>,
pub loss_value_destroyed: MetricPattern1<Dollars>,
pub lower_price_band: DollarsSatsPattern,
pub mvrv: MetricPattern4<StoredF32>,
pub neg_realized_loss: CumulativeSumPattern2<Dollars>,
pub net_realized_pnl: CumulativeSumPattern<Dollars>,
@@ -1026,11 +1031,12 @@ pub struct AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSe
pub sopr_30d_ema: MetricPattern6<StoredF64>,
pub sopr_7d_ema: MetricPattern6<StoredF64>,
pub total_realized_pnl: MetricPattern1<Dollars>,
pub upper_price_band: DollarsSatsPattern,
pub value_created: MetricPattern1<Dollars>,
pub value_destroyed: MetricPattern1<Dollars>,
}
impl AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern2 {
impl AdjustedCapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern2 {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
@@ -1047,6 +1053,7 @@ impl AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSopr
investor_price_extra: RatioPattern2::new(client.clone(), _m(&acc, "investor_price_ratio")),
loss_value_created: MetricPattern1::new(client.clone(), _m(&acc, "loss_value_created")),
loss_value_destroyed: MetricPattern1::new(client.clone(), _m(&acc, "loss_value_destroyed")),
lower_price_band: DollarsSatsPattern::new(client.clone(), _m(&acc, "lower_price_band")),
mvrv: MetricPattern4::new(client.clone(), _m(&acc, "mvrv")),
neg_realized_loss: CumulativeSumPattern2::new(client.clone(), _m(&acc, "neg_realized_loss")),
net_realized_pnl: CumulativeSumPattern::new(client.clone(), _m(&acc, "net_realized_pnl")),
@@ -1083,6 +1090,7 @@ impl AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSopr
sopr_30d_ema: MetricPattern6::new(client.clone(), _m(&acc, "sopr_30d_ema")),
sopr_7d_ema: MetricPattern6::new(client.clone(), _m(&acc, "sopr_7d_ema")),
total_realized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "total_realized_pnl")),
upper_price_band: DollarsSatsPattern::new(client.clone(), _m(&acc, "upper_price_band")),
value_created: MetricPattern1::new(client.clone(), _m(&acc, "value_created")),
value_destroyed: MetricPattern1::new(client.clone(), _m(&acc, "value_destroyed")),
}
@@ -1090,7 +1098,7 @@ impl AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSopr
}
/// Pattern struct for repeated tree structure.
pub struct CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern2 {
pub struct CapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern2 {
pub cap_raw: MetricPattern11<CentsSats>,
pub capitulation_flow: MetricPattern1<Dollars>,
pub investor_cap_raw: MetricPattern11<CentsSquaredSats>,
@@ -1099,6 +1107,7 @@ pub struct CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTo
pub investor_price_extra: RatioPattern,
pub loss_value_created: MetricPattern1<Dollars>,
pub loss_value_destroyed: MetricPattern1<Dollars>,
pub lower_price_band: DollarsSatsPattern,
pub mvrv: MetricPattern4<StoredF32>,
pub neg_realized_loss: CumulativeSumPattern2<Dollars>,
pub net_realized_pnl: CumulativeSumPattern<Dollars>,
@@ -1137,11 +1146,12 @@ pub struct CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTo
pub sopr_30d_ema: MetricPattern6<StoredF64>,
pub sopr_7d_ema: MetricPattern6<StoredF64>,
pub total_realized_pnl: MetricPattern1<Dollars>,
pub upper_price_band: DollarsSatsPattern,
pub value_created: MetricPattern1<Dollars>,
pub value_destroyed: MetricPattern1<Dollars>,
}
impl CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern2 {
impl CapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern2 {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
@@ -1153,6 +1163,7 @@ impl CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalVal
investor_price_extra: RatioPattern::new(client.clone(), _m(&acc, "investor_price_ratio")),
loss_value_created: MetricPattern1::new(client.clone(), _m(&acc, "loss_value_created")),
loss_value_destroyed: MetricPattern1::new(client.clone(), _m(&acc, "loss_value_destroyed")),
lower_price_band: DollarsSatsPattern::new(client.clone(), _m(&acc, "lower_price_band")),
mvrv: MetricPattern4::new(client.clone(), _m(&acc, "mvrv")),
neg_realized_loss: CumulativeSumPattern2::new(client.clone(), _m(&acc, "neg_realized_loss")),
net_realized_pnl: CumulativeSumPattern::new(client.clone(), _m(&acc, "net_realized_pnl")),
@@ -1191,6 +1202,7 @@ impl CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalVal
sopr_30d_ema: MetricPattern6::new(client.clone(), _m(&acc, "sopr_30d_ema")),
sopr_7d_ema: MetricPattern6::new(client.clone(), _m(&acc, "sopr_7d_ema")),
total_realized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "total_realized_pnl")),
upper_price_band: DollarsSatsPattern::new(client.clone(), _m(&acc, "upper_price_band")),
value_created: MetricPattern1::new(client.clone(), _m(&acc, "value_created")),
value_destroyed: MetricPattern1::new(client.clone(), _m(&acc, "value_destroyed")),
}
@@ -1198,7 +1210,7 @@ impl CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalVal
}
/// Pattern struct for repeated tree structure.
pub struct CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern {
pub struct CapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern {
pub cap_raw: MetricPattern11<CentsSats>,
pub capitulation_flow: MetricPattern1<Dollars>,
pub investor_cap_raw: MetricPattern11<CentsSquaredSats>,
@@ -1207,6 +1219,7 @@ pub struct CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTo
pub investor_price_extra: RatioPattern2,
pub loss_value_created: MetricPattern1<Dollars>,
pub loss_value_destroyed: MetricPattern1<Dollars>,
pub lower_price_band: DollarsSatsPattern,
pub mvrv: MetricPattern4<StoredF32>,
pub neg_realized_loss: CumulativeSumPattern2<Dollars>,
pub net_realized_pnl: CumulativeSumPattern<Dollars>,
@@ -1243,11 +1256,12 @@ pub struct CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTo
pub sopr_30d_ema: MetricPattern6<StoredF64>,
pub sopr_7d_ema: MetricPattern6<StoredF64>,
pub total_realized_pnl: MetricPattern1<Dollars>,
pub upper_price_band: DollarsSatsPattern,
pub value_created: MetricPattern1<Dollars>,
pub value_destroyed: MetricPattern1<Dollars>,
}
impl CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern {
impl CapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
@@ -1259,6 +1273,7 @@ impl CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalVal
investor_price_extra: RatioPattern2::new(client.clone(), _m(&acc, "investor_price_ratio")),
loss_value_created: MetricPattern1::new(client.clone(), _m(&acc, "loss_value_created")),
loss_value_destroyed: MetricPattern1::new(client.clone(), _m(&acc, "loss_value_destroyed")),
lower_price_band: DollarsSatsPattern::new(client.clone(), _m(&acc, "lower_price_band")),
mvrv: MetricPattern4::new(client.clone(), _m(&acc, "mvrv")),
neg_realized_loss: CumulativeSumPattern2::new(client.clone(), _m(&acc, "neg_realized_loss")),
net_realized_pnl: CumulativeSumPattern::new(client.clone(), _m(&acc, "net_realized_pnl")),
@@ -1295,6 +1310,7 @@ impl CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalVal
sopr_30d_ema: MetricPattern6::new(client.clone(), _m(&acc, "sopr_30d_ema")),
sopr_7d_ema: MetricPattern6::new(client.clone(), _m(&acc, "sopr_7d_ema")),
total_realized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "total_realized_pnl")),
upper_price_band: DollarsSatsPattern::new(client.clone(), _m(&acc, "upper_price_band")),
value_created: MetricPattern1::new(client.clone(), _m(&acc, "value_created")),
value_destroyed: MetricPattern1::new(client.clone(), _m(&acc, "value_destroyed")),
}
@@ -2025,32 +2041,62 @@ impl<T: DeserializeOwned> AverageCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPa
}
}
/// Pattern struct for repeated tree structure.
pub struct ActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern {
pub activity: CoinblocksCoindaysSatblocksSatdaysSentPattern,
pub addr_count: MetricPattern1<StoredU64>,
pub addr_count_30d_change: MetricPattern4<StoredF64>,
pub cost_basis: MaxMinPattern,
pub outputs: UtxoPattern,
pub realized: CapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern,
pub relative: InvestedNegNetNuplSupplyUnrealizedPattern,
pub supply: _30dHalvedTotalPattern,
pub unrealized: GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern,
}
impl ActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
activity: CoinblocksCoindaysSatblocksSatdaysSentPattern::new(client.clone(), acc.clone()),
addr_count: MetricPattern1::new(client.clone(), _m(&acc, "addr_count")),
addr_count_30d_change: MetricPattern4::new(client.clone(), _m(&acc, "addr_count_30d_change")),
cost_basis: MaxMinPattern::new(client.clone(), acc.clone()),
outputs: UtxoPattern::new(client.clone(), _m(&acc, "utxo_count")),
realized: CapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern::new(client.clone(), acc.clone()),
relative: InvestedNegNetNuplSupplyUnrealizedPattern::new(client.clone(), acc.clone()),
supply: _30dHalvedTotalPattern::new(client.clone(), acc.clone()),
unrealized: GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern {
pub all: MetricPattern1<StoredU64>,
pub p2a: MetricPattern1<StoredU64>,
pub p2pk33: MetricPattern1<StoredU64>,
pub p2pk65: MetricPattern1<StoredU64>,
pub p2pkh: MetricPattern1<StoredU64>,
pub p2sh: MetricPattern1<StoredU64>,
pub p2tr: MetricPattern1<StoredU64>,
pub p2wpkh: MetricPattern1<StoredU64>,
pub p2wsh: MetricPattern1<StoredU64>,
pub all: _30dCountPattern,
pub p2a: _30dCountPattern,
pub p2pk33: _30dCountPattern,
pub p2pk65: _30dCountPattern,
pub p2pkh: _30dCountPattern,
pub p2sh: _30dCountPattern,
pub p2tr: _30dCountPattern,
pub p2wpkh: _30dCountPattern,
pub p2wsh: _30dCountPattern,
}
impl AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
all: MetricPattern1::new(client.clone(), acc.clone()),
p2a: MetricPattern1::new(client.clone(), _p("p2a", &acc)),
p2pk33: MetricPattern1::new(client.clone(), _p("p2pk33", &acc)),
p2pk65: MetricPattern1::new(client.clone(), _p("p2pk65", &acc)),
p2pkh: MetricPattern1::new(client.clone(), _p("p2pkh", &acc)),
p2sh: MetricPattern1::new(client.clone(), _p("p2sh", &acc)),
p2tr: MetricPattern1::new(client.clone(), _p("p2tr", &acc)),
p2wpkh: MetricPattern1::new(client.clone(), _p("p2wpkh", &acc)),
p2wsh: MetricPattern1::new(client.clone(), _p("p2wsh", &acc)),
all: _30dCountPattern::new(client.clone(), acc.clone()),
p2a: _30dCountPattern::new(client.clone(), _p("p2a", &acc)),
p2pk33: _30dCountPattern::new(client.clone(), _p("p2pk33", &acc)),
p2pk65: _30dCountPattern::new(client.clone(), _p("p2pk65", &acc)),
p2pkh: _30dCountPattern::new(client.clone(), _p("p2pkh", &acc)),
p2sh: _30dCountPattern::new(client.clone(), _p("p2sh", &acc)),
p2tr: _30dCountPattern::new(client.clone(), _p("p2tr", &acc)),
p2wpkh: _30dCountPattern::new(client.clone(), _p("p2wpkh", &acc)),
p2wsh: _30dCountPattern::new(client.clone(), _p("p2wsh", &acc)),
}
}
}
@@ -2115,34 +2161,6 @@ impl<T: DeserializeOwned> AverageBaseMaxMedianMinPct10Pct25Pct75Pct90Pattern<T>
}
}
/// Pattern struct for repeated tree structure.
pub struct ActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern {
pub activity: CoinblocksCoindaysSatblocksSatdaysSentPattern,
pub addr_count: MetricPattern1<StoredU64>,
pub cost_basis: MaxMinPattern,
pub outputs: UtxoPattern,
pub realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern,
pub relative: InvestedNegNetNuplSupplyUnrealizedPattern,
pub supply: _30dHalvedTotalPattern,
pub unrealized: GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern,
}
impl ActivityAddrCostOutputsRealizedRelativeSupplyUnrealizedPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
activity: CoinblocksCoindaysSatblocksSatdaysSentPattern::new(client.clone(), acc.clone()),
addr_count: MetricPattern1::new(client.clone(), _m(&acc, "addr_count")),
cost_basis: MaxMinPattern::new(client.clone(), acc.clone()),
outputs: UtxoPattern::new(client.clone(), _m(&acc, "utxo_count")),
realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern::new(client.clone(), acc.clone()),
relative: InvestedNegNetNuplSupplyUnrealizedPattern::new(client.clone(), acc.clone()),
supply: _30dHalvedTotalPattern::new(client.clone(), acc.clone()),
unrealized: GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct _10y2y3y4y5y6y8yPattern {
pub _10y: MetricPattern4<StoredF32>,
@@ -2174,7 +2192,7 @@ pub struct ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern {
pub activity: CoinblocksCoindaysSatblocksSatdaysSentPattern,
pub cost_basis: InvestedMaxMinPercentilesSpotPattern,
pub outputs: UtxoPattern,
pub realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern2,
pub realized: CapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern2,
pub relative: InvestedNegNetSupplyUnrealizedPattern,
pub supply: _30dHalvedTotalPattern,
pub unrealized: GreedInvestedInvestorNegNetPainPeakSupplyTotalUnrealizedPattern,
@@ -2187,7 +2205,7 @@ impl ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern {
activity: CoinblocksCoindaysSatblocksSatdaysSentPattern::new(client.clone(), acc.clone()),
cost_basis: InvestedMaxMinPercentilesSpotPattern::new(client.clone(), acc.clone()),
outputs: UtxoPattern::new(client.clone(), _m(&acc, "utxo_count")),
realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern2::new(client.clone(), acc.clone()),
realized: CapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern2::new(client.clone(), acc.clone()),
relative: InvestedNegNetSupplyUnrealizedPattern::new(client.clone(), acc.clone()),
supply: _30dHalvedTotalPattern::new(client.clone(), acc.clone()),
unrealized: GreedInvestedInvestorNegNetPainPeakSupplyTotalUnrealizedPattern::new(client.clone(), acc.clone()),
@@ -2200,7 +2218,7 @@ pub struct ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern5 {
pub activity: CoinblocksCoindaysSatblocksSatdaysSentPattern,
pub cost_basis: MaxMinPattern,
pub outputs: UtxoPattern,
pub realized: AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern2,
pub realized: AdjustedCapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern2,
pub relative: InvestedNegNetNuplSupplyUnrealizedPattern3,
pub supply: _30dHalvedTotalPattern,
pub unrealized: GreedInvestedInvestorNegNetPainPeakSupplyTotalUnrealizedPattern,
@@ -2213,7 +2231,7 @@ impl ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern5 {
activity: CoinblocksCoindaysSatblocksSatdaysSentPattern::new(client.clone(), acc.clone()),
cost_basis: MaxMinPattern::new(client.clone(), acc.clone()),
outputs: UtxoPattern::new(client.clone(), _m(&acc, "utxo_count")),
realized: AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern2::new(client.clone(), acc.clone()),
realized: AdjustedCapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern2::new(client.clone(), acc.clone()),
relative: InvestedNegNetNuplSupplyUnrealizedPattern3::new(client.clone(), acc.clone()),
supply: _30dHalvedTotalPattern::new(client.clone(), acc.clone()),
unrealized: GreedInvestedInvestorNegNetPainPeakSupplyTotalUnrealizedPattern::new(client.clone(), acc.clone()),
@@ -2226,7 +2244,7 @@ pub struct ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern4 {
pub activity: CoinblocksCoindaysSatblocksSatdaysSentPattern,
pub cost_basis: MaxMinPattern,
pub outputs: UtxoPattern,
pub realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern,
pub realized: CapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern,
pub relative: InvestedNegNetNuplSupplyUnrealizedPattern,
pub supply: _30dHalvedTotalPattern,
pub unrealized: GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern,
@@ -2239,7 +2257,7 @@ impl ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern4 {
activity: CoinblocksCoindaysSatblocksSatdaysSentPattern::new(client.clone(), acc.clone()),
cost_basis: MaxMinPattern::new(client.clone(), acc.clone()),
outputs: UtxoPattern::new(client.clone(), _m(&acc, "utxo_count")),
realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern::new(client.clone(), acc.clone()),
realized: CapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern::new(client.clone(), acc.clone()),
relative: InvestedNegNetNuplSupplyUnrealizedPattern::new(client.clone(), acc.clone()),
supply: _30dHalvedTotalPattern::new(client.clone(), acc.clone()),
unrealized: GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern::new(client.clone(), acc.clone()),
@@ -2252,7 +2270,7 @@ pub struct ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern6 {
pub activity: CoinblocksCoindaysSatblocksSatdaysSentPattern,
pub cost_basis: MaxMinPattern,
pub outputs: UtxoPattern,
pub realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern,
pub realized: CapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern,
pub relative: InvestedNegNetNuplSupplyUnrealizedPattern3,
pub supply: _30dHalvedTotalPattern,
pub unrealized: GreedInvestedInvestorNegNetPainPeakSupplyTotalUnrealizedPattern,
@@ -2265,7 +2283,7 @@ impl ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern6 {
activity: CoinblocksCoindaysSatblocksSatdaysSentPattern::new(client.clone(), acc.clone()),
cost_basis: MaxMinPattern::new(client.clone(), acc.clone()),
outputs: UtxoPattern::new(client.clone(), _m(&acc, "utxo_count")),
realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern::new(client.clone(), acc.clone()),
realized: CapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern::new(client.clone(), acc.clone()),
relative: InvestedNegNetNuplSupplyUnrealizedPattern3::new(client.clone(), acc.clone()),
supply: _30dHalvedTotalPattern::new(client.clone(), acc.clone()),
unrealized: GreedInvestedInvestorNegNetPainPeakSupplyTotalUnrealizedPattern::new(client.clone(), acc.clone()),
@@ -2278,7 +2296,7 @@ pub struct ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern3 {
pub activity: CoinblocksCoindaysSatblocksSatdaysSentPattern,
pub cost_basis: MaxMinPattern,
pub outputs: UtxoPattern,
pub realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern,
pub realized: CapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern,
pub relative: InvestedSupplyPattern,
pub supply: _30dHalvedTotalPattern,
pub unrealized: GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern,
@@ -2291,7 +2309,7 @@ impl ActivityCostOutputsRealizedRelativeSupplyUnrealizedPattern3 {
activity: CoinblocksCoindaysSatblocksSatdaysSentPattern::new(client.clone(), acc.clone()),
cost_basis: MaxMinPattern::new(client.clone(), acc.clone()),
outputs: UtxoPattern::new(client.clone(), _m(&acc, "utxo_count")),
realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern::new(client.clone(), acc.clone()),
realized: CapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern::new(client.clone(), acc.clone()),
relative: InvestedSupplyPattern::new(client.clone(), acc.clone()),
supply: _30dHalvedTotalPattern::new(client.clone(), acc.clone()),
unrealized: GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern::new(client.clone(), acc.clone()),
@@ -2304,7 +2322,7 @@ pub struct ActivityCostOutputsRealizedSupplyUnrealizedPattern {
pub activity: CoinblocksCoindaysSatblocksSatdaysSentPattern,
pub cost_basis: MaxMinPattern,
pub outputs: UtxoPattern,
pub realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern,
pub realized: CapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern,
pub supply: _30dHalvedTotalPattern,
pub unrealized: GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern,
}
@@ -2316,7 +2334,7 @@ impl ActivityCostOutputsRealizedSupplyUnrealizedPattern {
activity: CoinblocksCoindaysSatblocksSatdaysSentPattern::new(client.clone(), acc.clone()),
cost_basis: MaxMinPattern::new(client.clone(), acc.clone()),
outputs: UtxoPattern::new(client.clone(), _m(&acc, "utxo_count")),
realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern::new(client.clone(), acc.clone()),
realized: CapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern::new(client.clone(), acc.clone()),
supply: _30dHalvedTotalPattern::new(client.clone(), acc.clone()),
unrealized: GreedInvestedInvestorNegNetPainSupplyTotalUnrealizedPattern::new(client.clone(), acc.clone()),
}
@@ -2561,6 +2579,22 @@ impl BitcoinDollarsSatsPattern3 {
}
}
/// Pattern struct for repeated tree structure.
pub struct _30dCountPattern {
pub _30d_change: MetricPattern4<StoredF64>,
pub count: MetricPattern1<StoredU64>,
}
impl _30dCountPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
_30d_change: MetricPattern4::new(client.clone(), _m(&acc, "30d_change")),
count: MetricPattern1::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct DollarsSatsPattern {
pub dollars: MetricPattern1<Dollars>,
@@ -2625,6 +2659,22 @@ impl SdSmaPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct UtxoPattern {
pub utxo_count: MetricPattern1<StoredU64>,
pub utxo_count_30d_change: MetricPattern4<StoredF64>,
}
impl UtxoPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
utxo_count: MetricPattern1::new(client.clone(), acc.clone()),
utxo_count_30d_change: MetricPattern4::new(client.clone(), _m(&acc, "30d_change")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct CumulativeSumPattern<T> {
pub cumulative: MetricPattern1<T>,
@@ -2687,20 +2737,6 @@ impl RatioPattern2 {
}
}
/// Pattern struct for repeated tree structure.
pub struct UtxoPattern {
pub utxo_count: MetricPattern1<StoredU64>,
}
impl UtxoPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
utxo_count: MetricPattern1::new(client.clone(), acc.clone()),
}
}
}
// Metrics tree
/// Metrics tree node.
@@ -2859,6 +2895,8 @@ pub struct MetricsTree_Blocks_Mining {
pub hash_rate_1m_sma: MetricPattern4<StoredF32>,
pub hash_rate_2m_sma: MetricPattern4<StoredF32>,
pub hash_rate_1y_sma: MetricPattern4<StoredF32>,
pub hash_rate_ath: MetricPattern1<StoredF64>,
pub hash_rate_drawdown: MetricPattern1<StoredF32>,
pub hash_price_ths: MetricPattern1<StoredF32>,
pub hash_price_ths_min: MetricPattern1<StoredF32>,
pub hash_price_phs: MetricPattern1<StoredF32>,
@@ -2879,6 +2917,8 @@ impl MetricsTree_Blocks_Mining {
hash_rate_1m_sma: MetricPattern4::new(client.clone(), "hash_rate_1m_sma".to_string()),
hash_rate_2m_sma: MetricPattern4::new(client.clone(), "hash_rate_2m_sma".to_string()),
hash_rate_1y_sma: MetricPattern4::new(client.clone(), "hash_rate_1y_sma".to_string()),
hash_rate_ath: MetricPattern1::new(client.clone(), "hash_rate_ath".to_string()),
hash_rate_drawdown: MetricPattern1::new(client.clone(), "hash_rate_drawdown".to_string()),
hash_price_ths: MetricPattern1::new(client.clone(), "hash_price_ths".to_string()),
hash_price_ths_min: MetricPattern1::new(client.clone(), "hash_price_ths_min".to_string()),
hash_price_phs: MetricPattern1::new(client.clone(), "hash_price_phs".to_string()),
@@ -3515,7 +3555,7 @@ impl MetricsTree_Cointime_Adjusted {
/// Metrics tree node.
pub struct MetricsTree_Cointime_ReserveRisk {
pub vocdd_365d_sma: MetricPattern6<StoredF64>,
pub vocdd_365d_median: MetricPattern6<StoredF64>,
pub hodl_bank: MetricPattern6<StoredF64>,
pub reserve_risk: MetricPattern4<StoredF64>,
}
@@ -3523,7 +3563,7 @@ pub struct MetricsTree_Cointime_ReserveRisk {
impl MetricsTree_Cointime_ReserveRisk {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
vocdd_365d_sma: MetricPattern6::new(client.clone(), "vocdd_365d_sma".to_string()),
vocdd_365d_median: MetricPattern6::new(client.clone(), "vocdd_365d_median".to_string()),
hodl_bank: MetricPattern6::new(client.clone(), "hodl_bank".to_string()),
reserve_risk: MetricPattern4::new(client.clone(), "reserve_risk".to_string()),
}
@@ -5038,6 +5078,7 @@ pub struct MetricsTree_Price {
pub cents: MetricsTree_Price_Cents,
pub usd: MetricsTree_Price_Usd,
pub sats: OhlcSplitPattern2<OHLCSats>,
pub oracle: MetricsTree_Price_Oracle,
}
impl MetricsTree_Price {
@@ -5046,6 +5087,7 @@ impl MetricsTree_Price {
cents: MetricsTree_Price_Cents::new(client.clone(), format!("{base_path}_cents")),
usd: MetricsTree_Price_Usd::new(client.clone(), format!("{base_path}_usd")),
sats: OhlcSplitPattern2::new(client.clone(), "price".to_string()),
oracle: MetricsTree_Price_Oracle::new(client.clone(), format!("{base_path}_oracle")),
}
}
}
@@ -5099,6 +5141,27 @@ impl MetricsTree_Price_Usd {
}
}
/// Metrics tree node.
pub struct MetricsTree_Price_Oracle {
pub price_cents: MetricPattern11<CentsUnsigned>,
pub ohlc_cents: MetricPattern6<OHLCCentsUnsigned>,
pub split: CloseHighLowOpenPattern2<CentsUnsigned>,
pub ohlc: MetricPattern1<OHLCCentsUnsigned>,
pub ohlc_dollars: MetricPattern1<OHLCDollars>,
}
impl MetricsTree_Price_Oracle {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
price_cents: MetricPattern11::new(client.clone(), "oracle_price_cents".to_string()),
ohlc_cents: MetricPattern6::new(client.clone(), "oracle_ohlc_cents".to_string()),
split: CloseHighLowOpenPattern2::new(client.clone(), "oracle_price".to_string()),
ohlc: MetricPattern1::new(client.clone(), "oracle_price_ohlc".to_string()),
ohlc_dollars: MetricPattern1::new(client.clone(), "oracle_ohlc_dollars".to_string()),
}
}
}
/// Metrics tree node.
pub struct MetricsTree_Distribution {
pub supply_state: MetricPattern11<SupplyState>,
@@ -5109,7 +5172,7 @@ pub struct MetricsTree_Distribution {
pub addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern,
pub empty_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern,
pub address_activity: MetricsTree_Distribution_AddressActivity,
pub total_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern,
pub total_addr_count: MetricsTree_Distribution_TotalAddrCount,
pub new_addr_count: MetricsTree_Distribution_NewAddrCount,
pub growth_rate: MetricsTree_Distribution_GrowthRate,
pub fundedaddressindex: MetricPattern31<FundedAddressIndex>,
@@ -5127,7 +5190,7 @@ impl MetricsTree_Distribution {
addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern::new(client.clone(), "addr_count".to_string()),
empty_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern::new(client.clone(), "empty_addr_count".to_string()),
address_activity: MetricsTree_Distribution_AddressActivity::new(client.clone(), format!("{base_path}_address_activity")),
total_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern::new(client.clone(), "total_addr_count".to_string()),
total_addr_count: MetricsTree_Distribution_TotalAddrCount::new(client.clone(), format!("{base_path}_total_addr_count")),
new_addr_count: MetricsTree_Distribution_NewAddrCount::new(client.clone(), format!("{base_path}_new_addr_count")),
growth_rate: MetricsTree_Distribution_GrowthRate::new(client.clone(), format!("{base_path}_growth_rate")),
fundedaddressindex: MetricPattern31::new(client.clone(), "fundedaddressindex".to_string()),
@@ -5216,7 +5279,7 @@ pub struct MetricsTree_Distribution_UtxoCohorts_All {
pub supply: _30dHalvedTotalPattern,
pub outputs: UtxoPattern,
pub activity: CoinblocksCoindaysSatblocksSatdaysSentPattern,
pub realized: AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern,
pub realized: AdjustedCapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern,
pub unrealized: GreedInvestedInvestorNegNetPainPeakSupplyTotalUnrealizedPattern,
pub cost_basis: InvestedMaxMinPercentilesSpotPattern,
pub relative: MetricsTree_Distribution_UtxoCohorts_All_Relative,
@@ -5228,7 +5291,7 @@ impl MetricsTree_Distribution_UtxoCohorts_All {
supply: _30dHalvedTotalPattern::new(client.clone(), "".to_string()),
outputs: UtxoPattern::new(client.clone(), "utxo_count".to_string()),
activity: CoinblocksCoindaysSatblocksSatdaysSentPattern::new(client.clone(), "".to_string()),
realized: AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern::new(client.clone(), "".to_string()),
realized: AdjustedCapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern::new(client.clone(), "".to_string()),
unrealized: GreedInvestedInvestorNegNetPainPeakSupplyTotalUnrealizedPattern::new(client.clone(), "".to_string()),
cost_basis: InvestedMaxMinPercentilesSpotPattern::new(client.clone(), "".to_string()),
relative: MetricsTree_Distribution_UtxoCohorts_All_Relative::new(client.clone(), format!("{base_path}_relative")),
@@ -5541,7 +5604,7 @@ pub struct MetricsTree_Distribution_UtxoCohorts_Term_Short {
pub supply: _30dHalvedTotalPattern,
pub outputs: UtxoPattern,
pub activity: CoinblocksCoindaysSatblocksSatdaysSentPattern,
pub realized: AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern,
pub realized: AdjustedCapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern,
pub unrealized: GreedInvestedInvestorNegNetPainPeakSupplyTotalUnrealizedPattern,
pub cost_basis: InvestedMaxMinPercentilesSpotPattern,
pub relative: InvestedNegNetNuplSupplyUnrealizedPattern4,
@@ -5553,7 +5616,7 @@ impl MetricsTree_Distribution_UtxoCohorts_Term_Short {
supply: _30dHalvedTotalPattern::new(client.clone(), "sth".to_string()),
outputs: UtxoPattern::new(client.clone(), "sth_utxo_count".to_string()),
activity: CoinblocksCoindaysSatblocksSatdaysSentPattern::new(client.clone(), "sth".to_string()),
realized: AdjustedCapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern::new(client.clone(), "sth".to_string()),
realized: AdjustedCapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern::new(client.clone(), "sth".to_string()),
unrealized: GreedInvestedInvestorNegNetPainPeakSupplyTotalUnrealizedPattern::new(client.clone(), "sth".to_string()),
cost_basis: InvestedMaxMinPercentilesSpotPattern::new(client.clone(), "sth".to_string()),
relative: InvestedNegNetNuplSupplyUnrealizedPattern4::new(client.clone(), "sth".to_string()),
@@ -5566,7 +5629,7 @@ pub struct MetricsTree_Distribution_UtxoCohorts_Term_Long {
pub supply: _30dHalvedTotalPattern,
pub outputs: UtxoPattern,
pub activity: CoinblocksCoindaysSatblocksSatdaysSentPattern,
pub realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern2,
pub realized: CapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern2,
pub unrealized: GreedInvestedInvestorNegNetPainPeakSupplyTotalUnrealizedPattern,
pub cost_basis: InvestedMaxMinPercentilesSpotPattern,
pub relative: InvestedNegNetNuplSupplyUnrealizedPattern4,
@@ -5578,7 +5641,7 @@ impl MetricsTree_Distribution_UtxoCohorts_Term_Long {
supply: _30dHalvedTotalPattern::new(client.clone(), "lth".to_string()),
outputs: UtxoPattern::new(client.clone(), "lth_utxo_count".to_string()),
activity: CoinblocksCoindaysSatblocksSatdaysSentPattern::new(client.clone(), "lth".to_string()),
realized: CapCapitulationInvestorLossMvrvNegNetPeakProfitRealizedSellSentSoprTotalValuePattern2::new(client.clone(), "lth".to_string()),
realized: CapCapitulationInvestorLossLowerMvrvNegNetPeakProfitRealizedSellSentSoprTotalUpperValuePattern2::new(client.clone(), "lth".to_string()),
unrealized: GreedInvestedInvestorNegNetPainPeakSupplyTotalUnrealizedPattern::new(client.clone(), "lth".to_string()),
cost_basis: InvestedMaxMinPercentilesSpotPattern::new(client.clone(), "lth".to_string()),
relative: InvestedNegNetNuplSupplyUnrealizedPattern4::new(client.clone(), "lth".to_string()),
@@ -5864,6 +5927,35 @@ impl MetricsTree_Distribution_AddressActivity {
}
}
/// Metrics tree node.
pub struct MetricsTree_Distribution_TotalAddrCount {
pub all: MetricPattern1<StoredU64>,
pub p2pk65: MetricPattern1<StoredU64>,
pub p2pk33: MetricPattern1<StoredU64>,
pub p2pkh: MetricPattern1<StoredU64>,
pub p2sh: MetricPattern1<StoredU64>,
pub p2wpkh: MetricPattern1<StoredU64>,
pub p2wsh: MetricPattern1<StoredU64>,
pub p2tr: MetricPattern1<StoredU64>,
pub p2a: MetricPattern1<StoredU64>,
}
impl MetricsTree_Distribution_TotalAddrCount {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
all: MetricPattern1::new(client.clone(), "total_addr_count".to_string()),
p2pk65: MetricPattern1::new(client.clone(), "p2pk65_total_addr_count".to_string()),
p2pk33: MetricPattern1::new(client.clone(), "p2pk33_total_addr_count".to_string()),
p2pkh: MetricPattern1::new(client.clone(), "p2pkh_total_addr_count".to_string()),
p2sh: MetricPattern1::new(client.clone(), "p2sh_total_addr_count".to_string()),
p2wpkh: MetricPattern1::new(client.clone(), "p2wpkh_total_addr_count".to_string()),
p2wsh: MetricPattern1::new(client.clone(), "p2wsh_total_addr_count".to_string()),
p2tr: MetricPattern1::new(client.clone(), "p2tr_total_addr_count".to_string()),
p2a: MetricPattern1::new(client.clone(), "p2a_total_addr_count".to_string()),
}
}
}
/// Metrics tree node.
pub struct MetricsTree_Distribution_NewAddrCount {
pub all: AverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern2<StoredU64>,
@@ -5929,6 +6021,9 @@ pub struct MetricsTree_Supply {
pub inflation: MetricPattern4<StoredF32>,
pub velocity: MetricsTree_Supply_Velocity,
pub market_cap: MetricPattern1<Dollars>,
pub market_cap_growth_rate: MetricPattern4<StoredF32>,
pub realized_cap_growth_rate: MetricPattern4<StoredF32>,
pub cap_growth_rate_diff: MetricPattern6<StoredF32>,
}
impl MetricsTree_Supply {
@@ -5939,6 +6034,9 @@ impl MetricsTree_Supply {
inflation: MetricPattern4::new(client.clone(), "inflation_rate".to_string()),
velocity: MetricsTree_Supply_Velocity::new(client.clone(), format!("{base_path}_velocity")),
market_cap: MetricPattern1::new(client.clone(), "market_cap".to_string()),
market_cap_growth_rate: MetricPattern4::new(client.clone(), "market_cap_growth_rate".to_string()),
realized_cap_growth_rate: MetricPattern4::new(client.clone(), "realized_cap_growth_rate".to_string()),
cap_growth_rate_diff: MetricPattern6::new(client.clone(), "cap_growth_rate_diff".to_string()),
}
}
}
@@ -5998,7 +6096,7 @@ pub struct BrkClient {
impl BrkClient {
/// Client version.
pub const VERSION: &'static str = "v0.1.2";
pub const VERSION: &'static str = "v0.1.7";
/// Create a new client with the given base URL.
pub fn new(base_url: impl Into<String>) -> Self {
@@ -6222,6 +6320,15 @@ impl BrkClient {
self.base.get_json(&format!("/api/mempool/info"))
}
/// Live BTC/USD price
///
/// Returns the current BTC/USD price in dollars, derived from on-chain round-dollar output patterns in the last 12 blocks plus mempool.
///
/// Endpoint: `GET /api/mempool/price`
pub fn get_live_price(&self) -> Result<Dollars> {
self.base.get_json(&format!("/api/mempool/price"))
}
/// Mempool transaction IDs
///
/// Get all transaction IDs currently in the mempool.
@@ -6293,6 +6400,42 @@ impl BrkClient {
}
}
/// Available cost basis cohorts
///
/// List available cohorts for cost basis distribution.
///
/// Endpoint: `GET /api/metrics/cost-basis`
pub fn get_cost_basis_cohorts(&self) -> Result<Vec<String>> {
self.base.get_json(&format!("/api/metrics/cost-basis"))
}
/// Available cost basis dates
///
/// List available dates for a cohort's cost basis distribution.
///
/// Endpoint: `GET /api/metrics/cost-basis/{cohort}/dates`
pub fn get_cost_basis_dates(&self, cohort: Cohort) -> Result<Vec<Date>> {
self.base.get_json(&format!("/api/metrics/cost-basis/{cohort}/dates"))
}
/// Cost basis distribution
///
/// Get the cost basis distribution for a cohort on a specific date.
///
/// Query params:
/// - `bucket`: raw (default), lin200, lin500, lin1000, log10, log50, log100
/// - `value`: supply (default, in BTC), realized (USD), unrealized (USD)
///
/// Endpoint: `GET /api/metrics/cost-basis/{cohort}/{date}`
pub fn get_cost_basis(&self, cohort: Cohort, date: &str, bucket: Option<CostBasisBucket>, value: Option<CostBasisValue>) -> Result<serde_json::Value> {
let mut query = Vec::new();
if let Some(v) = bucket { query.push(format!("bucket={}", v)); }
if let Some(v) = value { query.push(format!("value={}", v)); }
let query_str = if query.is_empty() { String::new() } else { format!("?{}", query.join("&")) };
let path = format!("/api/metrics/cost-basis/{cohort}/{date}{}", query_str);
self.base.get_json(&path)
}
/// Metric count
///
/// Returns the number of metrics available per index type.
+2 -1
View File
@@ -13,6 +13,7 @@ brk_error = { workspace = true, features = ["vecdb"] }
brk_fetcher = { workspace = true }
brk_cohort = { workspace = true }
brk_indexer = { workspace = true }
brk_oracle = { workspace = true }
brk_iterator = { workspace = true }
brk_logger = { workspace = true }
brk_reader = { workspace = true }
@@ -21,8 +22,8 @@ brk_store = { workspace = true }
brk_traversable = { workspace = true }
brk_types = { workspace = true }
derive_more = { workspace = true }
pco = { workspace = true }
tracing = { workspace = true }
pco = "1.0.0"
rayon = { workspace = true }
rustc-hash = { workspace = true }
schemars = { workspace = true }
@@ -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>,
@@ -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,28 +164,29 @@ 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()
}
@@ -140,27 +197,35 @@ impl AddressTypeToAddrCountVecs {
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(())
@@ -168,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(())
}
@@ -198,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,
@@ -225,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(())
}
@@ -251,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(())
@@ -268,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(())
}
}
@@ -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,
)
},
)?;
@@ -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,
)
},
@@ -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.
@@ -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::{CentsUnsigned, 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,
)?,
})
}
@@ -234,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,13 +1,15 @@
use std::{cmp::Reverse, collections::BinaryHeap, path::Path};
use std::{cmp::Reverse, collections::BinaryHeap, fs, path::Path};
use brk_cohort::{
AGE_BOUNDARIES, ByAgeRange, ByAmountRange, ByEpoch, ByGreatEqualAmount, ByLowerThanAmount,
ByMaxAge, ByMinAge, BySpendableType, ByTerm, ByYear, Filter, Filtered, StateLevel, UTXOGroups,
ByMaxAge, ByMinAge, BySpendableType, ByTerm, ByYear, Filter, Filtered, StateLevel, TERM_NAMES,
Term, UTXOGroups,
};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{
CentsUnsigned, DateIndex, Dollars, Height, ONE_HOUR_IN_SEC, Sats, StoredF32, Timestamp, Version,
CentsUnsigned, CentsUnsignedCompact, CostBasisDistribution, Date, DateIndex, Dollars, Height,
ONE_HOUR_IN_SEC, Sats, StoredF32, Timestamp, Version,
};
use derive_more::{Deref, DerefMut};
use rayon::prelude::*;
@@ -25,6 +27,9 @@ use super::{super::traits::CohortVecs, vecs::UTXOCohortVecs};
const VERSION: Version = Version::new(0);
/// Significant digits for cost basis prices (after rounding to dollars).
const COST_BASIS_PRICE_DIGITS: i32 = 5;
/// All UTXO cohorts organized by filter type.
#[derive(Clone, Deref, DerefMut, Traversable)]
pub struct UTXOCohorts(pub(crate) UTXOGroups<UTXOCohortVecs>);
@@ -152,70 +157,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.
@@ -226,8 +244,28 @@ 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.
@@ -329,10 +367,12 @@ impl UTXOCohorts {
/// 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.
/// Also writes daily cost basis snapshots to states_path.
pub fn truncate_push_aggregate_percentiles(
&mut self,
dateindex: DateIndex,
spot: Dollars,
states_path: &Path,
) -> Result<()> {
// Collect (filter, entries, total_sats, total_usd) from age_range cohorts.
// Keep data in CentsUnsigned to avoid float conversions until output.
@@ -403,6 +443,7 @@ impl UTXOCohorts {
}
// K-way merge using min-heap: O(n log k) where k = number of cohorts
// Collects merged price->sats map while computing percentiles
let mut heap: BinaryHeap<Reverse<(CentsUnsigned, usize, usize)>> = BinaryHeap::new();
// Initialize heap with first entry from each cohort
@@ -428,6 +469,44 @@ impl UTXOCohorts {
let mut sats_at_price: u64 = 0;
let mut usd_at_price: u128 = 0;
// Collect merged entries during the merge (already in sorted order)
// Pre-allocate with max possible unique prices (actual count likely lower due to dedup)
let max_unique_prices = relevant.iter().map(|e| e.len()).max().unwrap_or(0);
let mut merged: Vec<(CentsUnsignedCompact, Sats)> =
Vec::with_capacity(max_unique_prices);
// Finalize a price point: compute percentiles and accumulate for merged vec
let mut finalize_price = |price: CentsUnsigned, sats: u64, usd: u128| {
// Percentile computation uses exact price for accuracy
cumsum_sats += sats;
cumsum_usd += usd;
if sat_idx < PERCENTILES_LEN || usd_idx < PERCENTILES_LEN {
let dollars = price.to_dollars();
while sat_idx < PERCENTILES_LEN && cumsum_sats >= sat_targets[sat_idx] {
sat_result[sat_idx] = dollars;
sat_idx += 1;
}
while usd_idx < PERCENTILES_LEN && cumsum_usd >= usd_targets[usd_idx] {
usd_result[usd_idx] = dollars;
usd_idx += 1;
}
}
// Round to nearest dollar with N significant digits for storage
let rounded: CentsUnsignedCompact =
price.round_to_dollar(COST_BASIS_PRICE_DIGITS).into();
// Merge entries with same rounded price using last_mut
if let Some((last_price, last_sats)) = merged.last_mut()
&& *last_price == rounded
{
*last_sats += Sats::from(sats);
} else {
merged.push((rounded, Sats::from(sats)));
}
};
while let Some(Reverse((price, cohort_idx, entry_idx))) = heap.pop() {
let entries = relevant[cohort_idx];
let (_, amount) = entries[entry_idx];
@@ -438,27 +517,7 @@ impl UTXOCohorts {
if let Some(prev_price) = current_price
&& prev_price != price
{
cumsum_sats += sats_at_price;
cumsum_usd += usd_at_price;
// 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;
}
}
finalize_price(prev_price, sats_at_price, usd_at_price);
sats_at_price = 0;
usd_at_price = 0;
}
@@ -474,22 +533,9 @@ impl UTXOCohorts {
}
}
// 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;
}
// Finalize last price
if let Some(price) = current_price {
finalize_price(price, sats_at_price, usd_at_price);
}
// Push both sat-weighted and USD-weighted results
@@ -510,6 +556,23 @@ impl UTXOCohorts {
spot_pct.dateindex.truncate_push(dateindex, rank)?;
}
// Write daily cost basis snapshot
let cohort_name = match &filter {
Filter::All => "all",
Filter::Term(Term::Sth) => TERM_NAMES.short.id,
Filter::Term(Term::Lth) => TERM_NAMES.long.id,
_ => return Ok(()),
};
let date = Date::from(dateindex);
let dir = states_path.join(format!("utxo_{cohort_name}_cost_basis/by_date"));
fs::create_dir_all(&dir)?;
let path = dir.join(date.to_string());
fs::write(
path,
CostBasisDistribution::serialize_iter(merged.into_iter())?,
)?;
Ok(())
})
}
@@ -399,7 +399,7 @@ pub fn process_blocks(
.map(|c| c.to_dollars())
.unwrap_or(Dollars::NAN);
vecs.utxo_cohorts
.truncate_push_aggregate_percentiles(dateindex, spot)?;
.truncate_push_aggregate_percentiles(dateindex, spot, &vecs.states_path)?;
// Compute unrealized peak regret by age range (once per day)
// Aggregate cohorts (all, term, etc.) get values via compute_from_stateful
@@ -296,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,
@@ -350,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,
)?,
})
}
@@ -42,7 +49,11 @@ impl OutputsMetrics {
/// 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.
@@ -70,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(())
}
}
@@ -17,7 +17,8 @@ use crate::{
internal::{
CentsUnsignedToDollars, ComputedFromDateLast, ComputedFromDateRatio,
ComputedFromHeightLast, ComputedFromHeightSum, ComputedFromHeightSumCum, DollarsMinus,
DollarsPlus, LazyBinaryFromHeightSum, LazyBinaryFromHeightSumCum,
DollarsPlus, DollarsSquaredDivide, LazyBinaryFromHeightSum, LazyBinaryFromHeightSumCum,
LazyBinaryPriceFromHeight,
LazyComputedValueFromHeightSumCum, LazyFromDateLast, LazyFromHeightLast, LazyFromHeightSum,
LazyFromHeightSumCum, LazyPriceFromCents, PercentageDollarsF32, PriceFromHeight,
StoredF32Identity, ValueFromDateLast,
@@ -43,6 +44,10 @@ pub struct RealizedMetrics {
pub investor_price: LazyPriceFromCents,
pub investor_price_extra: ComputedFromDateRatio,
// === Floor/Ceiling Price Bands (lazy: realized²/investor, investor²/realized) ===
pub lower_price_band: LazyBinaryPriceFromHeight,
pub upper_price_band: LazyBinaryPriceFromHeight,
// === 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>,
@@ -286,6 +291,24 @@ impl RealizedMetrics {
extended,
)?;
// Floor price = realized² / investor (lower band)
let lower_price_band =
LazyBinaryPriceFromHeight::from_price_and_lazy_price::<DollarsSquaredDivide>(
&cfg.name("lower_price_band"),
cfg.version,
&realized_price,
&investor_price,
);
// Ceiling price = investor² / realized (upper band)
let upper_price_band =
LazyBinaryPriceFromHeight::from_lazy_price_and_price::<DollarsSquaredDivide>(
&cfg.name("upper_price_band"),
cfg.version,
&investor_price,
&realized_price,
);
// Raw values for aggregation
let cap_raw = BytesVec::forced_import(cfg.db, &cfg.name("cap_raw"), cfg.version)?;
let investor_cap_raw =
@@ -429,6 +452,11 @@ impl RealizedMetrics {
investor_price_cents,
investor_price,
investor_price_extra,
// === Floor/Ceiling Price Bands ===
lower_price_band,
upper_price_band,
cap_raw,
investor_cap_raw,
@@ -5,8 +5,8 @@ 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, RealizedMetrics, SupplyMetrics, UnrealizedMetrics};
@@ -337,33 +337,33 @@ 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,
)
@@ -1,6 +1,6 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{CentsSats, CentsSquaredSats, CentsUnsigned, DateIndex, Dollars, Height};
use brk_types::{CentsSats, CentsSquaredSats, CentsUnsigned, DateIndex, Dollars, Height, Version};
use rayon::prelude::*;
use vecdb::{
AnyStoredVec, AnyVec, BytesVec, Exit, GenericStoredVec, ImportableVec, Negate,
@@ -150,7 +150,7 @@ impl UnrealizedMetrics {
let net_sentiment = ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("net_sentiment"),
cfg.version,
cfg.version + Version::ONE, // v1: weighted average for aggregate cohorts
cfg.indexes,
)?;
@@ -598,17 +598,32 @@ impl UnrealizedMetrics {
)?)
})?;
// Net sentiment: greed - pain
self.net_sentiment
.compute_all(indexes, starting_indexes, exit, |vec| {
Ok(vec.compute_subtract(
starting_indexes.height,
&self.greed_index.height,
&self.pain_index.height,
exit,
)?)
})?;
// Net sentiment height (greed - pain) computed separately for separate cohorts only
// Aggregate cohorts compute it via weighted average in compute_from_stateful
// Dateindex derivation for ALL cohorts happens in compute_net_sentiment_rest
Ok(())
}
/// Compute net_sentiment.height for separate cohorts (greed - pain).
/// Aggregate cohorts skip this - their height is computed via weighted average in compute_from_stateful.
pub fn compute_net_sentiment_height(&mut self, starting_indexes: &ComputeIndexes, exit: &Exit) -> Result<()> {
Ok(self.net_sentiment.height.compute_subtract(
starting_indexes.height,
&self.greed_index.height,
&self.pain_index.height,
exit,
)?)
}
/// Compute net_sentiment dateindex derivation from height.
/// Called for ALL cohorts after height is computed (either via greed-pain or weighted avg).
pub fn compute_net_sentiment_rest(
&mut self,
indexes: &indexes::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
self.net_sentiment.compute_rest(indexes, starting_indexes, exit)
}
}
@@ -6,6 +6,9 @@ use vecdb::unlikely;
use super::{super::cost_basis::RealizedState, base::CohortState};
/// Significant digits for address cost basis prices (after rounding to dollars).
const COST_BASIS_PRICE_DIGITS: i32 = 4;
#[derive(Clone)]
pub struct AddressCohortState {
pub addr_count: u64,
@@ -16,7 +19,8 @@ impl AddressCohortState {
pub fn new(path: &Path, name: &str, compute_dollars: bool) -> Self {
Self {
addr_count: 0,
inner: CohortState::new(path, name, compute_dollars),
inner: CohortState::new(path, name, compute_dollars)
.with_price_rounding(COST_BASIS_PRICE_DIGITS),
}
}
@@ -3,9 +3,7 @@ use std::path::Path;
use brk_error::Result;
use brk_types::{Age, CentsSats, CentsUnsigned, CostBasisSnapshot, Height, Sats, SupplyState};
use super::super::cost_basis::{
CachedUnrealizedState, Percentiles, CostBasisData, RealizedState, UnrealizedState,
};
use super::super::cost_basis::{CostBasisData, Percentiles, RealizedState, UnrealizedState};
#[derive(Clone)]
pub struct CohortState {
@@ -15,7 +13,6 @@ pub struct CohortState {
pub satblocks_destroyed: Sats,
pub satdays_destroyed: Sats,
cost_basis_data: Option<CostBasisData>,
cached_unrealized: Option<CachedUnrealizedState>,
}
impl CohortState {
@@ -27,12 +24,18 @@ impl CohortState {
satblocks_destroyed: Sats::ZERO,
satdays_destroyed: Sats::ZERO,
cost_basis_data: compute_dollars.then_some(CostBasisData::create(path, name)),
cached_unrealized: None,
}
}
/// Enable price rounding for cost basis data.
pub fn with_price_rounding(mut self, digits: i32) -> Self {
if let Some(data) = self.cost_basis_data.take() {
self.cost_basis_data = Some(data.with_price_rounding(digits));
}
self
}
pub fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
self.cached_unrealized = None;
match self.cost_basis_data.as_mut() {
Some(p) => p.import_at_or_before(height),
None => Ok(height),
@@ -55,7 +58,6 @@ impl CohortState {
p.clean()?;
p.init();
}
self.cached_unrealized = None;
Ok(())
}
@@ -66,11 +68,17 @@ impl CohortState {
}
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))
self.cost_basis_data
.as_ref()?
.first_key_value()
.map(|(k, v)| (k.into(), v))
}
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))
self.cost_basis_data
.as_ref()?
.last_key_value()
.map(|(k, v)| (k.into(), v))
}
pub fn reset_single_iteration_values(&mut self) {
@@ -102,10 +110,6 @@ impl CohortState {
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);
}
}
}
@@ -129,10 +133,6 @@ impl CohortState {
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);
}
}
}
@@ -151,16 +151,10 @@ impl CohortState {
realized.receive(price, sats);
self.cost_basis_data.as_mut().unwrap().increment(
price,
sats,
price_sats,
investor_cap,
);
if let Some(cache) = self.cached_unrealized.as_mut() {
cache.on_receive(price, sats);
}
self.cost_basis_data
.as_mut()
.unwrap()
.increment(price, sats, price_sats, investor_cap);
}
}
@@ -185,10 +179,6 @@ impl CohortState {
current.price_sats,
current.investor_cap,
);
if let Some(cache) = self.cached_unrealized.as_mut() {
cache.on_receive(current.realized_price, current.supply_state.value);
}
}
if prev.supply_state.value.is_not_zero() {
@@ -198,10 +188,6 @@ impl CohortState {
prev.price_sats,
prev.investor_cap,
);
if let Some(cache) = self.cached_unrealized.as_mut() {
cache.on_send(prev.realized_price, prev.supply_state.value);
}
}
}
}
@@ -245,10 +231,6 @@ impl CohortState {
prev_ps,
prev_investor_cap,
);
if let Some(cache) = self.cached_unrealized.as_mut() {
cache.on_send(pp, sats);
}
}
}
}
@@ -293,10 +275,6 @@ impl CohortState {
current.price_sats,
current.investor_cap,
);
if let Some(cache) = self.cached_unrealized.as_mut() {
cache.on_receive(current.realized_price, current.supply_state.value);
}
}
if prev.supply_state.value.is_not_zero() {
@@ -306,10 +284,6 @@ impl CohortState {
prev.price_sats,
prev.investor_cap,
);
if let Some(cache) = self.cached_unrealized.as_mut() {
cache.on_send(prev.realized_price, prev.supply_state.value);
}
}
}
}
@@ -324,25 +298,13 @@ impl CohortState {
height_price: CentsUnsigned,
date_price: Option<CentsUnsigned>,
) -> (UnrealizedState, Option<UnrealizedState>) {
let cost_basis_data = match self.cost_basis_data.as_ref() {
Some(p) if !p.is_empty() => p,
_ => return (UnrealizedState::ZERO, date_price.map(|_| UnrealizedState::ZERO)),
};
let date_state = date_price.map(|date_price| {
CachedUnrealizedState::compute_full_standalone(date_price.into(), cost_basis_data)
});
let height_state = if let Some(cache) = self.cached_unrealized.as_mut() {
cache.get_at_price(height_price, cost_basis_data)
} else {
let cache = CachedUnrealizedState::compute_fresh(height_price, cost_basis_data);
let state = cache.current_state();
self.cached_unrealized = Some(cache);
state
};
(height_state, date_state)
match self.cost_basis_data.as_mut() {
Some(p) => p.compute_unrealized_states(height_price, date_price),
None => (
UnrealizedState::ZERO,
date_price.map(|_| UnrealizedState::ZERO),
),
}
}
pub fn write(&mut self, height: Height, cleanup: bool) -> Result<()> {
@@ -353,16 +315,22 @@ impl CohortState {
}
pub fn min_price(&self) -> Option<CentsUnsigned> {
self.cost_basis_data.as_ref()?.first_key_value().map(|(k, _)| k.into())
self.cost_basis_data
.as_ref()?
.first_key_value()
.map(|(k, _)| k.into())
}
pub fn max_price(&self) -> Option<CentsUnsigned> {
self.cost_basis_data.as_ref()?.last_key_value().map(|(k, _)| k.into())
self.cost_basis_data
.as_ref()?
.last_key_value()
.map(|(k, _)| k.into())
}
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)))
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)))
}
}
@@ -1,22 +1,23 @@
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 brk_types::{
CentsSats, CentsSquaredSats, CentsUnsigned, CentsUnsignedCompact, CostBasisDistribution,
Height, Sats,
};
use rustc_hash::FxHashMap;
use vecdb::Bytes;
use crate::utils::OptionExt;
use super::Percentiles;
use super::{CachedUnrealizedState, Percentiles, UnrealizedState};
/// Type alias for the price-to-sats map used in cost basis data.
pub(super) type CostBasisMap = BTreeMap<CentsUnsignedCompact, Sats>;
#[derive(Clone, Debug, Default)]
struct PendingRaw {
@@ -32,6 +33,8 @@ pub struct CostBasisData {
state: Option<State>,
pending: FxHashMap<CentsUnsignedCompact, (Sats, Sats)>,
pending_raw: PendingRaw,
cache: Option<CachedUnrealizedState>,
rounding_digits: Option<i32>,
}
const STATE_TO_KEEP: usize = 10;
@@ -43,6 +46,21 @@ impl CostBasisData {
state: None,
pending: FxHashMap::default(),
pending_raw: PendingRaw::default(),
cache: None,
rounding_digits: None,
}
}
pub fn with_price_rounding(mut self, digits: i32) -> Self {
self.rounding_digits = Some(digits);
self
}
#[inline]
fn round_price(&self, price: CentsUnsigned) -> CentsUnsigned {
match self.rounding_digits {
Some(digits) => price.round_to_dollar(digits),
None => price,
}
}
@@ -54,6 +72,7 @@ impl CostBasisData {
self.state = Some(State::deserialize(&fs::read(path)?)?);
self.pending.clear();
self.pending_raw = PendingRaw::default();
self.cache = None;
Ok(height)
}
@@ -73,29 +92,31 @@ impl CostBasisData {
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))
self.state.u().base.map.iter().map(|(&k, v)| (k, v))
}
pub fn is_empty(&self) -> bool {
self.pending.is_empty() && self.state.u().map.is_empty()
self.pending.is_empty() && self.state.u().base.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))
self.state
.u()
.base
.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))
self.state
.u()
.base
.map
.last_key_value()
.map(|(&k, v)| (k, v))
}
/// Get the exact cap_raw value (not recomputed from map).
@@ -110,7 +131,8 @@ impl CostBasisData {
self.state.u().investor_cap_raw
}
/// Increment with pre-computed typed values
/// Increment with pre-computed typed values.
/// Handles rounding and cache update.
pub fn increment(
&mut self,
price: CentsUnsigned,
@@ -118,14 +140,19 @@ impl CostBasisData {
price_sats: CentsSats,
investor_cap: CentsSquaredSats,
) {
let price = self.round_price(price);
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;
}
if let Some(cache) = self.cache.as_mut() {
cache.on_receive(price, sats);
}
}
/// Decrement with pre-computed typed values
/// Decrement with pre-computed typed values.
/// Handles rounding and cache update.
pub fn decrement(
&mut self,
price: CentsUnsigned,
@@ -133,16 +160,20 @@ impl CostBasisData {
price_sats: CentsSats,
investor_cap: CentsSquaredSats,
) {
let price = self.round_price(price);
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;
}
if let Some(cache) = self.cache.as_mut() {
cache.on_send(price, sats);
}
}
pub fn apply_pending(&mut self) {
for (cents, (inc, dec)) in self.pending.drain() {
let entry = self.state.um().map.entry(cents).or_default();
let entry = self.state.um().base.map.entry(cents).or_default();
*entry += inc;
if *entry < dec {
panic!(
@@ -159,7 +190,7 @@ impl CostBasisData {
}
*entry -= dec;
if *entry == Sats::ZERO {
self.state.um().map.remove(&cents);
self.state.um().base.map.remove(&cents);
}
}
@@ -205,6 +236,7 @@ impl CostBasisData {
self.state.replace(State::default());
self.pending.clear();
self.pending_raw = PendingRaw::default();
self.cache = None;
}
pub fn compute_percentiles(&self) -> Option<Percentiles> {
@@ -212,14 +244,49 @@ impl CostBasisData {
Percentiles::compute(self.iter().map(|(k, &v)| (k, v)))
}
pub fn compute_unrealized_states(
&mut self,
height_price: CentsUnsigned,
date_price: Option<CentsUnsigned>,
) -> (UnrealizedState, Option<UnrealizedState>) {
if self.is_empty() {
return (UnrealizedState::ZERO, date_price.map(|_| UnrealizedState::ZERO));
}
let map = &self.state.u().base.map;
let date_state =
date_price.map(|p| CachedUnrealizedState::compute_full_standalone(p.into(), map));
let height_state = if let Some(cache) = self.cache.as_mut() {
cache.get_at_price(height_price, map)
} else {
let cache = CachedUnrealizedState::compute_fresh(height_price, map);
let state = cache.current_state();
self.cache = Some(cache);
state
};
(height_state, date_state)
}
pub fn clean(&mut self) -> Result<()> {
let _ = fs::remove_dir_all(&self.pathbuf);
fs::create_dir_all(&self.pathbuf)?;
fs::create_dir_all(self.path_by_height())?;
self.cache = None;
Ok(())
}
fn path_by_height(&self) -> PathBuf {
self.pathbuf.join("by_height")
}
fn read_dir(&self, keep_only_before: Option<Height>) -> Result<BTreeMap<Height, PathBuf>> {
Ok(fs::read_dir(&self.pathbuf)?
let by_height = self.path_by_height();
if !by_height.exists() {
return Ok(BTreeMap::new());
}
Ok(fs::read_dir(&by_height)?
.filter_map(|entry| {
let path = entry.ok()?.path();
let name = path.file_name()?.to_str()?;
@@ -257,13 +324,13 @@ impl CostBasisData {
}
fn path_state(&self, height: Height) -> PathBuf {
self.pathbuf.join(u32::from(height).to_string())
self.path_by_height().join(height.to_string())
}
}
#[derive(Clone, Default, Debug)]
struct State {
map: BTreeMap<CentsUnsignedCompact, Sats>,
base: CostBasisDistribution,
/// Exact realized cap: Σ(price × sats)
cap_raw: CentsSats,
/// Exact investor cap: Σ(price² × sats)
@@ -271,51 +338,20 @@ struct State {
}
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);
fn serialize(&self) -> Result<Vec<u8>> {
let mut buffer = self.base.serialize()?;
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])?;
fn deserialize(data: &[u8]) -> Result<Self> {
let (base, rest) = CostBasisDistribution::deserialize_with_rest(data)?;
let cap_raw = CentsSats::from_bytes(&rest[0..16])?;
let investor_cap_raw = CentsSquaredSats::from_bytes(&rest[16..32])?;
Ok(Self {
map,
base,
cap_raw,
investor_cap_raw,
})
@@ -1,9 +1,12 @@
mod cost_basis_data;
mod data;
mod percentiles;
mod realized;
mod unrealized;
pub use cost_basis_data::*;
pub use data::*;
pub use percentiles::*;
pub use realized::*;
pub use unrealized::*;
pub use unrealized::UnrealizedState;
// Internal use only
pub(super) use unrealized::CachedUnrealizedState;
@@ -2,7 +2,7 @@ use std::ops::Bound;
use brk_types::{CentsUnsigned, CentsUnsignedCompact, Sats};
use super::cost_basis_data::CostBasisData;
use super::CostBasisMap;
#[derive(Debug, Default, Clone)]
pub struct UnrealizedState {
@@ -91,9 +91,7 @@ impl CachedStateRaw {
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,
),
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,
),
@@ -115,10 +113,13 @@ pub struct CachedUnrealizedState {
}
impl CachedUnrealizedState {
pub fn compute_fresh(price: CentsUnsigned, cost_basis_data: &CostBasisData) -> Self {
pub fn compute_fresh(price: CentsUnsigned, map: &CostBasisMap) -> Self {
let price: CentsUnsignedCompact = price.into();
let state = Self::compute_raw(price, cost_basis_data);
Self { state, at_price: price }
let state = Self::compute_raw(price, map);
Self {
state,
at_price: price,
}
}
/// Get the current cached state as output (without price update).
@@ -129,11 +130,11 @@ impl CachedUnrealizedState {
pub fn get_at_price(
&mut self,
new_price: CentsUnsigned,
cost_basis_data: &CostBasisData,
map: &CostBasisMap,
) -> UnrealizedState {
let new_price: CentsUnsignedCompact = new_price.into();
if new_price != self.at_price {
self.update_for_price_change(new_price, cost_basis_data);
self.update_for_price_change(new_price, map);
}
self.state.to_output()
}
@@ -186,11 +187,7 @@ impl CachedUnrealizedState {
}
}
fn update_for_price_change(
&mut self,
new_price: CentsUnsignedCompact,
cost_basis_data: &CostBasisData,
) {
fn update_for_price_change(&mut self, new_price: CentsUnsignedCompact, map: &CostBasisMap) {
let old_price = self.at_price;
if new_price > old_price {
@@ -201,8 +198,7 @@ impl CachedUnrealizedState {
// First, process UTXOs crossing from loss to profit
// Range (old_price, new_price] means: old_price < price <= new_price
for (price, &sats) in
cost_basis_data.range((Bound::Excluded(old_price), Bound::Included(new_price)))
for (&price, &sats) in map.range((Bound::Excluded(old_price), Bound::Included(new_price)))
{
let sats_u128 = sats.as_u128();
let price_u128 = price.as_u128();
@@ -233,8 +229,7 @@ impl CachedUnrealizedState {
// 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
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 {
let delta = (old_price - new_price).as_u128();
@@ -244,8 +239,7 @@ impl CachedUnrealizedState {
// First, process UTXOs crossing from profit to loss
// Range (new_price, old_price] means: new_price < price <= old_price
for (price, &sats) in
cost_basis_data.range((Bound::Excluded(new_price), Bound::Included(old_price)))
for (&price, &sats) in map.range((Bound::Excluded(new_price), Bound::Included(old_price)))
{
let sats_u128 = sats.as_u128();
let price_u128 = price.as_u128();
@@ -276,22 +270,18 @@ impl CachedUnrealizedState {
// 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
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;
}
/// Compute raw cached state from cost_basis_data.
fn compute_raw(
current_price: CentsUnsignedCompact,
cost_basis_data: &CostBasisData,
) -> CachedStateRaw {
/// Compute raw cached state from the map.
fn compute_raw(current_price: CentsUnsignedCompact, map: &CostBasisMap) -> CachedStateRaw {
let mut state = CachedStateRaw::default();
for (price, &sats) in cost_basis_data.iter() {
for (&price, &sats) in map.iter() {
let sats_u128 = sats.as_u128();
let price_u128 = price.as_u128();
let invested_capital = price_u128 * sats_u128;
@@ -321,8 +311,8 @@ impl CachedUnrealizedState {
/// Used for date_state which doesn't use the cache.
pub fn compute_full_standalone(
current_price: CentsUnsignedCompact,
cost_basis_data: &CostBasisData,
map: &CostBasisMap,
) -> UnrealizedState {
Self::compute_raw(current_price, cost_basis_data).to_output()
Self::compute_raw(current_price, map).to_output()
}
}
+9 -6
View File
@@ -1,4 +1,4 @@
use std::path::Path;
use std::path::{Path, PathBuf};
use brk_error::Result;
use brk_indexer::Indexer;
@@ -25,7 +25,7 @@ use crate::{
use super::{
AddressCohorts, AddressesDataVecs, AnyAddressIndexesVecs, UTXOCohorts,
address::{
AddrCountVecs, AddressActivityVecs, GrowthRateVecs, NewAddrCountVecs, TotalAddrCountVecs,
AddrCountsVecs, AddressActivityVecs, GrowthRateVecs, NewAddrCountVecs, TotalAddrCountVecs,
},
compute::aggregates,
};
@@ -37,6 +37,8 @@ const VERSION: Version = Version::new(22);
pub struct Vecs {
#[traversable(skip)]
db: Database,
#[traversable(skip)]
pub states_path: PathBuf,
pub supply_state: BytesVec<Height, SupplyState>,
pub any_address_indexes: AnyAddressIndexesVecs,
@@ -44,8 +46,8 @@ pub struct Vecs {
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
@@ -115,9 +117,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)?;
@@ -163,6 +165,7 @@ impl Vecs {
emptyaddressindex,
db,
states_path,
};
this.db.retain_regions(
@@ -457,6 +457,88 @@ where
}
}
/// Create from a ComputedFromHeightLast (block last with derived dates) and a LazyFromHeightLast.
pub fn from_block_last_and_lazy_block_last<F, S2SourceT>(
name: &str,
version: Version,
source1: &ComputedFromHeightLast<S1T>,
source2: &LazyFromHeightLast<S2T, S2SourceT>,
) -> Self
where
F: BinaryTransform<S1T, S2T, T>,
S1T: NumericValue,
S2SourceT: 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),
}
}
/// Create from a LazyFromHeightLast and a ComputedFromHeightLast (reversed source order).
pub fn from_lazy_block_last_and_block_last<F, S1SourceT>(
name: &str,
version: Version,
source1: &LazyFromHeightLast<S1T, S1SourceT>,
source2: &ComputedFromHeightLast<S2T>,
) -> Self
where
F: BinaryTransform<S1T, S2T, T>,
S2T: NumericValue,
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),
}
}
/// Create from a LazyDateDerivedLast source and a BinaryDateLast source.
pub fn from_derived_last_and_binary_last<F, S2aT, S2bT>(
name: &str,
@@ -7,7 +7,7 @@ use brk_types::{
use schemars::JsonSchema;
use vecdb::{IterableBoxedVec, IterableCloneableVec, UnaryTransform};
use crate::internal::{ComputedFromHeightLast, ComputedFromDateLast, ComputedVecValue, LazyDateDerivedLast, LazyTransformLast, NumericValue};
use crate::internal::{ComputedFromHeightLast, ComputedFromDateLast, ComputedVecValue, LazyBinaryFromDateLast, LazyDateDerivedLast, LazyTransformLast, NumericValue};
const VERSION: Version = Version::ZERO;
@@ -84,4 +84,34 @@ where
{
Self::from_derived::<F>(name, version, source.dateindex.boxed_clone(), &source.dates)
}
/// Create by unary-transforming a LazyBinaryFromDateLast source.
pub fn from_binary<F, S1aT, S1bT>(
name: &str,
version: Version,
source: &LazyBinaryFromDateLast<S1T, S1aT, S1bT>,
) -> Self
where
F: UnaryTransform<S1T, T>,
S1aT: ComputedVecValue + JsonSchema,
S1bT: ComputedVecValue + JsonSchema,
{
let v = version + VERSION;
macro_rules! period {
($p:ident) => {
LazyTransformLast::from_boxed::<F>(name, v, source.$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),
}
}
}
@@ -2,6 +2,8 @@
//!
//! Both dollars and sats are computed from the same source.
use std::marker::PhantomData;
use brk_traversable::Traversable;
use brk_types::{Dollars, SatsFract, Version};
use derive_more::{Deref, DerefMut};
@@ -28,7 +30,7 @@ where
}
/// Composed transform: ST -> Dollars -> SatsFract
pub struct ComposedDollarsToSatsFract<F>(std::marker::PhantomData<F>);
pub struct ComposedDollarsToSatsFract<F>(PhantomData<F>);
impl<F, ST> UnaryTransform<ST, SatsFract> for ComposedDollarsToSatsFract<F>
where
@@ -80,6 +80,58 @@ where
}
}
pub fn from_block_last_and_lazy_block_last<F, S2SourceT>(
name: &str,
version: Version,
source1: &ComputedFromHeightLast<S1T>,
source2: &LazyFromHeightLast<S2T, S2SourceT>,
) -> Self
where
F: BinaryTransform<S1T, S2T, T>,
S1T: NumericValue,
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_block_last_and_lazy_block_last::<F, _>(
name, v, source1, source2,
),
}
}
pub fn from_lazy_block_last_and_block_last<F, S1SourceT>(
name: &str,
version: Version,
source1: &LazyFromHeightLast<S1T, S1SourceT>,
source2: &ComputedFromHeightLast<S2T>,
) -> Self
where
F: BinaryTransform<S1T, S2T, T>,
S2T: NumericValue,
S1SourceT: ComputedVecValue + JsonSchema,
{
let v = version + VERSION;
Self {
height: LazyVecFrom2::transformed::<F>(
name,
v,
source1.height.boxed_clone(),
source2.height.boxed_clone(),
),
rest: LazyBinaryHeightDerivedLast::from_lazy_block_last_and_block_last::<F, _>(
name, v, source1, source2,
),
}
}
pub fn from_computed_height_date_last<F: BinaryTransform<S1T, S2T, T>>(
name: &str,
version: Version,
@@ -0,0 +1,72 @@
//! Fully lazy binary price wrapper with both USD and sats representations.
//!
//! All levels (height, dateindex, date periods, difficultyepoch) are lazy.
//! Derives dateindex from the two source dateindexes rather than storing it.
use brk_traversable::Traversable;
use brk_types::{CentsUnsigned, Dollars, SatsFract, Version};
use derive_more::{Deref, DerefMut};
use vecdb::BinaryTransform;
use crate::internal::{
DollarsToSatsFract, LazyBinaryFromHeightLast, LazyFromHeightLast, LazyPriceFromCents,
PriceFromHeight,
};
/// Fully lazy binary price metric with both USD and sats representations.
///
/// Dollars: lazy binary transform at all levels (height, dateindex, date periods, difficultyepoch).
/// Sats: lazy unary transform of dollars.
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(merge)]
pub struct LazyBinaryPriceFromHeight {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub dollars: LazyBinaryFromHeightLast<Dollars, Dollars, Dollars>,
pub sats: LazyFromHeightLast<SatsFract, Dollars>,
}
impl LazyBinaryPriceFromHeight {
/// Create from a PriceFromHeight (source1) and a LazyPriceFromCents (source2).
pub fn from_price_and_lazy_price<F: BinaryTransform<Dollars, Dollars, Dollars>>(
name: &str,
version: Version,
source1: &PriceFromHeight,
source2: &LazyPriceFromCents,
) -> Self {
let dollars = LazyBinaryFromHeightLast::from_block_last_and_lazy_block_last::<
F,
CentsUnsigned,
>(name, version, &source1.dollars, &source2.dollars);
let sats = LazyFromHeightLast::from_binary::<DollarsToSatsFract, _, _>(
&format!("{name}_sats"),
version,
&dollars,
);
Self { dollars, sats }
}
/// Create from a LazyPriceFromCents (source1) and a PriceFromHeight (source2).
pub fn from_lazy_price_and_price<F: BinaryTransform<Dollars, Dollars, Dollars>>(
name: &str,
version: Version,
source1: &LazyPriceFromCents,
source2: &PriceFromHeight,
) -> Self {
let dollars = LazyBinaryFromHeightLast::from_lazy_block_last_and_block_last::<
F,
CentsUnsigned,
>(name, version, &source1.dollars, &source2.dollars);
let sats = LazyFromHeightLast::from_binary::<DollarsToSatsFract, _, _>(
&format!("{name}_sats"),
version,
&dollars,
);
Self { dollars, sats }
}
}
@@ -8,7 +8,8 @@ use vecdb::{IterableBoxedVec, IterableCloneableVec, LazyVecFrom1, UnaryTransform
use crate::internal::{
ComputedFromHeightAndDateLast, ComputedFromHeightLast, ComputedHeightDerivedLast,
ComputedVecValue, LazyBinaryComputedFromHeightLast, LazyHeightDerivedLast, NumericValue,
ComputedVecValue, LazyBinaryComputedFromHeightLast, LazyBinaryFromHeightLast,
LazyHeightDerivedLast, NumericValue,
};
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(merge)]
@@ -96,4 +97,22 @@ where
rest: LazyHeightDerivedLast::from_derived_computed::<F>(name, v, &source.rest),
}
}
/// Create by unary-transforming a LazyBinaryFromHeightLast source.
pub fn from_binary<F, S1aT, S1bT>(
name: &str,
version: Version,
source: &LazyBinaryFromHeightLast<S1T, S1aT, S1bT>,
) -> Self
where
F: UnaryTransform<S1T, T>,
S1aT: ComputedVecValue + JsonSchema,
S1bT: ComputedVecValue + JsonSchema,
{
let v = version + VERSION;
Self {
height: LazyVecFrom1::transformed::<F>(name, v, source.height.boxed_clone()),
rest: LazyHeightDerivedLast::from_binary::<F, _, _>(name, v, &source.rest),
}
}
}
@@ -5,29 +5,30 @@ mod binary_sum_cum;
mod distribution;
mod full;
mod last;
mod lazy_distribution;
mod lazy_full;
mod lazy_transform_distribution;
mod lazy_binary_computed_distribution;
mod lazy_binary_computed_full;
mod lazy_binary_computed_last;
mod lazy_binary_computed_sum;
mod lazy_binary_computed_sum_cum;
mod lazy_binary_price;
mod lazy_computed_full;
mod lazy_computed_sum_cum;
mod lazy_distribution;
mod lazy_full;
mod lazy_last;
mod lazy_price_from_cents;
mod lazy_sum;
mod price;
mod unary_last;
mod lazy_sum_cum;
mod lazy_transform_distribution;
mod lazy_value;
mod price;
mod sum;
mod sum_cum;
mod unary_last;
mod value_binary;
mod value_full;
mod value_last;
mod value_lazy_binary_last;
mod lazy_value;
mod value_lazy_computed_sum_cum;
mod value_lazy_last;
mod value_lazy_sum_cum;
@@ -41,29 +42,30 @@ pub use binary_sum_cum::*;
pub use distribution::*;
pub use full::*;
pub use last::*;
pub use lazy_distribution::*;
pub use lazy_full::*;
pub use lazy_transform_distribution::*;
pub use lazy_binary_computed_distribution::*;
pub use lazy_binary_computed_full::*;
pub use lazy_binary_computed_last::*;
pub use lazy_binary_computed_sum::*;
pub use lazy_binary_computed_sum_cum::*;
pub use lazy_binary_price::*;
pub use lazy_computed_full::*;
pub use lazy_computed_sum_cum::*;
pub use lazy_distribution::*;
pub use lazy_full::*;
pub use lazy_last::*;
pub use lazy_price_from_cents::*;
pub use lazy_sum::*;
pub use price::*;
pub use unary_last::*;
pub use lazy_sum_cum::*;
pub use lazy_transform_distribution::*;
pub use lazy_value::*;
pub use price::*;
pub use sum::*;
pub use sum_cum::*;
pub use unary_last::*;
pub use value_binary::*;
pub use value_full::*;
pub use value_last::*;
pub use value_lazy_binary_last::*;
pub use lazy_value::*;
pub use value_lazy_computed_sum_cum::*;
pub use value_lazy_last::*;
pub use value_lazy_sum_cum::*;
@@ -86,6 +86,32 @@ where
}
}
pub fn from_lazy_block_last_and_block_last<F, S1SourceT>(
name: &str,
version: Version,
source1: &LazyFromHeightLast<S1T, S1SourceT>,
source2: &ComputedFromHeightLast<S2T>,
) -> Self
where
F: BinaryTransform<S1T, S2T, T>,
S2T: NumericValue,
S1SourceT: ComputedVecValue + JsonSchema,
{
let v = version + VERSION;
Self {
dates: LazyBinaryFromDateLast::from_lazy_block_last_and_block_last::<F, _>(
name, v, source1, source2,
),
difficultyepoch: LazyBinaryTransformLast::from_vecs::<F>(
name,
v,
source1.rest.difficultyepoch.boxed_clone(),
source2.rest.difficultyepoch.boxed_clone(),
),
}
}
pub fn from_computed_height_date_last<F: BinaryTransform<S1T, S2T, T>>(
name: &str,
version: Version,
@@ -114,6 +140,32 @@ where
}
}
pub fn from_block_last_and_lazy_block_last<F, S2SourceT>(
name: &str,
version: Version,
source1: &ComputedFromHeightLast<S1T>,
source2: &LazyFromHeightLast<S2T, S2SourceT>,
) -> Self
where
F: BinaryTransform<S1T, S2T, T>,
S1T: NumericValue,
S2SourceT: ComputedVecValue + JsonSchema,
{
let v = version + VERSION;
Self {
dates: LazyBinaryFromDateLast::from_block_last_and_lazy_block_last::<F, _>(
name, v, source1, source2,
),
difficultyepoch: LazyBinaryTransformLast::from_vecs::<F>(
name,
v,
source1.rest.difficultyepoch.boxed_clone(),
source2.rest.difficultyepoch.boxed_clone(),
),
}
}
pub fn from_computed_height_date_and_block_last<F: BinaryTransform<S1T, S2T, T>>(
name: &str,
version: Version,
@@ -8,7 +8,7 @@ use vecdb::{IterableCloneableVec, UnaryTransform};
use crate::internal::{
ComputedFromHeightLast, ComputedHeightDerivedLast, ComputedFromHeightAndDateLast, ComputedVecValue,
LazyFromDateLast, LazyTransformLast, NumericValue,
LazyBinaryHeightDerivedLast, LazyFromDateLast, LazyTransformLast, NumericValue,
};
#[derive(Clone, Deref, DerefMut, Traversable)]
@@ -81,6 +81,29 @@ where
}
}
/// Create by unary-transforming a LazyBinaryHeightDerivedLast source.
pub fn from_binary<F, S1aT, S1bT>(
name: &str,
version: Version,
source: &LazyBinaryHeightDerivedLast<S1T, S1aT, S1bT>,
) -> Self
where
F: UnaryTransform<S1T, T>,
S1aT: ComputedVecValue + JsonSchema,
S1bT: ComputedVecValue + JsonSchema,
{
let v = version + VERSION;
Self {
dates: LazyFromDateLast::from_binary::<F, _, _>(name, v, &source.dates),
difficultyepoch: LazyTransformLast::from_boxed::<F>(
name,
v,
source.difficultyepoch.boxed_clone(),
),
}
}
pub fn from_computed_height_date<F: UnaryTransform<S1T, T>>(
name: &str,
version: Version,
@@ -0,0 +1,18 @@
use brk_types::Dollars;
use vecdb::BinaryTransform;
/// (Dollars, Dollars) -> Dollars: a² / b
pub struct DollarsSquaredDivide;
impl BinaryTransform<Dollars, Dollars, Dollars> for DollarsSquaredDivide {
#[inline(always)]
fn apply(a: Dollars, b: Dollars) -> Dollars {
let af = f64::from(a);
let bf = f64::from(b);
if bf == 0.0 {
Dollars::NAN
} else {
Dollars::from(af * af / bf)
}
}
}
@@ -4,10 +4,11 @@ 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_squared_divide;
mod dollars_to_sats_fract;
mod f32_identity;
mod half_close_price_times_sats;
mod ohlc;
@@ -19,7 +20,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;
@@ -47,10 +47,11 @@ 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_squared_divide::*;
pub use dollars_to_sats_fract::*;
pub use f32_identity::*;
pub use half_close_price_times_sats::*;
pub use ohlc::*;
@@ -61,10 +62,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::*;
@@ -79,7 +79,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
View File
@@ -1 +0,0 @@
oracle
+2 -13
View File
@@ -6,7 +6,6 @@ use super::Vecs;
use crate::{indexes, ComputeIndexes};
impl Vecs {
#[allow(unused_variables)]
pub fn compute(
&mut self,
indexer: &Indexer,
@@ -18,18 +17,8 @@ impl Vecs {
self.sats.compute(starting_indexes, &self.usd, exit)?;
// Oracle price computation is slow and still WIP, only run in dev builds
// #[cfg(debug_assertions)]
// {
// use std::time::Instant;
// use tracing::info;
//
// info!("Computing oracle prices...");
// let i = Instant::now();
// self.oracle
// .compute(indexer, indexes, &self.cents, starting_indexes, exit)?;
// info!("Computed oracle prices in {:?}", i.elapsed());
// }
self.oracle
.compute(indexer, indexes, starting_indexes, exit)?;
let _lock = exit.lock();
self.db().compact()?;
+5 -5
View File
@@ -2,12 +2,12 @@ mod compute;
mod fetch;
pub mod cents;
// pub mod oracle;
pub mod oracle;
pub mod sats;
pub mod usd;
pub use cents::Vecs as CentsVecs;
// pub use oracle::Vecs as OracleVecs;
pub use oracle::Vecs as OracleVecs;
pub use sats::Vecs as SatsVecs;
pub use usd::Vecs as UsdVecs;
@@ -33,7 +33,7 @@ pub struct Vecs {
pub cents: CentsVecs,
pub usd: UsdVecs,
pub sats: SatsVecs,
// pub oracle: OracleVecs,
pub oracle: OracleVecs,
}
impl Vecs {
@@ -67,7 +67,7 @@ impl Vecs {
let cents = CentsVecs::forced_import(db, version)?;
let usd = UsdVecs::forced_import(db, version, indexes)?;
let sats = SatsVecs::forced_import(db, version, indexes)?;
// let oracle = OracleVecs::forced_import(db, version)?;
let oracle = OracleVecs::forced_import(db, version, indexes)?;
Ok(Self {
db: db.clone(),
@@ -75,7 +75,7 @@ impl Vecs {
cents,
usd,
sats,
// oracle,
oracle,
})
}
@@ -0,0 +1,491 @@
use std::ops::Range;
use brk_error::Result;
use brk_indexer::Indexer;
use brk_oracle::{Config, NUM_BINS, Oracle, START_HEIGHT, bin_to_cents, cents_to_bin};
use brk_types::{
CentsUnsigned, Close, DateIndex, Height, High, Low, OHLCCentsUnsigned, OHLCDollars, Open,
OutputType, Sats, TxIndex, TxOutIndex,
};
use tracing::info;
use vecdb::{
AnyStoredVec, AnyVec, Exit, GenericStoredVec, IterableVec, TypedVecIterator, VecIndex,
VecIterator,
};
use super::Vecs;
use crate::{ComputeIndexes, indexes};
impl Vecs {
pub fn compute(
&mut self,
indexer: &Indexer,
indexes: &indexes::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
self.compute_prices(indexer, starting_indexes, exit)?;
self.compute_daily_ohlc(indexes, starting_indexes, exit)?;
self.compute_split_and_ohlc(starting_indexes, exit)?;
Ok(())
}
fn compute_split_and_ohlc(
&mut self,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
// Destructure to allow simultaneous borrows of different fields
let Self {
price_cents,
ohlc_cents,
split,
ohlc,
ohlc_dollars,
} = self;
// Open: first-value aggregation
split.open.height.compute_transform(
starting_indexes.height,
&*price_cents,
|(h, price, ..)| (h, Open::new(price)),
exit,
)?;
split.open.compute_rest(starting_indexes, exit, |v| {
v.compute_transform(
starting_indexes.dateindex,
&*ohlc_cents,
|(di, ohlc_val, ..)| (di, ohlc_val.open),
exit,
)?;
Ok(())
})?;
// High: max-value aggregation
split.high.height.compute_transform(
starting_indexes.height,
&*price_cents,
|(h, price, ..)| (h, High::new(price)),
exit,
)?;
split.high.compute_rest(starting_indexes, exit, |v| {
v.compute_transform(
starting_indexes.dateindex,
&*ohlc_cents,
|(di, ohlc_val, ..)| (di, ohlc_val.high),
exit,
)?;
Ok(())
})?;
// Low: min-value aggregation
split.low.height.compute_transform(
starting_indexes.height,
&*price_cents,
|(h, price, ..)| (h, Low::new(price)),
exit,
)?;
split.low.compute_rest(starting_indexes, exit, |v| {
v.compute_transform(
starting_indexes.dateindex,
&*ohlc_cents,
|(di, ohlc_val, ..)| (di, ohlc_val.low),
exit,
)?;
Ok(())
})?;
// Close: last-value aggregation
split.close.height.compute_transform(
starting_indexes.height,
&*price_cents,
|(h, price, ..)| (h, Close::new(price)),
exit,
)?;
split.close.compute_rest(starting_indexes, exit, |v| {
v.compute_transform(
starting_indexes.dateindex,
&*ohlc_cents,
|(di, ohlc_val, ..)| (di, ohlc_val.close),
exit,
)?;
Ok(())
})?;
// Period OHLC aggregates - time based
ohlc.dateindex.compute_transform4(
starting_indexes.dateindex,
&split.open.dateindex,
&split.high.dateindex,
&split.low.dateindex,
&split.close.dateindex,
|(i, open, high, low, close, _)| {
(i, OHLCCentsUnsigned { open, high, low, close })
},
exit,
)?;
ohlc.week.compute_transform4(
starting_indexes.weekindex,
&*split.open.weekindex,
&*split.high.weekindex,
&*split.low.weekindex,
&*split.close.weekindex,
|(i, open, high, low, close, _)| {
(i, OHLCCentsUnsigned { open, high, low, close })
},
exit,
)?;
ohlc.month.compute_transform4(
starting_indexes.monthindex,
&*split.open.monthindex,
&*split.high.monthindex,
&*split.low.monthindex,
&*split.close.monthindex,
|(i, open, high, low, close, _)| {
(i, OHLCCentsUnsigned { open, high, low, close })
},
exit,
)?;
ohlc.quarter.compute_transform4(
starting_indexes.quarterindex,
&*split.open.quarterindex,
&*split.high.quarterindex,
&*split.low.quarterindex,
&*split.close.quarterindex,
|(i, open, high, low, close, _)| {
(i, OHLCCentsUnsigned { open, high, low, close })
},
exit,
)?;
ohlc.semester.compute_transform4(
starting_indexes.semesterindex,
&*split.open.semesterindex,
&*split.high.semesterindex,
&*split.low.semesterindex,
&*split.close.semesterindex,
|(i, open, high, low, close, _)| {
(i, OHLCCentsUnsigned { open, high, low, close })
},
exit,
)?;
ohlc.year.compute_transform4(
starting_indexes.yearindex,
&*split.open.yearindex,
&*split.high.yearindex,
&*split.low.yearindex,
&*split.close.yearindex,
|(i, open, high, low, close, _)| {
(i, OHLCCentsUnsigned { open, high, low, close })
},
exit,
)?;
ohlc.decade.compute_transform4(
starting_indexes.decadeindex,
&*split.open.decadeindex,
&*split.high.decadeindex,
&*split.low.decadeindex,
&*split.close.decadeindex,
|(i, open, high, low, close, _)| {
(i, OHLCCentsUnsigned { open, high, low, close })
},
exit,
)?;
// Period OHLC aggregates - chain based
ohlc.height.compute_transform4(
starting_indexes.height,
&split.open.height,
&split.high.height,
&split.low.height,
&split.close.height,
|(i, open, high, low, close, _)| {
(i, OHLCCentsUnsigned { open, high, low, close })
},
exit,
)?;
ohlc.difficultyepoch.compute_transform4(
starting_indexes.difficultyepoch,
&*split.open.difficultyepoch,
&*split.high.difficultyepoch,
&*split.low.difficultyepoch,
&*split.close.difficultyepoch,
|(i, open, high, low, close, _)| {
(i, OHLCCentsUnsigned { open, high, low, close })
},
exit,
)?;
// OHLC dollars - transform cents to dollars at every period level
macro_rules! cents_to_dollars {
($field:ident, $idx:expr) => {
ohlc_dollars.$field.compute_transform(
$idx,
&ohlc.$field,
|(i, c, ..)| (i, OHLCDollars::from(c)),
exit,
)?;
};
}
cents_to_dollars!(dateindex, starting_indexes.dateindex);
cents_to_dollars!(week, starting_indexes.weekindex);
cents_to_dollars!(month, starting_indexes.monthindex);
cents_to_dollars!(quarter, starting_indexes.quarterindex);
cents_to_dollars!(semester, starting_indexes.semesterindex);
cents_to_dollars!(year, starting_indexes.yearindex);
cents_to_dollars!(decade, starting_indexes.decadeindex);
cents_to_dollars!(height, starting_indexes.height);
cents_to_dollars!(difficultyepoch, starting_indexes.difficultyepoch);
Ok(())
}
fn compute_prices(
&mut self,
indexer: &Indexer,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
let source_version =
indexer.vecs.outputs.value.version() + indexer.vecs.outputs.outputtype.version();
self.price_cents
.validate_computed_version_or_reset(source_version)?;
let total_heights = indexer.vecs.blocks.timestamp.len();
if total_heights <= START_HEIGHT {
return Ok(());
}
// Reorg: truncate to starting_indexes
let truncate_to = self
.price_cents
.len()
.min(starting_indexes.height.to_usize());
self.price_cents.truncate_if_needed_at(truncate_to)?;
if self.price_cents.len() < START_HEIGHT {
for line in brk_oracle::PRICES.lines().skip(self.price_cents.len()) {
if self.price_cents.len() >= START_HEIGHT {
break;
}
let dollars: f64 = line.parse().unwrap_or(0.0);
let cents = (dollars * 100.0).round() as u64;
self.price_cents.push(CentsUnsigned::new(cents));
}
}
if self.price_cents.len() >= total_heights {
return Ok(());
}
let config = Config::default();
let committed = self.price_cents.len();
let prev_cents = self.price_cents
.iter()?
.get(Height::from(committed - 1))
.unwrap();
let seed_bin = cents_to_bin(prev_cents.inner() as f64);
let warmup = config.window_size.min(committed - START_HEIGHT);
let mut oracle = Oracle::from_checkpoint(seed_bin, config, |o| {
Self::feed_blocks(o, indexer, (committed - warmup)..committed);
});
let num_new = total_heights - committed;
info!(
"Computing oracle prices: {} to {} ({warmup} warmup)",
committed, total_heights
);
let ref_bins = Self::feed_blocks(&mut oracle, indexer, committed..total_heights);
for (i, ref_bin) in ref_bins.into_iter().enumerate() {
self.price_cents.push(CentsUnsigned::new(bin_to_cents(ref_bin)));
let progress = ((i + 1) * 100 / num_new) as u8;
if i > 0 && progress > ((i * 100 / num_new) as u8) {
info!("Oracle price computation: {}%", progress);
}
}
{
let _lock = exit.lock();
self.price_cents.write()?;
}
info!(
"Oracle prices complete: {} committed",
self.price_cents.len()
);
Ok(())
}
/// Returns an Oracle seeded from the last committed price, with the last
/// window_size blocks already processed. Ready for additional blocks (e.g. mempool).
pub fn live_oracle(&self, indexer: &Indexer) -> Result<Oracle> {
let config = Config::default();
let height = indexer.vecs.blocks.timestamp.len();
let last_cents = self.price_cents
.iter()?
.get(Height::from(self.price_cents.len() - 1))
.unwrap();
let seed_bin = cents_to_bin(last_cents.inner() as f64);
let window_size = config.window_size;
let oracle = Oracle::from_checkpoint(seed_bin, config, |o| {
Self::feed_blocks(o, indexer, height.saturating_sub(window_size)..height);
});
Ok(oracle)
}
/// Feed a range of blocks from the indexer into an Oracle (skipping coinbase),
/// returning per-block ref_bin values.
fn feed_blocks(oracle: &mut Oracle, indexer: &Indexer, range: Range<usize>) -> Vec<f64> {
let total_txs = indexer.vecs.transactions.height.len();
let total_outputs = indexer.vecs.outputs.value.len();
let mut first_txindex_iter = indexer.vecs.transactions.first_txindex.into_iter();
let mut first_txoutindex_iter = indexer.vecs.transactions.first_txoutindex.into_iter();
let mut out_first_iter = indexer.vecs.outputs.first_txoutindex.into_iter();
let mut value_iter = indexer.vecs.outputs.value.into_iter();
let mut outputtype_iter = indexer.vecs.outputs.outputtype.into_iter();
let mut ref_bins = Vec::with_capacity(range.len());
for h in range {
let first_txindex: TxIndex = first_txindex_iter.get_at_unwrap(h);
let next_first_txindex = first_txindex_iter
.get_at(h + 1)
.unwrap_or(TxIndex::from(total_txs));
let out_start = if first_txindex.to_usize() + 1 < next_first_txindex.to_usize() {
first_txoutindex_iter
.get_at_unwrap(first_txindex.to_usize() + 1)
.to_usize()
} else {
out_first_iter
.get_at(h + 1)
.unwrap_or(TxOutIndex::from(total_outputs))
.to_usize()
};
let out_end = out_first_iter
.get_at(h + 1)
.unwrap_or(TxOutIndex::from(total_outputs))
.to_usize();
let mut hist = [0u32; NUM_BINS];
for i in out_start..out_end {
let sats: Sats = value_iter.get_at_unwrap(i);
let output_type: OutputType = outputtype_iter.get_at_unwrap(i);
if let Some(bin) = oracle.output_to_bin(sats, output_type) {
hist[bin] += 1;
}
}
ref_bins.push(oracle.process_histogram(&hist));
}
ref_bins
}
fn compute_daily_ohlc(
&mut self,
indexes: &indexes::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
let last_dateindex = DateIndex::from(indexes.dateindex.date.len());
let start_dateindex = starting_indexes
.dateindex
.min(DateIndex::from(self.ohlc_cents.len()));
if start_dateindex >= last_dateindex {
return Ok(());
}
let last_height = Height::from(self.price_cents.len());
let mut height_to_price_iter = self.price_cents.iter()?;
let mut dateindex_to_first_height_iter = indexes.dateindex.first_height.iter();
let mut height_count_iter = indexes.dateindex.height_count.iter();
for dateindex_usize in start_dateindex.to_usize()..last_dateindex.to_usize() {
let dateindex = DateIndex::from(dateindex_usize);
let first_height = dateindex_to_first_height_iter.get_unwrap(dateindex);
let count = height_count_iter.get_unwrap(dateindex);
if *count == 0 || first_height >= last_height {
self.ohlc_cents
.truncate_push(dateindex, self.previous_ohlc(dateindex)?)?;
continue;
}
let count = *count as usize;
let mut open = None;
let mut high = CentsUnsigned::ZERO;
let mut low = CentsUnsigned::MAX;
let mut close = CentsUnsigned::ZERO;
for i in 0..count {
let height = first_height + Height::from(i);
if height >= last_height {
break;
}
if let Some(price) = height_to_price_iter.get(height) {
if price == CentsUnsigned::ZERO {
continue;
}
if open.is_none() {
open = Some(price);
}
if price > high {
high = price;
}
if price < low {
low = price;
}
close = price;
}
}
let ohlc = if let Some(open_price) = open {
OHLCCentsUnsigned {
open: Open::new(open_price),
high: High::new(high),
low: Low::new(low),
close: Close::new(close),
}
} else {
self.previous_ohlc(dateindex)?
};
self.ohlc_cents.truncate_push(dateindex, ohlc)?;
}
{
let _lock = exit.lock();
self.ohlc_cents.write()?;
}
Ok(())
}
fn previous_ohlc(&self, dateindex: DateIndex) -> Result<OHLCCentsUnsigned> {
Ok(if dateindex > DateIndex::from(0usize) {
self.ohlc_cents
.iter()?
.get(dateindex.decremented().unwrap())
.unwrap_or_default()
} else {
OHLCCentsUnsigned::default()
})
}
}
@@ -0,0 +1,54 @@
use brk_error::Result;
use brk_types::Version;
use vecdb::{BytesVec, Database, EagerVec, ImportableVec, PcoVec};
use super::Vecs;
use crate::indexes;
use crate::internal::{ComputedOHLC, LazyFromHeightAndDateOHLC};
impl Vecs {
pub fn forced_import(
db: &Database,
parent_version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let version = parent_version + Version::new(11);
let price_cents = PcoVec::forced_import(db, "oracle_price_cents", version)?;
let ohlc_cents = BytesVec::forced_import(db, "oracle_ohlc_cents", version)?;
let split = ComputedOHLC::forced_import(db, "oracle_price", version, indexes)?;
let ohlc = LazyFromHeightAndDateOHLC {
dateindex: EagerVec::forced_import(db, "oracle_price_ohlc", version)?,
week: EagerVec::forced_import(db, "oracle_price_ohlc", version)?,
month: EagerVec::forced_import(db, "oracle_price_ohlc", version)?,
quarter: EagerVec::forced_import(db, "oracle_price_ohlc", version)?,
semester: EagerVec::forced_import(db, "oracle_price_ohlc", version)?,
year: EagerVec::forced_import(db, "oracle_price_ohlc", version)?,
decade: EagerVec::forced_import(db, "oracle_price_ohlc", version)?,
height: EagerVec::forced_import(db, "oracle_price_ohlc", version)?,
difficultyepoch: EagerVec::forced_import(db, "oracle_price_ohlc", version)?,
};
let ohlc_dollars = LazyFromHeightAndDateOHLC {
dateindex: EagerVec::forced_import(db, "oracle_ohlc_dollars", version)?,
week: EagerVec::forced_import(db, "oracle_ohlc_dollars", version)?,
month: EagerVec::forced_import(db, "oracle_ohlc_dollars", version)?,
quarter: EagerVec::forced_import(db, "oracle_ohlc_dollars", version)?,
semester: EagerVec::forced_import(db, "oracle_ohlc_dollars", version)?,
year: EagerVec::forced_import(db, "oracle_ohlc_dollars", version)?,
decade: EagerVec::forced_import(db, "oracle_ohlc_dollars", version)?,
height: EagerVec::forced_import(db, "oracle_ohlc_dollars", version)?,
difficultyepoch: EagerVec::forced_import(db, "oracle_ohlc_dollars", version)?,
};
Ok(Self {
price_cents,
ohlc_cents,
split,
ohlc,
ohlc_dollars,
})
}
}
@@ -0,0 +1,14 @@
use brk_traversable::Traversable;
use brk_types::{CentsUnsigned, DateIndex, Height, OHLCCentsUnsigned, OHLCDollars};
use vecdb::{BytesVec, PcoVec};
use crate::internal::{ComputedOHLC, LazyFromHeightAndDateOHLC};
#[derive(Clone, Traversable)]
pub struct Vecs {
pub price_cents: PcoVec<Height, CentsUnsigned>,
pub ohlc_cents: BytesVec<DateIndex, OHLCCentsUnsigned>,
pub split: ComputedOHLC<CentsUnsigned>,
pub ohlc: LazyFromHeightAndDateOHLC<OHLCCentsUnsigned>,
pub ohlc_dollars: LazyFromHeightAndDateOHLC<OHLCDollars>,
}
@@ -1,20 +0,0 @@
use brk_types::Version;
use super::Vecs;
use crate::{
distribution,
internal::{DollarsIdentity, LazyValueFromHeightLast, SatsIdentity},
};
impl Vecs {
pub fn import(version: Version, distribution: &distribution::Vecs) -> Self {
let supply_metrics = &distribution.utxo_cohorts.all.metrics.supply;
Self(LazyValueFromHeightLast::from_block_source::<
SatsIdentity,
DollarsIdentity,
>(
"circulating_supply", &supply_metrics.total, version)
)
}
}
@@ -1,4 +0,0 @@
mod import;
mod vecs;
pub use vecs::Vecs;
@@ -1,8 +0,0 @@
use brk_traversable::Traversable;
use derive_more::{Deref, DerefMut};
use crate::internal::LazyValueFromHeightLast;
/// Circulating supply - lazy references to distribution's actual supply (KISS)
#[derive(Clone, Deref, DerefMut, Traversable)]
pub struct Vecs(pub LazyValueFromHeightLast);
+50 -5
View File
@@ -2,7 +2,7 @@ use brk_error::Result;
use vecdb::Exit;
use super::Vecs;
use crate::{blocks, distribution, indexes, scripts, transactions, ComputeIndexes};
use crate::{ComputeIndexes, blocks, distribution, indexes, scripts, transactions};
impl Vecs {
#[allow(clippy::too_many_arguments)]
@@ -20,15 +20,60 @@ impl Vecs {
self.burned
.compute(indexes, scripts, blocks, starting_indexes, exit)?;
// 2. Compute inflation rate
self.inflation
.compute(blocks, distribution, starting_indexes, exit)?;
// 2. Compute inflation rate: daily_subsidy / circulating_supply * 365 * 100
let circulating_supply = &distribution.utxo_cohorts.all.metrics.supply.total.sats;
self.inflation.compute_all(starting_indexes, exit, |v| {
v.compute_transform2(
starting_indexes.dateindex,
&blocks.rewards.subsidy.sats.dateindex.sum_cum.sum.0,
&circulating_supply.dateindex.0,
|(i, subsidy_1d_sum, supply, ..)| {
let inflation = if *supply > 0 {
365.0 * *subsidy_1d_sum as f64 / *supply as f64 * 100.0
} else {
0.0
};
(i, inflation.into())
},
exit,
)?;
Ok(())
})?;
// 3. Compute velocity
self.velocity
.compute(transactions, distribution, starting_indexes, exit)?;
// Note: circulating and market_cap are lazy - no compute needed
// 4. Compute cap growth rates
if let Some(market_cap) = self.market_cap.as_ref() {
let mcap_dateindex = &market_cap.dateindex.0;
self.market_cap_growth_rate
.compute_all(starting_indexes, exit, |vec| {
vec.compute_percentage_change(
starting_indexes.dateindex,
mcap_dateindex,
365,
exit,
)?;
Ok(())
})?;
}
if let Some(realized) = distribution.utxo_cohorts.all.metrics.realized.as_ref() {
let rcap_dateindex = &realized.realized_cap.dateindex.0;
self.realized_cap_growth_rate
.compute_all(starting_indexes, exit, |vec| {
vec.compute_percentage_change(
starting_indexes.dateindex,
rcap_dateindex,
365,
exit,
)?;
Ok(())
})?;
}
// Note: circulating, market_cap, cap_growth_rate_diff are lazy
let _lock = exit.lock();
self.db.compact()?;
+51 -7
View File
@@ -3,12 +3,19 @@ use std::path::Path;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::Version;
use vecdb::{Database, PAGE_SIZE};
use vecdb::{Database, IterableCloneableVec, LazyVecFrom2, PAGE_SIZE};
use super::Vecs;
use crate::{distribution, indexes, price};
use crate::{
distribution, indexes,
internal::{
ComputedFromDateAverage, ComputedFromDateLast, DifferenceF32, DollarsIdentity,
LazyFromHeightLast, LazyValueFromHeightLast, SatsIdentity,
},
price,
};
const VERSION: Version = Version::ZERO;
const VERSION: Version = Version::ONE;
impl Vecs {
pub fn forced_import(
@@ -24,21 +31,55 @@ impl Vecs {
let version = parent_version + VERSION;
let compute_dollars = price.is_some();
let supply_metrics = &distribution.utxo_cohorts.all.metrics.supply;
// Circulating supply - lazy refs to distribution
let circulating = super::circulating::Vecs::import(version, distribution);
let circulating = LazyValueFromHeightLast::from_block_source::<SatsIdentity, DollarsIdentity>(
"circulating_supply",
&supply_metrics.total,
version,
);
// Burned/unspendable supply - computed from scripts
let burned = super::burned::Vecs::forced_import(&db, version, indexes, price)?;
// Inflation rate
let inflation = super::inflation::Vecs::forced_import(&db, version, indexes)?;
let inflation =
ComputedFromDateAverage::forced_import(&db, "inflation_rate", version, indexes)?;
// Velocity
let velocity =
super::velocity::Vecs::forced_import(&db, version, indexes, compute_dollars)?;
// Market cap - lazy refs to supply in USD
let market_cap = super::market_cap::Vecs::import(version, distribution);
// Market cap - lazy identity from distribution supply in USD
let market_cap = supply_metrics.total.dollars.as_ref().map(|d| {
LazyFromHeightLast::from_lazy_binary_computed::<DollarsIdentity, _, _>(
"market_cap",
version,
d.height.boxed_clone(),
d,
)
});
// Growth rates
let market_cap_growth_rate = ComputedFromDateLast::forced_import(
&db,
"market_cap_growth_rate",
version + Version::ONE,
indexes,
)?;
let realized_cap_growth_rate = ComputedFromDateLast::forced_import(
&db,
"realized_cap_growth_rate",
version + Version::ONE,
indexes,
)?;
let cap_growth_rate_diff = LazyVecFrom2::transformed::<DifferenceF32>(
"cap_growth_rate_diff",
version,
market_cap_growth_rate.dateindex.boxed_clone(),
realized_cap_growth_rate.dateindex.boxed_clone(),
);
let this = Self {
db,
@@ -47,6 +88,9 @@ impl Vecs {
inflation,
velocity,
market_cap,
market_cap_growth_rate,
realized_cap_growth_rate,
cap_growth_rate_diff,
};
this.db.retain_regions(
@@ -1,38 +0,0 @@
use brk_error::Result;
use vecdb::Exit;
use super::Vecs;
use crate::{ComputeIndexes, blocks, distribution};
impl Vecs {
pub fn compute(
&mut self,
blocks: &blocks::Vecs,
distribution: &distribution::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
// inflation = daily_subsidy / circulating_supply * 365 * 100
let circulating_supply = &distribution.utxo_cohorts.all.metrics.supply.total.sats;
self.compute_all(starting_indexes, exit, |v| {
v.compute_transform2(
starting_indexes.dateindex,
&blocks.rewards.subsidy.sats.dateindex.sum_cum.sum.0,
&circulating_supply.dateindex.0,
|(i, subsidy_1d_sum, supply, ..)| {
let inflation = if *supply > 0 {
365.0 * *subsidy_1d_sum as f64 / *supply as f64 * 100.0
} else {
0.0
};
(i, inflation.into())
},
exit,
)?;
Ok(())
})?;
Ok(())
}
}
@@ -1,17 +0,0 @@
use brk_error::Result;
use brk_types::Version;
use vecdb::Database;
use super::Vecs;
use crate::{indexes, internal::ComputedFromDateAverage};
impl Vecs {
pub fn forced_import(db: &Database, version: Version, indexes: &indexes::Vecs) -> Result<Self> {
Ok(Self(ComputedFromDateAverage::forced_import(
db,
"inflation_rate",
version,
indexes,
)?))
}
}
@@ -1,10 +0,0 @@
use brk_traversable::Traversable;
use brk_types::StoredF32;
use derive_more::{Deref, DerefMut};
use crate::internal::ComputedFromDateAverage;
/// Inflation rate metrics
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct Vecs(pub ComputedFromDateAverage<StoredF32>);
@@ -1,23 +0,0 @@
use brk_types::Version;
use vecdb::IterableCloneableVec;
use super::Vecs;
use crate::{
distribution,
internal::{DollarsIdentity, LazyFromHeightLast},
};
impl Vecs {
pub fn import(version: Version, distribution: &distribution::Vecs) -> Option<Self> {
let supply_metrics = &distribution.utxo_cohorts.all.metrics.supply;
supply_metrics.total.dollars.as_ref().map(|d| {
Self(LazyFromHeightLast::from_lazy_binary_computed::<DollarsIdentity, _, _>(
"market_cap",
version,
d.height.boxed_clone(),
d,
))
})
}
}
@@ -1,4 +0,0 @@
mod import;
mod vecs;
pub use vecs::Vecs;
@@ -1,8 +0,0 @@
use brk_traversable::Traversable;
use brk_types::Dollars;
use derive_more::{Deref, DerefMut};
use crate::internal::LazyFromHeightLast;
#[derive(Clone, Deref, DerefMut, Traversable)]
pub struct Vecs(pub LazyFromHeightLast<Dollars>);
-3
View File
@@ -1,7 +1,4 @@
pub mod burned;
pub mod circulating;
pub mod inflation;
pub mod market_cap;
pub mod velocity;
mod compute;
+13 -13
View File
@@ -1,24 +1,24 @@
use brk_traversable::Traversable;
use vecdb::Database;
use brk_types::{DateIndex, Dollars, StoredF32};
use vecdb::{Database, LazyVecFrom2};
use super::{burned, circulating, inflation, market_cap, velocity};
use super::{burned, velocity};
use crate::internal::{
ComputedFromDateAverage, ComputedFromDateLast, LazyFromHeightLast, LazyValueFromHeightLast,
};
/// Supply metrics module
///
/// This module owns all supply-related metrics:
/// - circulating: Lazy references to distribution's actual circulating supply
/// - burned: Cumulative opreturn and unspendable supply
/// - inflation: Inflation rate derived from supply
/// - velocity: BTC and USD velocity metrics
/// - market_cap: Lazy references to supply in USD (circulating * price)
#[derive(Clone, Traversable)]
pub struct Vecs {
#[traversable(skip)]
pub(crate) db: Database,
pub circulating: circulating::Vecs,
pub circulating: LazyValueFromHeightLast,
pub burned: burned::Vecs,
pub inflation: inflation::Vecs,
pub inflation: ComputedFromDateAverage<StoredF32>,
pub velocity: velocity::Vecs,
pub market_cap: Option<market_cap::Vecs>,
pub market_cap: Option<LazyFromHeightLast<Dollars>>,
pub market_cap_growth_rate: ComputedFromDateLast<StoredF32>,
pub realized_cap_growth_rate: ComputedFromDateLast<StoredF32>,
pub cap_growth_rate_diff:
LazyVecFrom2<DateIndex, StoredF32, DateIndex, StoredF32, DateIndex, StoredF32>,
}
+27 -16
View File
@@ -1,7 +1,8 @@
use brk_error::Result;
use brk_types::{Bitcoin, CheckedSub, Close, Date, DateIndex, Dollars, Sats, StoredF32};
use vecdb::{
AnyStoredVec, AnyVec, EagerVec, Exit, GenericStoredVec, IterableVec, PcoVec, VecIndex, Version,
AnyStoredVec, AnyVec, EagerVec, Exit, GenericStoredVec, IterableVec, PcoVec, VecIndex, VecValue,
Version,
};
mod pricing;
@@ -295,37 +296,47 @@ where
}
pub trait ComputeDrawdown<I> {
fn compute_drawdown(
fn compute_drawdown<C, A>(
&mut self,
max_from: I,
close: &impl IterableVec<I, Close<Dollars>>,
ath: &impl IterableVec<I, Dollars>,
current: &impl IterableVec<I, C>,
ath: &impl IterableVec<I, A>,
exit: &Exit,
) -> Result<()>;
) -> Result<()>
where
C: VecValue,
A: VecValue,
f64: From<C> + From<A>;
}
impl<I> ComputeDrawdown<I> for EagerVec<PcoVec<I, StoredF32>>
where
I: VecIndex,
{
fn compute_drawdown(
fn compute_drawdown<C, A>(
&mut self,
max_from: I,
close: &impl IterableVec<I, Close<Dollars>>,
ath: &impl IterableVec<I, Dollars>,
current: &impl IterableVec<I, C>,
ath: &impl IterableVec<I, A>,
exit: &Exit,
) -> Result<()> {
) -> Result<()>
where
C: VecValue,
A: VecValue,
f64: From<C> + From<A>,
{
self.compute_transform2(
max_from,
current,
ath,
close,
|(i, ath, close, _)| {
if ath == Dollars::ZERO {
(i, StoredF32::default())
|(i, current, ath, _)| {
let ath_f64 = f64::from(ath);
let drawdown = if ath_f64 == 0.0 {
StoredF32::default()
} else {
let drawdown = StoredF32::from((*ath - **close) / *ath * -100.0);
(i, drawdown)
}
StoredF32::from((f64::from(current) - ath_f64) / ath_f64 * 100.0)
};
(i, drawdown)
},
exit,
)?;
+2
View File
@@ -13,6 +13,7 @@ bitcoincore-rpc = ["dep:bitcoincore-rpc"]
fjall = ["dep:fjall"]
jiff = ["dep:jiff"]
minreq = ["dep:minreq"]
pco = ["dep:pco"]
serde_json = ["dep:serde_json"]
tokio = ["dep:tokio"]
vecdb = ["dep:vecdb"]
@@ -23,6 +24,7 @@ bitcoincore-rpc = { workspace = true, optional = true }
fjall = { workspace = true, optional = true }
jiff = { workspace = true, optional = true }
minreq = { workspace = true, optional = true }
pco = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
thiserror = "2.0"
tokio = { workspace = true, optional = true }
+4
View File
@@ -58,6 +58,10 @@ pub enum Error {
#[error(transparent)]
BitcoinHexToArrayError(#[from] bitcoin::hex::HexToArrayError),
#[cfg(feature = "pco")]
#[error(transparent)]
Pco(#[from] pco::errors::PcoError),
#[cfg(feature = "serde_json")]
#[error(transparent)]
SerdeJSON(#[from] serde_json::Error),
-10
View File
@@ -78,8 +78,6 @@ impl<'a> BlockProcessor<'a> {
pub fn store_transaction_metadata(&mut self, txs: Vec<ComputedTx>) -> Result<()> {
let height = self.height;
let mut inserted = 0usize;
let mut skipped = 0usize;
for ct in txs {
if ct.prev_txindex_opt.is_none() {
@@ -88,9 +86,6 @@ impl<'a> BlockProcessor<'a> {
ct.txindex,
height,
);
inserted += 1;
} else {
skipped += 1;
}
self.vecs
@@ -123,11 +118,6 @@ impl<'a> BlockProcessor<'a> {
.checked_push(ct.txindex, StoredBool::from(ct.tx.is_explicitly_rbf()))?;
}
tracing::debug!(
"store_transaction_metadata: height={}, inserted={}, skipped={}",
height, inserted, skipped
);
Ok(())
}
}
@@ -1,3 +1,5 @@
use std::hash::{DefaultHasher, Hash, Hasher};
use brk_types::RecommendedFees;
use super::{fees, stats::{self, BlockStats}};
@@ -36,4 +38,14 @@ impl Snapshot {
fees,
}
}
/// Hash of the first projected block (the one about to be mined).
pub fn next_block_hash(&self) -> u64 {
let Some(block) = self.blocks.first() else {
return 0;
};
let mut hasher = DefaultHasher::new();
block.hash(&mut hasher);
hasher.finish()
}
}
+16 -1
View File
@@ -1,4 +1,5 @@
use std::{
hash::{DefaultHasher, Hash, Hasher},
sync::{
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
@@ -9,7 +10,7 @@ use std::{
use brk_error::Result;
use brk_rpc::Client;
use brk_types::{MempoolEntryInfo, MempoolInfo, TxWithHex, Txid, TxidPrefix};
use brk_types::{AddressBytes, MempoolEntryInfo, MempoolInfo, TxWithHex, Txid, TxidPrefix};
use derive_more::Deref;
use parking_lot::{RwLock, RwLockReadGuard};
use rustc_hash::FxHashMap;
@@ -87,6 +88,20 @@ impl MempoolInner {
self.snapshot.read().block_stats.clone()
}
pub fn next_block_hash(&self) -> u64 {
self.snapshot.read().next_block_hash()
}
pub fn address_hash(&self, address: &AddressBytes) -> u64 {
let addresses = self.addresses.read();
let Some((stats, _)) = addresses.get(address) else {
return 0;
};
let mut hasher = DefaultHasher::new();
stats.hash(&mut hasher);
hasher.finish()
}
pub fn get_txs(&self) -> RwLockReadGuard<'_, TxStore> {
self.txs.read()
}
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "brk_oracle"
description = "Pure on-chain BTC/USD price oracle algorithm"
version.workspace = true
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
[dependencies]
brk_types = { workspace = true }
[dev-dependencies]
brk_indexer = { workspace = true }
serde_json = { workspace = true }
vecdb = { workspace = true }
+103
View File
@@ -0,0 +1,103 @@
# brk_oracle
BTC/USD price oracle from on-chain Bitcoin data alone. No exchange feeds, no external APIs. Given an initial price estimate, tracks block by block from height 575,000 (May 2019) onward.
## The insight
When someone buys $100 of bitcoin at $50,000/BTC, the output is 200,000 sats. At $60,000 it would be 166,667 sats. Millions of round-dollar purchases happen every day at common amounts like $1, $5, $10, $20, $50, $100, $200, $500. Each amount creates its own spike in the histogram of transaction outputs, at a position that depends on the current price. As the price moves, all spikes shift together. The oracle finds those spikes and reads the price from their position.
## How it works
For each new block:
1. **Filter outputs.** Skip the coinbase transaction, then apply the configured filters: excluded script types, dust threshold, and round BTC exclusion.
2. **Map to bins.** Each output's satoshi value is placed into a log-scale histogram with 2,400 bins (200 per 10x): bin = round(log10(sats) * 200). Log-scale is key: if the price doubles, all spikes shift by 60 bins whether bitcoin goes from $1k to $2k or from $50k to $100k.
3. **Store in ring buffer.** The block histogram goes into a ring buffer of configurable depth. A single block is too sparse to get a clean signal, so the oracle accumulates several.
4. **Compute EMA.** The stored histograms are combined into a weighted average where recent blocks count more than older ones: weight = alpha * (1 - alpha)^age. Fully recomputed from the ring buffer each block.
5. **Score with stencil.** A 19-point stencil encodes where the spikes from round-dollar amounts ($1 through $10,000) should appear relative to each other. The oracle slides this stencil across the EMA histogram within a search window around the previous estimate. At each position, it reads the EMA value at each of the 19 expected spike locations, divides each by that offset's peak in the window, and sums them into a score. This gives every dollar amount, common or rare, an equal vote.
6. **Pick the best.** The position with the highest score is the new price estimate. Parabolic interpolation between neighbors refines it to fractional-bin precision.
The resulting bin converts to a dollar price: 10^(10 - bin/200). The search is bounded to prevent the stencil from matching at wrong price levels, so the oracle tracks incrementally block by block.
The oracle accepts three input formats: raw block data, an iterator of (sats, output type) pairs, or a pre-built histogram. Each call returns the current estimate as a fractional bin, convertible to cents or dollars. Daily candles can be built from the per-block prices.
The initial seed must be close to the real price at the starting height. The crate includes a PRICES constant with exchange prices for every height before 630,000 to derive a seed from.
## Config
All parameters are exposed via Config with sensible defaults:
- **alpha** (2/7): EMA decay rate, ~6-block span
- **window_size** (12): number of block histograms in the ring buffer
- **search_below / search_above** (9 / 11): how far to search around the previous estimate, in bins
- **min_sats** (1,000): minimum output value, filters dust
- **exclude_common_round_values** (true): exclude common round values (d × 10^n, d ∈ {1,2,3,5,6}) that create false stencil matches
- **excluded_output_types** (P2TR, P2WSH): script types dominated by protocol activity, not round-dollar purchases
## Inspiration
Inspired by [UTXOracle](https://utxo.live/oracle/) by [@SteveSimple](https://x.com/SteveSimple), which showed that the BTC/USD price can be derived from on-chain data alone. brk_oracle takes the same core insight (round-dollar detection via log-scale histogram) and redesigns the algorithm for per-block resolution and rolling operation.
### Differences from UTXOracle
| | brk_oracle | UTXOracle |
|---|---|---|
| Resolution | Per-block (~10 min) and daily candles | Per-day |
| Algorithm | Single-pass stencil scoring | Multi-step: rough stencil match, output-to-USD mapping, iterative median convergence |
| Operation | Rolling EMA over configurable window | Stateless, processes a full day from scratch |
| Stencil | 19 offsets with per-offset peak normalization | Gaussian smooth + empirically weighted spikes |
| Round BTC handling | Excludes outputs entirely | Smooths histogram bins by averaging neighbors |
| Output filtering | Script type, dust threshold, round BTC | 2-output txs only, input count limits, same-day exclusion, witness size limits |
| Validated from | Height 575,000 (May 2019) | December 2023 |
Both use 200 bins per 10x on a log scale.
## Accuracy
Tested over 361,245 blocks (heights 575,000 to 936,244) against exchange OHLC data. Error is measured per block as the distance from the oracle's estimate to the exchange high-low range at that height. If the oracle falls within the range, the error is zero.
### Per-block
| Metric | Value |
|--------|-------|
| Median error | 0.10% |
| 95th percentile | 0.55% |
| 99th percentile | 1.4% |
| 99.9th percentile | 4.4% |
| RMSE | 0.38% |
| Max error | 18.1% |
| Bias | +0.04 bins (essentially zero) |
| Blocks > 5% error | 237 (0.07%) |
| Blocks > 10% error | 22 (0.006%) |
| Blocks > 20% error | 0 |
### Daily candles
Oracle daily OHLC built from per-block prices vs exchange daily OHLC:
| | Median | RMSE | Max |
|-------|--------|------|-----|
| Open | 0.20% | 0.49% | 5.9% |
| High | 0.54% | 0.87% | 9.1% |
| Low | 0.48% | 1.31% | 19.7% |
| Close | 0.23% | 0.58% | 6.9% |
### By year
| Year | Blocks | Median | RMSE | Max | >5% | >10% | Price range |
|------|--------|--------|------|-----|-----|------|-------------|
| 2019 | 35,764 | 0.10% | 0.61% | 17.2% | 103 | 16 | $5,656$13,868 |
| 2020 | 53,102 | 0.10% | 0.48% | 18.2% | 85 | 15 | $3,858$29,322 |
| 2021 | 52,733 | 0.07% | 0.47% | 14.4% | 38 | 9 | $27,678$69,000 |
| 2022 | 53,230 | 0.07% | 0.32% | 6.8% | 10 | 0 | $15,460$48,240 |
| 2023 | 54,032 | 0.10% | 0.25% | 6.7% | 5 | 0 | $16,490$44,700 |
| 2024 | 53,367 | 0.11% | 0.31% | 9.7% | 16 | 0 | $38,555$108,298 |
| 2025 | 53,113 | 0.11% | 0.25% | 5.8% | 4 | 0 | $74,409$126,198 |
| 2026 | 5,904 | 0.11% | 0.27% | 3.3% | 0 | 0 | $60,000$97,900 |
Accuracy improves over time as on-chain transaction volume grows. Since 2022, zero blocks exceed 10% error. All worst-case errors occur during the fastest intraday price moves in 20192021.
@@ -0,0 +1,286 @@
//! Compare specific digit filter configurations across multiple start heights.
//!
//! Run with: cargo run -p brk_oracle --example compare_digits --release
use std::path::PathBuf;
use std::time::Instant;
use brk_indexer::Indexer;
use brk_oracle::{Config, NUM_BINS, Oracle, PRICES, START_HEIGHT, cents_to_bin, sats_to_bin};
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
use vecdb::{AnyVec, VecIndex, VecIterator};
const BINS_5PCT: f64 = 4.24;
const BINS_10PCT: f64 = 8.28;
const BINS_20PCT: f64 = 15.84;
fn bins_to_pct(bins: f64) -> f64 {
(10.0_f64.powf(bins / 200.0) - 1.0) * 100.0
}
fn seed_bin(start_height: usize) -> f64 {
let price: f64 = PRICES
.lines()
.nth(start_height - 1)
.expect("prices.txt too short")
.parse()
.expect("Failed to parse seed price");
cents_to_bin(price * 100.0)
}
fn leading_digit(sats: u64) -> u8 {
let log = (sats as f64).log10();
let magnitude = 10.0_f64.powf(log.floor());
let d = (sats as f64 / magnitude).round() as u8;
if d >= 10 { 1 } else { d }
}
fn is_round(sats: u64) -> bool {
let log = (sats as f64).log10();
let magnitude = 10.0_f64.powf(log.floor());
let leading = (sats as f64 / magnitude).round();
let round_val = leading * magnitude;
(sats as f64 - round_val).abs() <= round_val * 0.001
}
struct Stats {
total_sq_err: f64,
total_bias: f64,
max_err: f64,
total_blocks: u64,
gt_5pct: u64,
gt_10pct: u64,
gt_20pct: u64,
}
impl Stats {
fn new() -> Self {
Self {
total_sq_err: 0.0,
total_bias: 0.0,
max_err: 0.0,
total_blocks: 0,
gt_5pct: 0,
gt_10pct: 0,
gt_20pct: 0,
}
}
fn update(&mut self, err: f64) {
self.total_sq_err += err * err;
self.total_bias += err;
self.total_blocks += 1;
let abs_err = err.abs();
if abs_err > self.max_err {
self.max_err = abs_err;
}
if abs_err > BINS_5PCT {
self.gt_5pct += 1;
}
if abs_err > BINS_10PCT {
self.gt_10pct += 1;
}
if abs_err > BINS_20PCT {
self.gt_20pct += 1;
}
}
fn rmse_pct(&self) -> f64 {
bins_to_pct((self.total_sq_err / self.total_blocks as f64).sqrt())
}
fn max_pct(&self) -> f64 {
bins_to_pct(self.max_err)
}
fn bias(&self) -> f64 {
self.total_bias / self.total_blocks as f64
}
}
fn main() {
let t0 = Instant::now();
let data_dir = std::env::var("BRK_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap();
PathBuf::from(home).join(".brk")
});
let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer");
let total_heights = indexer.vecs.blocks.timestamp.len();
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let height_ohlc: Vec<[f64; 4]> = serde_json::from_str(
&std::fs::read_to_string(format!("{manifest_dir}/examples/height_price_ohlc.json"))
.expect("Failed to read height_price_ohlc.json"),
)
.expect("Failed to parse height OHLC");
let height_bands: Vec<(f64, f64)> = height_ohlc
.iter()
.map(|ohlc| {
let high = ohlc[1];
let low = ohlc[2];
if high > 0.0 && low > 0.0 {
(cents_to_bin(high * 100.0), cents_to_bin(low * 100.0))
} else {
(0.0, 0.0)
}
})
.collect();
// Configs to compare.
// 987654321
let masks: &[(u16, &str)] = &[
(0b0_0111_0111, "{1,2,3,5,6,7}"),
(0b0_0011_0111, "{1,2,3,5,6}"),
(0b0_0001_1111, "{1,2,3,4,5}"),
(0b0_0001_0111, "{1,2,3,5}"),
];
let start_heights: &[usize] = &[575_000, 600_000, 630_000];
// (mask_idx, start_idx) -> (Oracle, Stats)
let n = masks.len() * start_heights.len();
let mut oracles: Vec<Option<Oracle>> = (0..n).map(|_| None).collect();
let mut stats: Vec<Stats> = (0..n).map(|_| Stats::new()).collect();
let idx = |m: usize, s: usize| -> usize { m * start_heights.len() + s };
let total_txs = indexer.vecs.transactions.height.len();
let total_outputs = indexer.vecs.outputs.value.len();
let mut first_txindex_iter = indexer.vecs.transactions.first_txindex.into_iter();
let mut first_txoutindex_iter = indexer.vecs.transactions.first_txoutindex.into_iter();
let mut out_first_iter = indexer.vecs.outputs.first_txoutindex.into_iter();
let mut value_iter = indexer.vecs.outputs.value.into_iter();
let mut outputtype_iter = indexer.vecs.outputs.outputtype.into_iter();
let ref_config = Config::default();
let earliest_start = *start_heights.iter().min().unwrap();
for h in START_HEIGHT..total_heights {
let first_txindex: TxIndex = first_txindex_iter.get_at_unwrap(h);
let next_first_txindex = first_txindex_iter
.get_at(h + 1)
.unwrap_or(TxIndex::from(total_txs));
let out_start = if first_txindex.to_usize() + 1 < next_first_txindex.to_usize() {
first_txoutindex_iter
.get_at_unwrap(first_txindex.to_usize() + 1)
.to_usize()
} else {
out_first_iter
.get_at(h + 1)
.unwrap_or(TxOutIndex::from(total_outputs))
.to_usize()
};
let out_end = out_first_iter
.get_at(h + 1)
.unwrap_or(TxOutIndex::from(total_outputs))
.to_usize();
if h < earliest_start {
continue;
}
// Build full histogram and per-digit histograms.
let mut full_hist = [0u32; NUM_BINS];
let mut digit_hist = [[0u32; NUM_BINS]; 9];
for i in out_start..out_end {
let sats: Sats = value_iter.get_at_unwrap(i);
let output_type: OutputType = outputtype_iter.get_at_unwrap(i);
if ref_config.excluded_output_types.contains(&output_type) {
continue;
}
if *sats < ref_config.min_sats {
continue;
}
if let Some(bin) = sats_to_bin(sats) {
full_hist[bin] += 1;
if is_round(*sats) {
let d = leading_digit(*sats);
if (1..=9).contains(&d) {
digit_hist[(d - 1) as usize][bin] += 1;
}
}
}
}
// Feed each (mask, start_height) combo.
for (mi, &(mask, _)) in masks.iter().enumerate() {
// Build filtered histogram for this mask.
let mut hist = full_hist;
(0..9usize).for_each(|d| {
if mask & (1 << d) != 0 {
for bin in 0..NUM_BINS {
hist[bin] -= digit_hist[d][bin];
}
}
});
for (si, &sh) in start_heights.iter().enumerate() {
if h < sh {
continue;
}
let i = idx(mi, si);
if oracles[i].is_none() {
oracles[i] = Some(Oracle::new(
seed_bin(sh),
Config {
exclude_common_round_values: false,
..Default::default()
},
));
}
let ref_bin = oracles[i].as_mut().unwrap().process_histogram(&hist);
if h < height_bands.len() {
let (high_bin, low_bin) = height_bands[h];
if high_bin > 0.0 && low_bin > 0.0 {
let err = if ref_bin < high_bin {
ref_bin - high_bin
} else if ref_bin > low_bin {
ref_bin - low_bin
} else {
0.0
};
stats[i].update(err);
}
}
}
}
}
// Print results grouped by start height.
for (si, &sh) in start_heights.iter().enumerate() {
println!();
println!("@ {}k:", sh / 1000);
println!(
" {:<16} {:>8} {:>10} {:>10} {:>6} {:>6} {:>6} {:>8}",
"Digits", "Blocks", "RMSE%", "Max%", ">5%", ">10%", ">20%", "Bias"
);
println!(" {}", "-".repeat(72));
for (mi, &(_, label)) in masks.iter().enumerate() {
let s = &stats[idx(mi, si)];
println!(
" {:<16} {:>8} {:>7.3}% {:>7.1}% {:>6} {:>6} {:>6} {:>+8.2}",
label,
s.total_blocks,
s.rmse_pct(),
s.max_pct(),
s.gt_5pct,
s.gt_10pct,
s.gt_20pct,
s.bias()
);
}
}
println!("\nDone in {:.1}s", t0.elapsed().as_secs_f64());
}
File diff suppressed because it is too large Load Diff
+207
View File
@@ -0,0 +1,207 @@
//! Verify oracle determinism: oracles started from different heights converge
//! to identical ref_bin values after the ring buffer fills.
//!
//! Creates a reference oracle at height 575k and test oracles every 1000 blocks
//! up to 630k. After window_size blocks, each test oracle should produce the
//! same ref_bin as the reference, proving the truncated EMA provides
//! start-point independence.
//!
//! Run with: cargo run -p brk_oracle --example determinism --release
use std::path::PathBuf;
use brk_indexer::Indexer;
use brk_oracle::{Config, NUM_BINS, Oracle, PRICES, START_HEIGHT, cents_to_bin, sats_to_bin};
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
use vecdb::{AnyVec, VecIndex, VecIterator};
fn seed_bin(height: usize) -> f64 {
let price: f64 = PRICES
.lines()
.nth(height - 1)
.expect("prices.txt too short")
.parse()
.expect("Failed to parse seed price");
cents_to_bin(price * 100.0)
}
struct TestRun {
start_height: usize,
oracle: Option<Oracle>,
converged_at: Option<usize>,
diverged_after: bool,
}
fn main() {
let data_dir = std::env::var("BRK_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap();
PathBuf::from(home).join(".brk")
});
let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer");
let total_heights = indexer.vecs.blocks.timestamp.len();
let config = Config::default();
let window_size = config.window_size;
let total_txs = indexer.vecs.transactions.height.len();
let total_outputs = indexer.vecs.outputs.value.len();
let mut first_txindex_iter = indexer.vecs.transactions.first_txindex.into_iter();
let mut first_txoutindex_iter = indexer.vecs.transactions.first_txoutindex.into_iter();
let mut out_first_iter = indexer.vecs.outputs.first_txoutindex.into_iter();
let mut value_iter = indexer.vecs.outputs.value.into_iter();
let mut outputtype_iter = indexer.vecs.outputs.outputtype.into_iter();
let ref_config = Config::default();
// Reference oracle at 575k.
let ref_start = START_HEIGHT;
let mut ref_oracle = Oracle::new(seed_bin(ref_start), Config::default());
// Test oracles every 1000 blocks from 576k to 630k.
let mut runs: Vec<TestRun> = (576_000..=630_000)
.step_by(1000)
.map(|h| TestRun {
start_height: h,
oracle: None,
converged_at: None,
diverged_after: false,
})
.collect();
let last_start = runs.last().map(|r| r.start_height).unwrap_or(ref_start);
// Process enough blocks for all oracles to converge + verification margin.
let end_height = (last_start + window_size + 100).min(total_heights);
for h in START_HEIGHT..end_height {
let first_txindex: TxIndex = first_txindex_iter.get_at_unwrap(h);
let next_first_txindex = first_txindex_iter
.get_at(h + 1)
.unwrap_or(TxIndex::from(total_txs));
let out_start = if first_txindex.to_usize() + 1 < next_first_txindex.to_usize() {
first_txoutindex_iter
.get_at_unwrap(first_txindex.to_usize() + 1)
.to_usize()
} else {
out_first_iter
.get_at(h + 1)
.unwrap_or(TxOutIndex::from(total_outputs))
.to_usize()
};
let out_end = out_first_iter
.get_at(h + 1)
.unwrap_or(TxOutIndex::from(total_outputs))
.to_usize();
let mut hist = [0u32; NUM_BINS];
for i in out_start..out_end {
let sats: Sats = value_iter.get_at_unwrap(i);
let output_type: OutputType = outputtype_iter.get_at_unwrap(i);
if ref_config.excluded_output_types.contains(&output_type) {
continue;
}
if *sats < ref_config.min_sats
|| (ref_config.exclude_common_round_values && sats.is_common_round_value())
{
continue;
}
if let Some(bin) = sats_to_bin(sats) {
hist[bin] += 1;
}
}
let ref_bin = ref_oracle.process_histogram(&hist);
for run in &mut runs {
if h < run.start_height {
continue;
}
if run.oracle.is_none() {
run.oracle = Some(Oracle::new(seed_bin(run.start_height), Config::default()));
}
let test_bin = run.oracle.as_mut().unwrap().process_histogram(&hist);
if run.converged_at.is_some() {
if test_bin != ref_bin {
run.diverged_after = true;
}
} else if test_bin == ref_bin {
run.converged_at = Some(h);
}
}
}
// Print results.
println!();
println!(
"{:<12} {:>16} {:>8}",
"Start", "Converged at", "Blocks"
);
println!("{}", "-".repeat(40));
let mut max_blocks = 0usize;
let mut failed = Vec::new();
let mut diverged = Vec::new();
for run in &runs {
if let Some(converged) = run.converged_at {
let blocks = converged - run.start_height;
if blocks > max_blocks {
max_blocks = blocks;
}
println!(
"{:<12} {:>16} {:>8}",
run.start_height, converged, blocks
);
if run.diverged_after {
diverged.push(run.start_height);
}
} else {
println!("{:<12} {:>16} {:>8}", run.start_height, "NEVER", "-");
failed.push(run.start_height);
}
}
println!();
println!(
"{}/{} converged, max {} blocks to converge (window_size={})",
runs.len() - failed.len(),
runs.len(),
max_blocks,
window_size,
);
if !diverged.is_empty() {
println!("DIVERGED after convergence: {:?}", diverged);
}
if !failed.is_empty() {
println!("NEVER converged: {:?}", failed);
}
// Assertions.
assert!(
failed.is_empty(),
"{} oracles never converged: {:?}",
failed.len(),
failed
);
assert!(
diverged.is_empty(),
"{} oracles diverged after convergence: {:?}",
diverged.len(),
diverged
);
assert!(
max_blocks <= window_size * 2,
"Convergence took {} blocks, expected <= {} (2 * window_size)",
max_blocks,
window_size * 2
);
println!();
println!("All assertions passed!");
}
File diff suppressed because one or more lines are too long
+461
View File
@@ -0,0 +1,461 @@
//! Generate detailed oracle accuracy report for README / documentation.
//!
//! Run with: cargo run -p brk_oracle --example report --release
use std::path::PathBuf;
use brk_indexer::Indexer;
use brk_oracle::{
Config, NUM_BINS, Oracle, PRICES, START_HEIGHT, bin_to_cents, cents_to_bin, sats_to_bin,
};
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
use vecdb::{AnyVec, VecIndex, VecIterator};
/// DateIndex 1 = Jan 9, 2009 (block 1). For dates after genesis week:
/// dateindex = floor(timestamp / 86400) - 14252.
const GENESIS_DAY: u32 = 14252;
const BINS_5PCT: f64 = 4.24;
const BINS_10PCT: f64 = 8.28;
const BINS_20PCT: f64 = 15.84;
fn bins_to_pct(bins: f64) -> f64 {
(10.0_f64.powf(bins / 200.0) - 1.0) * 100.0
}
fn timestamp_to_year(ts: u32) -> u16 {
let years_since_1970 = ts as f64 / 31557600.0;
(1970.0 + years_since_1970) as u16
}
struct YearStats {
year: u16,
total_sq_err: f64,
max_err: f64,
total_blocks: u64,
gt_5pct: u64,
gt_10pct: u64,
gt_20pct: u64,
min_price: f64,
max_price: f64,
errors: Vec<f64>,
}
impl YearStats {
fn new(year: u16) -> Self {
Self {
year,
total_sq_err: 0.0,
max_err: 0.0,
total_blocks: 0,
gt_5pct: 0,
gt_10pct: 0,
gt_20pct: 0,
min_price: f64::MAX,
max_price: 0.0,
errors: Vec::new(),
}
}
fn update(&mut self, err: f64, exchange_high: f64, exchange_low: f64) {
let abs_err = err.abs();
self.total_sq_err += err * err;
self.total_blocks += 1;
self.errors.push(bins_to_pct(abs_err));
if abs_err > self.max_err {
self.max_err = abs_err;
}
if abs_err > BINS_5PCT {
self.gt_5pct += 1;
}
if abs_err > BINS_10PCT {
self.gt_10pct += 1;
}
if abs_err > BINS_20PCT {
self.gt_20pct += 1;
}
if exchange_high > self.max_price {
self.max_price = exchange_high;
}
if exchange_low > 0.0 && exchange_low < self.min_price {
self.min_price = exchange_low;
}
}
fn rmse_pct(&self) -> f64 {
bins_to_pct((self.total_sq_err / self.total_blocks as f64).sqrt())
}
fn max_pct(&self) -> f64 {
bins_to_pct(self.max_err)
}
fn median_pct(&mut self) -> f64 {
self.errors.sort_by(|a, b| a.partial_cmp(b).unwrap());
let n = self.errors.len();
if n == 0 { 0.0 } else { self.errors[n / 2] }
}
fn percentile(&self, p: f64) -> f64 {
let n = self.errors.len();
if n == 0 {
return 0.0;
}
let idx = ((p / 100.0) * (n - 1) as f64).round() as usize;
self.errors[idx.min(n - 1)]
}
}
/// Oracle OHLC for a single day, built from per-block prices.
struct DayCandle {
dateindex: usize,
open: f64,
high: f64,
low: f64,
close: f64,
}
struct BlockError {
height: usize,
oracle_price: f64,
exchange_low: f64,
exchange_high: f64,
error_pct: f64,
}
fn main() {
let data_dir = std::env::var("BRK_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap();
PathBuf::from(home).join(".brk")
});
let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer");
let total_heights = indexer.vecs.blocks.timestamp.len();
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let height_ohlc: Vec<[f64; 4]> = serde_json::from_str(
&std::fs::read_to_string(format!("{manifest_dir}/examples/height_price_ohlc.json"))
.expect("Failed to read height_price_ohlc.json"),
)
.expect("Failed to parse height OHLC");
let daily_ohlc: Vec<[f64; 4]> = serde_json::from_str(
&std::fs::read_to_string(format!("{manifest_dir}/examples/date_price_ohlc.json"))
.expect("Failed to read date_price_ohlc.json"),
)
.expect("Failed to parse daily OHLC");
let height_bands: Vec<(f64, f64)> = height_ohlc
.iter()
.map(|ohlc| {
let high = ohlc[1];
let low = ohlc[2];
if high > 0.0 && low > 0.0 {
(cents_to_bin(high * 100.0), cents_to_bin(low * 100.0))
} else {
(0.0, 0.0)
}
})
.collect();
// Read block timestamps for year + dateindex mapping.
let mut timestamp_iter = indexer.vecs.blocks.timestamp.into_iter();
let mut height_years: Vec<u16> = Vec::with_capacity(total_heights);
let mut height_dateindexes: Vec<usize> = Vec::with_capacity(total_heights);
for h in 0..total_heights {
let ts: brk_types::Timestamp = timestamp_iter.get_at_unwrap(h);
let ts_u32 = *ts as u32;
height_years.push(timestamp_to_year(ts_u32));
height_dateindexes.push((ts_u32 / 86400).saturating_sub(GENESIS_DAY) as usize);
}
let start_price: f64 = PRICES
.lines()
.nth(START_HEIGHT - 1)
.expect("prices.txt too short")
.parse()
.expect("Failed to parse seed price");
let config = Config::default();
let mut oracle = Oracle::new(cents_to_bin(start_price * 100.0), config);
let total_txs = indexer.vecs.transactions.height.len();
let total_outputs = indexer.vecs.outputs.value.len();
let mut first_txindex_iter = indexer.vecs.transactions.first_txindex.into_iter();
let mut first_txoutindex_iter = indexer.vecs.transactions.first_txoutindex.into_iter();
let mut out_first_iter = indexer.vecs.outputs.first_txoutindex.into_iter();
let mut value_iter = indexer.vecs.outputs.value.into_iter();
let mut outputtype_iter = indexer.vecs.outputs.outputtype.into_iter();
let ref_config = Config::default();
let mut year_stats: Vec<YearStats> = Vec::new();
let mut overall = YearStats::new(0);
let mut worst_blocks: Vec<BlockError> = Vec::new();
let mut total_bias = 0.0f64;
// Track oracle daily candles.
let mut oracle_candles: Vec<DayCandle> = Vec::new();
let mut current_di: Option<usize> = None;
for h in START_HEIGHT..total_heights {
let first_txindex: TxIndex = first_txindex_iter.get_at_unwrap(h);
let next_first_txindex = first_txindex_iter
.get_at(h + 1)
.unwrap_or(TxIndex::from(total_txs));
let out_start = if first_txindex.to_usize() + 1 < next_first_txindex.to_usize() {
first_txoutindex_iter
.get_at_unwrap(first_txindex.to_usize() + 1)
.to_usize()
} else {
out_first_iter
.get_at(h + 1)
.unwrap_or(TxOutIndex::from(total_outputs))
.to_usize()
};
let out_end = out_first_iter
.get_at(h + 1)
.unwrap_or(TxOutIndex::from(total_outputs))
.to_usize();
let mut hist = [0u32; NUM_BINS];
for i in out_start..out_end {
let sats: Sats = value_iter.get_at_unwrap(i);
let output_type: OutputType = outputtype_iter.get_at_unwrap(i);
if ref_config.excluded_output_types.contains(&output_type) {
continue;
}
if *sats < ref_config.min_sats
|| (ref_config.exclude_common_round_values && sats.is_common_round_value())
{
continue;
}
if let Some(bin) = sats_to_bin(sats) {
hist[bin] += 1;
}
}
let ref_bin = oracle.process_histogram(&hist);
let oracle_price = bin_to_cents(ref_bin) as f64 / 100.0;
// Build oracle daily candle.
let di = height_dateindexes[h];
if current_di != Some(di) {
current_di = Some(di);
oracle_candles.push(DayCandle {
dateindex: di,
open: oracle_price,
high: oracle_price,
low: oracle_price,
close: oracle_price,
});
} else {
let candle = oracle_candles.last_mut().unwrap();
if oracle_price > candle.high {
candle.high = oracle_price;
}
if oracle_price < candle.low {
candle.low = oracle_price;
}
candle.close = oracle_price;
}
// Per-block error stats.
if h < height_bands.len() {
let (high_bin, low_bin) = height_bands[h];
if high_bin > 0.0 && low_bin > 0.0 {
let err = if ref_bin < high_bin {
ref_bin - high_bin
} else if ref_bin > low_bin {
ref_bin - low_bin
} else {
0.0
};
let exchange_high = height_ohlc[h][1];
let exchange_low = height_ohlc[h][2];
overall.update(err, exchange_high, exchange_low);
total_bias += err;
let year = height_years[h];
if year_stats.is_empty() || year_stats.last().unwrap().year != year {
year_stats.push(YearStats::new(year));
}
year_stats
.last_mut()
.unwrap()
.update(err, exchange_high, exchange_low);
if err.abs() > BINS_5PCT {
worst_blocks.push(BlockError {
height: h,
oracle_price,
exchange_low,
exchange_high,
error_pct: if err < 0.0 {
-bins_to_pct(err.abs())
} else {
bins_to_pct(err.abs())
},
});
}
}
}
}
worst_blocks.sort_by(|a, b| b.error_pct.abs().partial_cmp(&a.error_pct.abs()).unwrap());
overall.errors.sort_by(|a, b| a.partial_cmp(b).unwrap());
// Daily candle comparison: oracle OHLC vs exchange OHLC.
let mut daily_open_errors: Vec<f64> = Vec::new();
let mut daily_high_errors: Vec<f64> = Vec::new();
let mut daily_low_errors: Vec<f64> = Vec::new();
let mut daily_close_errors: Vec<f64> = Vec::new();
let mut daily_days = 0u64;
for candle in &oracle_candles {
let di = candle.dateindex;
if di >= daily_ohlc.len() {
continue;
}
let ex = &daily_ohlc[di];
if ex[0] <= 0.0 || ex[3] <= 0.0 {
continue;
}
let ex_open = ex[0];
let ex_high = ex[1];
let ex_low = ex[2];
let ex_close = ex[3];
// Error as percentage: (oracle - exchange) / exchange * 100
daily_open_errors.push((candle.open - ex_open) / ex_open * 100.0);
daily_high_errors.push((candle.high - ex_high) / ex_high * 100.0);
daily_low_errors.push((candle.low - ex_low) / ex_low * 100.0);
daily_close_errors.push((candle.close - ex_close) / ex_close * 100.0);
daily_days += 1;
}
fn daily_stats(errors: &mut [f64]) -> (f64, f64, f64) {
let n = errors.len() as f64;
let rmse = (errors.iter().map(|e| e * e).sum::<f64>() / n).sqrt();
errors.sort_by(|a, b| a.abs().partial_cmp(&b.abs()).unwrap());
let max = errors.last().map(|e| e.abs()).unwrap_or(0.0);
let median = errors[errors.len() / 2].abs();
(median, rmse, max)
}
let (open_med, open_rmse, open_max) = daily_stats(&mut daily_open_errors);
let (high_med, high_rmse, high_max) = daily_stats(&mut daily_high_errors);
let (low_med, low_rmse, low_max) = daily_stats(&mut daily_low_errors);
let (close_med, close_rmse, close_max) = daily_stats(&mut daily_close_errors);
// Print report.
println!();
println!(" brk_oracle accuracy report");
println!(" ══════════════════════════");
println!();
println!(" Config: w12, alpha=2/7, search -9/+11, noisy/dust/round-btc filtered");
println!(
" Test range: height {} .. {} ({} blocks)",
START_HEIGHT,
total_heights - 1,
overall.total_blocks
);
println!(
" Price range: ${:.0} .. ${:.0}",
overall.min_price, overall.max_price
);
println!();
println!(" Per-block accuracy (vs per-height exchange OHLC):");
println!(" Median: {:.3}%", overall.percentile(50.0));
println!(" 95th pct: {:.3}%", overall.percentile(95.0));
println!(" 99th pct: {:.3}%", overall.percentile(99.0));
println!(" 99.9th pct: {:.3}%", overall.percentile(99.9));
println!(" RMSE: {:.3}%", overall.rmse_pct());
println!(" Max: {:.1}%", overall.max_pct());
println!(
" Bias: {:+.2} bins",
total_bias / overall.total_blocks as f64
);
println!(
" > 5%: {} blocks ({:.3}%)",
overall.gt_5pct,
overall.gt_5pct as f64 / overall.total_blocks as f64 * 100.0
);
println!(" > 10%: {} blocks", overall.gt_10pct);
println!(" > 20%: {} blocks", overall.gt_20pct);
println!();
println!(
" Daily candle accuracy ({} days, vs exchange daily OHLC):",
daily_days
);
println!(
" {:>8} {:>10} {:>10} {:>10}",
"", "Median", "RMSE", "Max"
);
println!(
" {:>8} {:>9.2}% {:>9.2}% {:>9.1}%",
"Open", open_med, open_rmse, open_max
);
println!(
" {:>8} {:>9.2}% {:>9.2}% {:>9.1}%",
"High", high_med, high_rmse, high_max
);
println!(
" {:>8} {:>9.2}% {:>9.2}% {:>9.1}%",
"Low", low_med, low_rmse, low_max
);
println!(
" {:>8} {:>9.2}% {:>9.2}% {:>9.1}%",
"Close", close_med, close_rmse, close_max
);
println!();
println!(" By year:");
println!(
" {:<6} {:>7} {:>9} {:>9} {:>9} {:>6} {:>5} {:>5} {:>14}",
"Year", "Blocks", "Median", "RMSE", "Max", ">5%", ">10%", ">20%", "Price range"
);
println!(" {}", "-".repeat(80));
for ys in &mut year_stats {
let median = ys.median_pct();
println!(
" {:<6} {:>7} {:>8.3}% {:>8.3}% {:>8.1}% {:>6} {:>5} {:>5} ${:.0}..${:.0}",
ys.year,
ys.total_blocks,
median,
ys.rmse_pct(),
ys.max_pct(),
ys.gt_5pct,
ys.gt_10pct,
ys.gt_20pct,
ys.min_price,
ys.max_price,
);
}
if !worst_blocks.is_empty() {
println!();
println!(" Worst blocks:");
let show = worst_blocks.len().min(10);
for wb in &worst_blocks[..show] {
let dir = if wb.error_pct < 0.0 { "above" } else { "below" };
println!(
" height {:>7}: oracle ${:>9.0}, exchange ${:.0}..${:.0} ({:+.1}%, {})",
wb.height, wb.oracle_price, wb.exchange_low, wb.exchange_high, wb.error_pct, dir
);
}
if worst_blocks.len() > show {
println!(" ... and {} more", worst_blocks.len() - show);
}
}
println!();
}
+407
View File
@@ -0,0 +1,407 @@
//! Sweep round-value digit filter to find optimal configuration.
//!
//! Tests all 512 subsets of leading digits {1,...,9} to find which
//! digits to filter out for best oracle accuracy.
//!
//! Phase 1: single pass over indexer, precompute per-block histograms.
//! Phase 2: run 512 configs in parallel across CPU cores.
//!
//! Run with: cargo run -p brk_oracle --example sweep_digits --release
use std::path::PathBuf;
use std::time::Instant;
use brk_indexer::Indexer;
use brk_oracle::{Config, NUM_BINS, Oracle, PRICES, START_HEIGHT, cents_to_bin, sats_to_bin};
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
use vecdb::{AnyVec, VecIndex, VecIterator};
const BINS_5PCT: f64 = 4.24;
const BINS_10PCT: f64 = 8.28;
const BINS_20PCT: f64 = 15.84;
fn bins_to_pct(bins: f64) -> f64 {
(10.0_f64.powf(bins / 200.0) - 1.0) * 100.0
}
fn seed_bin(start_height: usize) -> f64 {
let price: f64 = PRICES
.lines()
.nth(start_height - 1)
.expect("prices.txt too short")
.parse()
.expect("Failed to parse seed price");
cents_to_bin(price * 100.0)
}
fn leading_digit(sats: u64) -> u8 {
let log = (sats as f64).log10();
let magnitude = 10.0_f64.powf(log.floor());
let d = (sats as f64 / magnitude).round() as u8;
if d >= 10 { 1 } else { d }
}
fn is_round(sats: u64) -> bool {
let log = (sats as f64).log10();
let magnitude = 10.0_f64.powf(log.floor());
let leading = (sats as f64 / magnitude).round();
let round_val = leading * magnitude;
(sats as f64 - round_val).abs() <= round_val * 0.001
}
fn mask_label(mask: u16) -> String {
let digits: String = (1..=9u8)
.filter(|&d| mask & (1 << (d - 1)) != 0)
.map(|d| char::from_digit(d as u32, 10).unwrap())
.collect();
if digits.is_empty() {
"none".to_string()
} else {
digits
}
}
struct Stats {
total_sq_err: f64,
total_bias: f64,
max_err: f64,
total_blocks: u64,
gt_5pct: u64,
gt_10pct: u64,
gt_20pct: u64,
}
impl Stats {
fn new() -> Self {
Self {
total_sq_err: 0.0,
total_bias: 0.0,
max_err: 0.0,
total_blocks: 0,
gt_5pct: 0,
gt_10pct: 0,
gt_20pct: 0,
}
}
fn update(&mut self, err: f64) {
self.total_sq_err += err * err;
self.total_bias += err;
self.total_blocks += 1;
let abs_err = err.abs();
if abs_err > self.max_err {
self.max_err = abs_err;
}
if abs_err > BINS_5PCT {
self.gt_5pct += 1;
}
if abs_err > BINS_10PCT {
self.gt_10pct += 1;
}
if abs_err > BINS_20PCT {
self.gt_20pct += 1;
}
}
fn rmse_pct(&self) -> f64 {
bins_to_pct((self.total_sq_err / self.total_blocks as f64).sqrt())
}
fn max_pct(&self) -> f64 {
bins_to_pct(self.max_err)
}
fn bias(&self) -> f64 {
self.total_bias / self.total_blocks as f64
}
}
struct BlockData {
full_hist: Box<[u32; NUM_BINS]>,
/// (bin_index, leading_digit) for outputs that are round values.
round_outputs: Vec<(u16, u8)>,
high_bin: f64,
low_bin: f64,
}
fn main() {
let t0 = Instant::now();
let data_dir = std::env::var("BRK_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap();
PathBuf::from(home).join(".brk")
});
let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer");
let total_heights = indexer.vecs.blocks.timestamp.len();
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let height_ohlc: Vec<[f64; 4]> = serde_json::from_str(
&std::fs::read_to_string(format!("{manifest_dir}/examples/height_price_ohlc.json"))
.expect("Failed to read height_price_ohlc.json"),
)
.expect("Failed to parse height OHLC");
let height_bands: Vec<(f64, f64)> = height_ohlc
.iter()
.map(|ohlc| {
let high = ohlc[1];
let low = ohlc[2];
if high > 0.0 && low > 0.0 {
(cents_to_bin(high * 100.0), cents_to_bin(low * 100.0))
} else {
(0.0, 0.0)
}
})
.collect();
let sweep_start: usize = 575_000;
// Phase 1: precompute per-block data in a single pass over the indexer.
eprintln!("Phase 1: precomputing block data...");
let total_txs = indexer.vecs.transactions.height.len();
let total_outputs = indexer.vecs.outputs.value.len();
let mut first_txindex_iter = indexer.vecs.transactions.first_txindex.into_iter();
let mut first_txoutindex_iter = indexer.vecs.transactions.first_txoutindex.into_iter();
let mut out_first_iter = indexer.vecs.outputs.first_txoutindex.into_iter();
let mut value_iter = indexer.vecs.outputs.value.into_iter();
let mut outputtype_iter = indexer.vecs.outputs.outputtype.into_iter();
let ref_config = Config::default();
let total_blocks = total_heights - sweep_start;
let mut blocks: Vec<BlockData> = Vec::with_capacity(total_blocks);
for h in START_HEIGHT..total_heights {
let first_txindex: TxIndex = first_txindex_iter.get_at_unwrap(h);
let next_first_txindex = first_txindex_iter
.get_at(h + 1)
.unwrap_or(TxIndex::from(total_txs));
let out_start = if first_txindex.to_usize() + 1 < next_first_txindex.to_usize() {
first_txoutindex_iter
.get_at_unwrap(first_txindex.to_usize() + 1)
.to_usize()
} else {
out_first_iter
.get_at(h + 1)
.unwrap_or(TxOutIndex::from(total_outputs))
.to_usize()
};
let out_end = out_first_iter
.get_at(h + 1)
.unwrap_or(TxOutIndex::from(total_outputs))
.to_usize();
if h < sweep_start {
continue;
}
let mut full_hist = Box::new([0u32; NUM_BINS]);
let mut round_outputs = Vec::new();
for i in out_start..out_end {
let sats: Sats = value_iter.get_at_unwrap(i);
let output_type: OutputType = outputtype_iter.get_at_unwrap(i);
if ref_config.excluded_output_types.contains(&output_type) {
continue;
}
if *sats < ref_config.min_sats {
continue;
}
if let Some(bin) = sats_to_bin(sats) {
full_hist[bin] += 1;
if is_round(*sats) {
let d = leading_digit(*sats);
if (1..=9).contains(&d) {
round_outputs.push((bin as u16, d));
}
}
}
}
let (high_bin, low_bin) = if h < height_bands.len() {
height_bands[h]
} else {
(0.0, 0.0)
};
blocks.push(BlockData {
full_hist,
round_outputs,
high_bin,
low_bin,
});
if (h - sweep_start).is_multiple_of(50_000) {
eprint!(
"\r {}/{} ({:.0}%)",
h - sweep_start,
total_blocks,
(h - sweep_start) as f64 / total_blocks as f64 * 100.0
);
}
}
let mem_hists = blocks.len() * std::mem::size_of::<[u32; NUM_BINS]>();
let mem_rounds: usize = blocks.iter().map(|b| b.round_outputs.len() * 3).sum();
eprintln!(
"\r {} blocks precomputed ({:.1} GB hists + {:.0} MB rounds) in {:.1}s",
blocks.len(),
mem_hists as f64 / 1e9,
mem_rounds as f64 / 1e6,
t0.elapsed().as_secs_f64()
);
// Phase 2: sweep digit masks in parallel.
// Always filter digit 1 (powers of 10), sweep digits 2-9.
let base_mask: u16 = 1 << 0; // digit 1 always on
let num_masks: usize = 256; // 2^8 subsets of {2,...,9}
let num_threads = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(8);
eprintln!(
"Phase 2: sweeping {} masks across {} threads...",
num_masks, num_threads
);
let t1 = Instant::now();
let blocks = &blocks; // shared reference for threads
let all_results: Vec<(u16, Stats)> = std::thread::scope(|s| {
let masks_per_thread = num_masks.div_ceil(num_threads);
let handles: Vec<_> = (0..num_threads)
.map(|t| {
s.spawn(move || {
let mask_start = t * masks_per_thread;
let mask_end = ((t + 1) * masks_per_thread).min(num_masks);
let mut results = Vec::with_capacity(mask_end - mask_start);
for idx in mask_start..mask_end {
// Shift idx bits into positions 1-8 (digits 2-9) and add base_mask (digit 1).
let mask = base_mask | ((idx as u16) << 1);
let mut oracle = Oracle::new(
seed_bin(sweep_start),
Config {
exclude_common_round_values: false,
..Default::default()
},
);
let mut stats = Stats::new();
for bd in blocks.iter() {
let mut hist = *bd.full_hist;
for &(bin, digit) in &bd.round_outputs {
if mask & (1 << (digit - 1)) != 0 {
hist[bin as usize] -= 1;
}
}
let ref_bin = oracle.process_histogram(&hist);
if bd.high_bin > 0.0 && bd.low_bin > 0.0 {
let err = if ref_bin < bd.high_bin {
ref_bin - bd.high_bin
} else if ref_bin > bd.low_bin {
ref_bin - bd.low_bin
} else {
0.0
};
stats.update(err);
}
}
results.push((mask, stats));
}
results
})
})
.collect();
handles
.into_iter()
.flat_map(|h| h.join().unwrap())
.collect()
});
eprintln!(" Done in {:.1}s.", t1.elapsed().as_secs_f64());
// Sort by RMSE.
let mut results: Vec<&(u16, Stats)> = all_results.iter().collect();
results.sort_by(|a, b| a.1.rmse_pct().partial_cmp(&b.1.rmse_pct()).unwrap());
// Print top 20.
println!();
println!("Top 20 (by RMSE):");
println!(
"{:>4} {:>12} {:>10} {:>10} {:>6} {:>6} {:>6} {:>8}",
"#", "Digits", "RMSE%", "Max%", ">5%", ">10%", ">20%", "Bias"
);
println!("{}", "-".repeat(70));
for (rank, (mask, s)) in results.iter().take(20).enumerate() {
println!(
"{:>4} {:>12} {:>8.3}% {:>8.1}% {:>6} {:>6} {:>6} {:>+8.2}",
rank + 1,
mask_label(*mask),
s.rmse_pct(),
s.max_pct(),
s.gt_5pct,
s.gt_10pct,
s.gt_20pct,
s.bias()
);
}
// Print bottom 5.
println!();
println!("Bottom 5 (worst):");
println!(
"{:>4} {:>12} {:>10} {:>10} {:>6} {:>6} {:>6} {:>8}",
"#", "Digits", "RMSE%", "Max%", ">5%", ">10%", ">20%", "Bias"
);
println!("{}", "-".repeat(70));
for (mask, s) in results.iter().rev().take(5) {
println!(
"{:>4} {:>12} {:>8.3}% {:>8.1}% {:>6} {:>6} {:>6} {:>+8.2}",
"",
mask_label(*mask),
s.rmse_pct(),
s.max_pct(),
s.gt_5pct,
s.gt_10pct,
s.gt_20pct,
s.bias()
);
}
// Print current config {1,2,3,5} for reference.
let current_mask: u16 = (1 << 0) | (1 << 1) | (1 << 2) | (1 << 4); // digits 1,2,3,5
let current_stats = all_results
.iter()
.find(|(m, _)| *m == current_mask)
.map(|(_, s)| s)
.unwrap();
let current_rank = results
.iter()
.position(|(m, _)| *m == current_mask)
.unwrap();
println!();
println!(
"Current {{1,2,3,5}} = rank {}/{}: RMSE {:.3}%, Max {:.1}%, >5%: {}, >10%: {}, >20%: {}",
current_rank + 1,
num_masks,
current_stats.rmse_pct(),
current_stats.max_pct(),
current_stats.gt_5pct,
current_stats.gt_10pct,
current_stats.gt_20pct,
);
println!("\nTotal time: {:.1}s", t0.elapsed().as_secs_f64());
}
@@ -0,0 +1,447 @@
//! Sweep round-value tolerance to find optimal rounding threshold.
//!
//! Tests different tolerance percentages (0%, 0.01%, 0.1%, 1%, etc.) for
//! detecting round BTC amounts, combined with several digit filter masks.
//!
//! Phase 1: single pass over indexer, store per-output relative errors.
//! Phase 2: sweep tolerance × mask combos across CPU cores.
//!
//! Run with: cargo run -p brk_oracle --example sweep_tolerance --release
use std::path::PathBuf;
use std::time::Instant;
use brk_indexer::Indexer;
use brk_oracle::{Config, NUM_BINS, Oracle, PRICES, START_HEIGHT, cents_to_bin, sats_to_bin};
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
use vecdb::{AnyVec, VecIndex, VecIterator};
const BINS_5PCT: f64 = 4.24;
const BINS_10PCT: f64 = 8.28;
const BINS_20PCT: f64 = 15.84;
fn bins_to_pct(bins: f64) -> f64 {
(10.0_f64.powf(bins / 200.0) - 1.0) * 100.0
}
fn seed_bin(start_height: usize) -> f64 {
let price: f64 = PRICES
.lines()
.nth(start_height - 1)
.expect("prices.txt too short")
.parse()
.expect("Failed to parse seed price");
cents_to_bin(price * 100.0)
}
fn leading_digit(sats: u64) -> u8 {
let log = (sats as f64).log10();
let magnitude = 10.0_f64.powf(log.floor());
let d = (sats as f64 / magnitude).round() as u8;
if d >= 10 { 1 } else { d }
}
/// Returns the relative error of `sats` from its nearest round value (d × 10^n).
/// e.g. 10_050 → leading=1, round_val=10_000, rel_err = 50/10000 = 0.005
fn relative_roundness(sats: u64) -> f64 {
let log = (sats as f64).log10();
let magnitude = 10.0_f64.powf(log.floor());
let leading = (sats as f64 / magnitude).round();
let round_val = leading * magnitude;
(sats as f64 - round_val).abs() / round_val
}
struct Stats {
total_sq_err: f64,
total_bias: f64,
max_err: f64,
total_blocks: u64,
gt_5pct: u64,
gt_10pct: u64,
gt_20pct: u64,
}
impl Stats {
fn new() -> Self {
Self {
total_sq_err: 0.0,
total_bias: 0.0,
max_err: 0.0,
total_blocks: 0,
gt_5pct: 0,
gt_10pct: 0,
gt_20pct: 0,
}
}
fn update(&mut self, err: f64) {
self.total_sq_err += err * err;
self.total_bias += err;
self.total_blocks += 1;
let abs_err = err.abs();
if abs_err > self.max_err {
self.max_err = abs_err;
}
if abs_err > BINS_5PCT {
self.gt_5pct += 1;
}
if abs_err > BINS_10PCT {
self.gt_10pct += 1;
}
if abs_err > BINS_20PCT {
self.gt_20pct += 1;
}
}
fn rmse_pct(&self) -> f64 {
bins_to_pct((self.total_sq_err / self.total_blocks as f64).sqrt())
}
fn max_pct(&self) -> f64 {
bins_to_pct(self.max_err)
}
fn bias(&self) -> f64 {
self.total_bias / self.total_blocks as f64
}
}
/// Per-output data: bin index, leading digit, relative error from round value.
struct RoundOutput {
bin: u16,
digit: u8,
rel_err: f32, // f32 is plenty of precision, saves memory
}
struct BlockData {
full_hist: Box<[u32; NUM_BINS]>,
round_outputs: Vec<RoundOutput>,
high_bin: f64,
low_bin: f64,
}
fn main() {
let t0 = Instant::now();
let data_dir = std::env::var("BRK_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap();
PathBuf::from(home).join(".brk")
});
let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer");
let total_heights = indexer.vecs.blocks.timestamp.len();
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let height_ohlc: Vec<[f64; 4]> = serde_json::from_str(
&std::fs::read_to_string(format!("{manifest_dir}/examples/height_price_ohlc.json"))
.expect("Failed to read height_price_ohlc.json"),
)
.expect("Failed to parse height OHLC");
let height_bands: Vec<(f64, f64)> = height_ohlc
.iter()
.map(|ohlc| {
let high = ohlc[1];
let low = ohlc[2];
if high > 0.0 && low > 0.0 {
(cents_to_bin(high * 100.0), cents_to_bin(low * 100.0))
} else {
(0.0, 0.0)
}
})
.collect();
let sweep_start: usize = 575_000;
// Phase 1: precompute per-block data.
// Store all potentially-round outputs with their relative error so we can
// filter at different tolerance thresholds in Phase 2.
eprintln!("Phase 1: precomputing block data...");
let total_txs = indexer.vecs.transactions.height.len();
let total_outputs = indexer.vecs.outputs.value.len();
let mut first_txindex_iter = indexer.vecs.transactions.first_txindex.into_iter();
let mut first_txoutindex_iter = indexer.vecs.transactions.first_txoutindex.into_iter();
let mut out_first_iter = indexer.vecs.outputs.first_txoutindex.into_iter();
let mut value_iter = indexer.vecs.outputs.value.into_iter();
let mut outputtype_iter = indexer.vecs.outputs.outputtype.into_iter();
let ref_config = Config::default();
let total_blocks = total_heights - sweep_start;
let mut blocks: Vec<BlockData> = Vec::with_capacity(total_blocks);
// Use the widest tolerance we'll test (5%) to decide what to store.
// Outputs beyond 5% relative error will never be filtered at any tolerance.
let max_tolerance: f64 = 0.05;
for h in START_HEIGHT..total_heights {
let first_txindex: TxIndex = first_txindex_iter.get_at_unwrap(h);
let next_first_txindex = first_txindex_iter
.get_at(h + 1)
.unwrap_or(TxIndex::from(total_txs));
let out_start = if first_txindex.to_usize() + 1 < next_first_txindex.to_usize() {
first_txoutindex_iter
.get_at_unwrap(first_txindex.to_usize() + 1)
.to_usize()
} else {
out_first_iter
.get_at(h + 1)
.unwrap_or(TxOutIndex::from(total_outputs))
.to_usize()
};
let out_end = out_first_iter
.get_at(h + 1)
.unwrap_or(TxOutIndex::from(total_outputs))
.to_usize();
if h < sweep_start {
continue;
}
let mut full_hist = Box::new([0u32; NUM_BINS]);
let mut round_outputs = Vec::new();
for i in out_start..out_end {
let sats: Sats = value_iter.get_at_unwrap(i);
let output_type: OutputType = outputtype_iter.get_at_unwrap(i);
if ref_config.excluded_output_types.contains(&output_type) {
continue;
}
if *sats < ref_config.min_sats {
continue;
}
if let Some(bin) = sats_to_bin(sats) {
full_hist[bin] += 1;
let d = leading_digit(*sats);
if (1..=9).contains(&d) {
let rel_err = relative_roundness(*sats);
if rel_err <= max_tolerance {
round_outputs.push(RoundOutput {
bin: bin as u16,
digit: d,
rel_err: rel_err as f32,
});
}
}
}
}
let (high_bin, low_bin) = if h < height_bands.len() {
height_bands[h]
} else {
(0.0, 0.0)
};
blocks.push(BlockData {
full_hist,
round_outputs,
high_bin,
low_bin,
});
if (h - sweep_start).is_multiple_of(50_000) {
eprint!(
"\r {}/{} ({:.0}%)",
h - sweep_start,
total_blocks,
(h - sweep_start) as f64 / total_blocks as f64 * 100.0
);
}
}
let mem_hists = blocks.len() * std::mem::size_of::<[u32; NUM_BINS]>();
let mem_rounds: usize = blocks
.iter()
.map(|b| b.round_outputs.len() * std::mem::size_of::<RoundOutput>())
.sum();
eprintln!(
"\r {} blocks precomputed ({:.1} GB hists + {:.0} MB rounds) in {:.1}s",
blocks.len(),
mem_hists as f64 / 1e9,
mem_rounds as f64 / 1e6,
t0.elapsed().as_secs_f64()
);
// Phase 2: sweep tolerance × mask combos.
// Tolerances as fractions (not percentages).
let tolerances: &[(f64, &str)] = &[
(0.0, "0%"),
(0.0001, "0.01%"),
(0.0005, "0.05%"),
(0.001, "0.1%"),
(0.002, "0.2%"),
(0.005, "0.5%"),
(0.01, "1%"),
(0.02, "2%"),
(0.05, "5%"),
];
// 987654321
let masks: &[(u16, &str)] = &[
(0b0_0000_0000, "none"),
(0b0_0001_0111, "{1,2,3,5}"),
(0b0_0001_1111, "{1,2,3,4,5}"),
(0b0_0011_0111, "{1,2,3,5,6}"),
(0b0_0111_0111, "{1,2,3,5,6,7}"),
(0b1_1111_1111, "{1-9}"),
];
let num_configs = tolerances.len() * masks.len();
let num_threads = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(8);
eprintln!(
"Phase 2: sweeping {} configs ({} tolerances × {} masks) across {} threads...",
num_configs,
tolerances.len(),
masks.len(),
num_threads
);
let t1 = Instant::now();
let blocks = &blocks;
let tolerances_ref = tolerances;
let masks_ref = masks;
let all_results: Vec<(usize, usize, Stats)> = std::thread::scope(|s| {
let configs_per_thread = num_configs.div_ceil(num_threads);
let handles: Vec<_> = (0..num_threads)
.map(|t| {
s.spawn(move || {
let cfg_start = t * configs_per_thread;
let cfg_end = ((t + 1) * configs_per_thread).min(num_configs);
if cfg_start >= cfg_end {
return vec![];
}
let mut results = Vec::with_capacity(cfg_end - cfg_start);
for cfg_idx in cfg_start..cfg_end {
let ti = cfg_idx / masks_ref.len();
let mi = cfg_idx % masks_ref.len();
let (tolerance, _) = tolerances_ref[ti];
let (mask, _) = masks_ref[mi];
let mut oracle = Oracle::new(
seed_bin(sweep_start),
Config {
exclude_common_round_values: false,
..Default::default()
},
);
let mut stats = Stats::new();
for bd in blocks.iter() {
let mut hist = *bd.full_hist;
// Remove outputs matching this tolerance + mask.
let tol_f32 = tolerance as f32;
for ro in &bd.round_outputs {
if mask & (1 << (ro.digit - 1)) != 0
&& ro.rel_err <= tol_f32
{
hist[ro.bin as usize] -= 1;
}
}
let ref_bin = oracle.process_histogram(&hist);
if bd.high_bin > 0.0 && bd.low_bin > 0.0 {
let err = if ref_bin < bd.high_bin {
ref_bin - bd.high_bin
} else if ref_bin > bd.low_bin {
ref_bin - bd.low_bin
} else {
0.0
};
stats.update(err);
}
}
results.push((ti, mi, stats));
}
results
})
})
.collect();
handles
.into_iter()
.flat_map(|h| h.join().unwrap())
.collect()
});
eprintln!(" Done in {:.1}s.", t1.elapsed().as_secs_f64());
// Print results grouped by tolerance.
println!();
println!(
"{:>8} {:>16} {:>8} {:>10} {:>10} {:>6} {:>6} {:>6} {:>8}",
"Tol", "Digits", "Blocks", "RMSE%", "Max%", ">5%", ">10%", ">20%", "Bias"
);
println!("{}", "-".repeat(88));
for (ti, &(_, tol_label)) in tolerances.iter().enumerate() {
for (mi, &(_, mask_label)) in masks.iter().enumerate() {
let (_, _, stats) = all_results
.iter()
.find(|(t, m, _)| *t == ti && *m == mi)
.unwrap();
println!(
"{:>8} {:>16} {:>8} {:>8.3}% {:>8.1}% {:>6} {:>6} {:>6} {:>+8.2}",
tol_label,
mask_label,
stats.total_blocks,
stats.rmse_pct(),
stats.max_pct(),
stats.gt_5pct,
stats.gt_10pct,
stats.gt_20pct,
stats.bias()
);
}
println!();
}
// Find overall best config by RMSE.
let best = all_results
.iter()
.min_by(|a, b| a.2.rmse_pct().partial_cmp(&b.2.rmse_pct()).unwrap())
.unwrap();
let (bti, bmi, bs) = best;
println!(
"Best: tolerance={}, digits={} → RMSE {:.3}%, Max {:.1}%, >5%: {}, >10%: {}, >20%: {}",
tolerances[*bti].1,
masks[*bmi].1,
bs.rmse_pct(),
bs.max_pct(),
bs.gt_5pct,
bs.gt_10pct,
bs.gt_20pct,
);
// Show current config for reference.
let current = all_results
.iter()
.find(|(t, m, _)| {
tolerances[*t].0 == 0.001 && masks[*m].0 == 0b0_0011_0111
})
.unwrap();
let (_, _, cs) = current;
println!(
"Current: tolerance=0.1%, digits={{1,2,3,5,6}} → RMSE {:.3}%, Max {:.1}%, >5%: {}, >10%: {}, >20%: {}",
cs.rmse_pct(),
cs.max_pct(),
cs.gt_5pct,
cs.gt_10pct,
cs.gt_20pct,
);
println!("\nTotal time: {:.1}s", t0.elapsed().as_secs_f64());
}
+253
View File
@@ -0,0 +1,253 @@
//! Validate oracle accuracy against exchange reference prices.
//!
//! Run with: cargo run -p brk_oracle --example validate --release
//!
//! Requires:
//! - ~/.brk indexed blockchain data (brk_indexer)
//! - examples/height_price_ohlc.json (per-height [open, high, low, close] in dollars)
use std::path::PathBuf;
use brk_indexer::Indexer;
use brk_oracle::{cents_to_bin, sats_to_bin, Config, Oracle, NUM_BINS, PRICES, START_HEIGHT};
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
use vecdb::{AnyVec, VecIndex, VecIterator};
const BINS_5PCT: f64 = 4.24;
const BINS_10PCT: f64 = 8.28;
const BINS_20PCT: f64 = 15.84;
fn bins_to_pct(bins: f64) -> f64 {
(10.0_f64.powf(bins / 200.0) - 1.0) * 100.0
}
fn seed_bin(start_height: usize) -> f64 {
let price: f64 = PRICES
.lines()
.nth(start_height - 1)
.expect("prices.txt too short")
.parse()
.expect("Failed to parse seed price");
cents_to_bin(price * 100.0)
}
struct Stats {
total_sq_err: f64,
total_bias: f64,
max_err: f64,
total_blocks: u64,
gt_5pct: u64,
gt_10pct: u64,
gt_20pct: u64,
}
impl Stats {
fn new() -> Self {
Self {
total_sq_err: 0.0,
total_bias: 0.0,
max_err: 0.0,
total_blocks: 0,
gt_5pct: 0,
gt_10pct: 0,
gt_20pct: 0,
}
}
fn update(&mut self, err: f64) {
self.total_sq_err += err * err;
self.total_bias += err;
self.total_blocks += 1;
let abs_err = err.abs();
if abs_err > self.max_err {
self.max_err = abs_err;
}
if abs_err > BINS_5PCT {
self.gt_5pct += 1;
}
if abs_err > BINS_10PCT {
self.gt_10pct += 1;
}
if abs_err > BINS_20PCT {
self.gt_20pct += 1;
}
}
fn rmse_pct(&self) -> f64 {
bins_to_pct((self.total_sq_err / self.total_blocks as f64).sqrt())
}
fn max_pct(&self) -> f64 {
bins_to_pct(self.max_err)
}
fn bias(&self) -> f64 {
self.total_bias / self.total_blocks as f64
}
}
struct Run {
label: &'static str,
start_height: usize,
oracle: Option<Oracle>,
stats: Stats,
}
fn main() {
let data_dir = std::env::var("BRK_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap();
PathBuf::from(home).join(".brk")
});
let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer");
let total_heights = indexer.vecs.blocks.timestamp.len();
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let height_ohlc: Vec<[f64; 4]> = serde_json::from_str(
&std::fs::read_to_string(format!("{manifest_dir}/examples/height_price_ohlc.json"))
.expect("Failed to read height_price_ohlc.json"),
)
.expect("Failed to parse height OHLC");
// Pre-compute per-height (high_bin, low_bin) tolerance band.
let height_bands: Vec<(f64, f64)> = height_ohlc
.iter()
.map(|ohlc| {
let high = ohlc[1];
let low = ohlc[2];
if high > 0.0 && low > 0.0 {
(cents_to_bin(high * 100.0), cents_to_bin(low * 100.0))
} else {
(0.0, 0.0)
}
})
.collect();
let mut runs = vec![
Run { label: "w12 @ 575k", start_height: 575_000, oracle: None, stats: Stats::new() },
Run { label: "w12 @ 600k", start_height: 600_000, oracle: None, stats: Stats::new() },
Run { label: "w12 @ 630k", start_height: 630_000, oracle: None, stats: Stats::new() },
];
// Build per-block filtered histograms from the indexer, feeding all oracles in one pass.
let total_txs = indexer.vecs.transactions.height.len();
let total_outputs = indexer.vecs.outputs.value.len();
let mut first_txindex_iter = indexer.vecs.transactions.first_txindex.into_iter();
let mut first_txoutindex_iter = indexer.vecs.transactions.first_txoutindex.into_iter();
let mut out_first_iter = indexer.vecs.outputs.first_txoutindex.into_iter();
let mut value_iter = indexer.vecs.outputs.value.into_iter();
let mut outputtype_iter = indexer.vecs.outputs.outputtype.into_iter();
let ref_config = Config::default();
for h in START_HEIGHT..total_heights {
let first_txindex: TxIndex = first_txindex_iter.get_at_unwrap(h);
let next_first_txindex = first_txindex_iter
.get_at(h + 1)
.unwrap_or(TxIndex::from(total_txs));
let out_start = if first_txindex.to_usize() + 1 < next_first_txindex.to_usize() {
first_txoutindex_iter
.get_at_unwrap(first_txindex.to_usize() + 1)
.to_usize()
} else {
out_first_iter
.get_at(h + 1)
.unwrap_or(TxOutIndex::from(total_outputs))
.to_usize()
};
let out_end = out_first_iter
.get_at(h + 1)
.unwrap_or(TxOutIndex::from(total_outputs))
.to_usize();
// Build filtered histogram once for all oracles.
let mut hist = [0u32; NUM_BINS];
for i in out_start..out_end {
let sats: Sats = value_iter.get_at_unwrap(i);
let output_type: OutputType = outputtype_iter.get_at_unwrap(i);
if ref_config.excluded_output_types.contains(&output_type) {
continue;
}
if *sats < ref_config.min_sats
|| (ref_config.exclude_common_round_values && sats.is_common_round_value())
{
continue;
}
if let Some(bin) = sats_to_bin(sats) {
hist[bin] += 1;
}
}
for run in &mut runs {
if h < run.start_height {
continue;
}
if run.oracle.is_none() {
let config = Config::default();
run.oracle = Some(Oracle::new(seed_bin(run.start_height), config));
}
let ref_bin = run.oracle.as_mut().unwrap().process_histogram(&hist);
if h < height_bands.len() {
let (high_bin, low_bin) = height_bands[h];
if high_bin > 0.0 && low_bin > 0.0 {
let err = if ref_bin < high_bin {
ref_bin - high_bin
} else if ref_bin > low_bin {
ref_bin - low_bin
} else {
0.0
};
run.stats.update(err);
}
}
}
}
// Print results.
println!();
println!(
"{:<14} {:>8} {:>10} {:>10} {:>6} {:>6} {:>6} {:>8}",
"Config", "Blocks", "RMSE%", "Max%", ">5%", ">10%", ">20%", "Bias"
);
println!("{}", "-".repeat(72));
for run in &runs {
let s = &run.stats;
println!(
"{:<14} {:>8} {:>7.2}% {:>7.1}% {:>6} {:>6} {:>6} {:>+8.2}",
run.label,
s.total_blocks,
s.rmse_pct(),
s.max_pct(),
s.gt_5pct,
s.gt_10pct,
s.gt_20pct,
s.bias()
);
}
println!();
// Verify exact counts against reference.
// Reference: trunc w12 @ 575k: 261 >5%, 40 >10%, 0 >20%
// trunc w12 @ 600k: 174 >5%, 31 >10%, 0 >20%
// trunc w12 @ 630k: 84 >5%, 9 >10%, 0 >20%
let expected: &[(&str, u64, u64, u64)] = &[
("w12 @ 575k", 237, 22, 0),
("w12 @ 600k", 152, 15, 0),
("w12 @ 630k", 84, 9, 0),
];
for (run, &(label, exp_5, exp_10, exp_20)) in runs.iter().zip(expected) {
let s = &run.stats;
assert_eq!(s.gt_20pct, exp_20, "{label}: expected {exp_20} blocks >20%, got {}", s.gt_20pct);
assert_eq!(s.gt_10pct, exp_10, "{label}: expected {exp_10} blocks >10%, got {}", s.gt_10pct);
assert_eq!(s.gt_5pct, exp_5, "{label}: expected {exp_5} blocks >5%, got {}", s.gt_5pct);
}
println!("All assertions passed!");
}
+335
View File
@@ -0,0 +1,335 @@
//! Pure on-chain BTC/USD price oracle.
//!
//! Detects round-dollar transaction patterns ($1, $5, $10, ... $10,000) in Bitcoin
//! block outputs to derive the current price without any exchange data.
use brk_types::{Block, CentsUnsigned, Dollars, OutputType, Sats};
/// Pre-oracle dollar prices, one per line, heights 0..630_000.
pub const PRICES: &str = include_str!("prices.txt");
/// First height where the oracle computes from on-chain data.
pub const START_HEIGHT: usize = 575_000;
pub const BINS_PER_DECADE: usize = 200;
const MIN_LOG_BTC: i32 = -8;
const MAX_LOG_BTC: i32 = 4;
pub const NUM_BINS: usize = BINS_PER_DECADE * (MAX_LOG_BTC - MIN_LOG_BTC) as usize;
/// Bin offsets for 19 round-USD amounts relative to the $100 reference (offset 0).
/// Each offset = log10(amount / 100) * BINS_PER_DECADE.
const STENCIL_OFFSETS: [i32; 19] = [
-400, // $1
-340, // $2
-305, // $3
-260, // $5
-200, // $10
-165, // $15
-140, // $20
-120, // $25
-105, // $30
-60, // $50
0, // $100
35, // $150
60, // $200
95, // $300
140, // $500
200, // $1000
260, // $2000
340, // $5000
400, // $10000
];
/// Maps a satoshi value to its log-scale bin index.
/// bin = round(log10(sats) * BINS_PER_DECADE).
#[inline(always)]
pub fn sats_to_bin(sats: Sats) -> Option<usize> {
if sats.is_zero() {
return None;
}
let bin = ((*sats as f64).log10() * BINS_PER_DECADE as f64).round() as i64;
if bin >= 0 && (bin as usize) < NUM_BINS {
Some(bin as usize)
} else {
None
}
}
/// Converts a fractional bin to a USD price in cents.
/// For a $D output at price P: sats = D * 1e8 / P, so P = 10^(10 - bin/200) dollars,
/// where 10 = log10($100 reference * 1e8 sats/BTC).
#[inline]
pub fn bin_to_cents(bin: f64) -> u64 {
let dollars = 10.0_f64.powf(10.0 - bin / BINS_PER_DECADE as f64);
(dollars * 100.0).round() as u64
}
/// Converts a USD price in cents to a fractional bin (inverse of bin_to_cents).
#[inline]
pub fn cents_to_bin(cents: f64) -> f64 {
(10.0 - (cents / 100.0).log10()) * BINS_PER_DECADE as f64
}
/// Scores each candidate bin in the search window by summing normalized stencil
/// matches across the EMA histogram, then refines with parabolic interpolation.
fn find_best_bin(
ema: &[f64; NUM_BINS],
prev_bin: f64,
search_below: usize,
search_above: usize,
) -> f64 {
let center = prev_bin.round() as usize;
let search_start = center.saturating_sub(search_below);
let search_end = (center + search_above + 1).min(NUM_BINS);
if search_start >= search_end {
return prev_bin;
}
// Per-offset peak within the search window (for normalization).
let mut track_norm = [0.0f64; 19];
for (i, &offset) in STENCIL_OFFSETS.iter().enumerate() {
for bin in search_start..search_end {
let idx = bin as i32 + offset;
if idx >= 0 && (idx as usize) < NUM_BINS {
track_norm[i] = track_norm[i].max(ema[idx as usize]);
}
}
}
let score = |bin: usize| -> f64 {
let mut total = 0.0;
for (i, &offset) in STENCIL_OFFSETS.iter().enumerate() {
let idx = bin as i32 + offset;
if idx >= 0 && (idx as usize) < NUM_BINS && track_norm[i] > 0.0 {
total += ema[idx as usize] / track_norm[i];
}
}
total
};
let mut best_bin = search_start;
let mut best_score = score(search_start);
for bin in (search_start + 1)..search_end {
let candidate = score(bin);
if candidate > best_score {
best_score = candidate;
best_bin = bin;
}
}
// Parabolic sub-bin interpolation for fractional precision.
let score_center = best_score;
let score_left = if best_bin > search_start { score(best_bin - 1) } else { score_center };
let score_right = if best_bin + 1 < search_end { score(best_bin + 1) } else { score_center };
let denom = score_left - 2.0 * score_center + score_right;
let sub_bin = if denom.abs() > 1e-10 {
(0.5 * (score_left - score_right) / denom).clamp(-0.5, 0.5)
} else {
0.0
};
best_bin as f64 + sub_bin
}
#[derive(Clone)]
pub struct Config {
/// EMA decay: 2/(N+1) where N is span in blocks. 2/7 = 6-block span.
pub alpha: f64,
/// Ring buffer depth. 12 blocks for deterministic convergence at any start height.
pub window_size: usize,
/// Search window bins below/above previous estimate. Asymmetric for log-scale.
pub search_below: usize,
pub search_above: usize,
/// Minimum output value in sats (dust filter).
pub min_sats: u64,
/// Exclude round BTC amounts that create false stencil matches.
pub exclude_common_round_values: bool,
/// Output types to ignore (e.g. P2TR, P2WSH are noisy).
pub excluded_output_types: Vec<OutputType>,
}
impl Default for Config {
fn default() -> Self {
Self {
alpha: 2.0 / 7.0,
window_size: 12,
search_below: 9,
search_above: 11,
min_sats: 1000,
exclude_common_round_values: true,
excluded_output_types: vec![OutputType::P2TR, OutputType::P2WSH],
}
}
}
#[derive(Clone)]
pub struct Oracle {
histograms: Vec<[u32; NUM_BINS]>,
ema: Box<[f64; NUM_BINS]>,
cursor: usize,
filled: usize,
ref_bin: f64,
config: Config,
weights: Vec<f64>,
excluded_mask: u16,
warmup: bool,
}
impl Oracle {
pub fn new(start_bin: f64, config: Config) -> Self {
let window_size = config.window_size;
let decay = 1.0 - config.alpha;
let weights: Vec<f64> = (0..window_size)
.map(|i| config.alpha * decay.powi(i as i32))
.collect();
let excluded_mask = config
.excluded_output_types
.iter()
.fold(0u16, |mask, ot| mask | (1 << *ot as u8));
Self {
histograms: vec![[0u32; NUM_BINS]; window_size],
ema: Box::new([0.0; NUM_BINS]),
cursor: 0,
filled: 0,
ref_bin: start_bin,
weights,
excluded_mask,
warmup: false,
config,
}
}
pub fn process_block(&mut self, block: &Block) -> f64 {
self.process_outputs(
block
.txdata
.iter()
.skip(1) // skip coinbase
.flat_map(|tx| &tx.output)
.map(|txout| (Sats::from(txout.value), OutputType::from(&txout.script_pubkey))),
)
}
pub fn process_outputs(&mut self, outputs: impl Iterator<Item = (Sats, OutputType)>) -> f64 {
let mut hist = [0u32; NUM_BINS];
for (sats, output_type) in outputs {
if let Some(bin) = self.eligible_bin(sats, output_type) {
hist[bin] += 1;
}
}
self.ingest(&hist)
}
/// Create an oracle restored from a known price.
/// `fill` should feed warmup blocks to populate the ring buffer.
/// ref_bin is anchored to the checkpoint regardless of warmup drift.
pub fn from_checkpoint(ref_bin: f64, config: Config, fill: impl FnOnce(&mut Self)) -> Self {
let mut oracle = Self::new(ref_bin, config);
oracle.warmup = true;
fill(&mut oracle);
oracle.warmup = false;
oracle.recompute_ema();
oracle.ref_bin = ref_bin;
oracle
}
pub fn process_histogram(&mut self, hist: &[u32; NUM_BINS]) -> f64 {
self.ingest(hist)
}
pub fn ref_bin(&self) -> f64 {
self.ref_bin
}
pub fn price_cents(&self) -> CentsUnsigned {
bin_to_cents(self.ref_bin).into()
}
pub fn price_dollars(&self) -> Dollars {
self.price_cents().into()
}
#[inline(always)]
pub fn output_to_bin(&self, sats: Sats, output_type: OutputType) -> Option<usize> {
self.eligible_bin(sats, output_type)
}
#[inline(always)]
fn eligible_bin(&self, sats: Sats, output_type: OutputType) -> Option<usize> {
if self.excluded_mask & (1 << output_type as u8) != 0 {
return None;
}
if *sats < self.config.min_sats
|| (self.config.exclude_common_round_values && sats.is_common_round_value())
{
return None;
}
sats_to_bin(sats)
}
fn ingest(&mut self, hist: &[u32; NUM_BINS]) -> f64 {
self.histograms[self.cursor] = *hist;
self.cursor = (self.cursor + 1) % self.config.window_size;
if self.filled < self.config.window_size {
self.filled += 1;
}
if !self.warmup {
self.recompute_ema();
self.ref_bin = find_best_bin(
&self.ema,
self.ref_bin,
self.config.search_below,
self.config.search_above,
);
}
self.ref_bin
}
fn recompute_ema(&mut self) {
self.ema.fill(0.0);
for age in 0..self.filled {
let idx =
(self.cursor + self.config.window_size - 1 - age) % self.config.window_size;
let weight = self.weights[age];
let h = &self.histograms[idx];
for bin in 0..NUM_BINS {
self.ema[bin] += weight * h[bin] as f64;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sats_to_bin_round_trip() {
assert_eq!(sats_to_bin(Sats::new(100_000_000)), Some(1600));
assert_eq!(sats_to_bin(Sats::new(1)), Some(0));
assert_eq!(sats_to_bin(Sats::ZERO), None);
}
#[test]
fn bin_to_cents_known_values() {
assert_eq!(bin_to_cents(1600.0), 10000);
assert_eq!(bin_to_cents(1800.0), 1000);
}
#[test]
fn sats_to_bin_boundary() {
assert_eq!(sats_to_bin(Sats::new(1_000_000_000_000)), None);
let sats = 10.0_f64.powf(11.995) as u64;
assert!(sats_to_bin(Sats::new(sats)).is_some());
}
#[test]
fn oracle_basic() {
let oracle = Oracle::new(1600.0, Config::default());
assert_eq!(oracle.ref_bin(), 1600.0);
assert_eq!(oracle.price_cents(), bin_to_cents(1600.0).into());
}
}
+10
View File
@@ -201,6 +201,16 @@ impl Query {
Ok(utxos)
}
pub fn address_mempool_hash(&self, address: &Address) -> u64 {
let Some(mempool) = self.mempool() else {
return 0;
};
let Ok(bytes) = AddressBytes::from_str(address) else {
return 0;
};
mempool.address_hash(&bytes)
}
pub fn address_mempool_txids(&self, address: Address) -> Result<Vec<Txid>> {
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
+101
View File
@@ -0,0 +1,101 @@
use std::{fs, path::PathBuf};
use brk_error::{Error, Result};
use brk_types::{
CostBasisBucket, CostBasisDistribution, CostBasisFormatted, CostBasisValue, Date, DateIndex,
};
use vecdb::IterableVec;
use crate::Query;
impl Query {
/// List available cohorts for cost basis distribution.
pub fn cost_basis_cohorts(&self) -> Result<Vec<String>> {
let states_path = &self.computer().distribution.states_path;
let mut cohorts: Vec<String> = fs::read_dir(states_path)?
.filter_map(|entry| {
let name = entry.ok()?.file_name().into_string().ok()?;
let cohort = name.strip_prefix("utxo_")?.strip_suffix("_cost_basis")?;
states_path
.join(&name)
.join("by_date")
.exists()
.then(|| cohort.to_string())
})
.collect();
cohorts.sort();
Ok(cohorts)
}
fn cost_basis_dir(&self, cohort: &str) -> Result<PathBuf> {
let dir = self
.computer()
.distribution
.states_path
.join(format!("utxo_{cohort}_cost_basis/by_date"));
if !dir.exists() {
return Err(Error::NotFound(format!("Unknown cohort '{cohort}'")));
}
Ok(dir)
}
/// Get the cost basis distribution for a cohort on a specific date.
pub fn cost_basis_distribution(
&self,
cohort: &str,
date: Date,
) -> Result<CostBasisDistribution> {
let path = self.cost_basis_dir(cohort)?.join(date.to_string());
if !path.exists() {
return Err(Error::NotFound(format!(
"No data for cohort '{cohort}' on {date}"
)));
}
CostBasisDistribution::deserialize(&fs::read(&path)?)
}
/// List available dates for a cohort's cost basis distribution.
pub fn cost_basis_dates(&self, cohort: &str) -> Result<Vec<Date>> {
let dir = self.cost_basis_dir(cohort)?;
let mut dates: Vec<Date> = fs::read_dir(&dir)?
.filter_map(|entry| entry.ok()?.file_name().to_str()?.parse().ok())
.collect();
dates.sort();
Ok(dates)
}
/// Get the formatted cost basis distribution.
pub fn cost_basis_formatted(
&self,
cohort: &str,
date: Date,
bucket: CostBasisBucket,
value: CostBasisValue,
) -> Result<CostBasisFormatted> {
let distribution = self.cost_basis_distribution(cohort, date)?;
let dateindex =
DateIndex::try_from(date).map_err(|e| Error::Parse(e.to_string()))?;
let price = self
.computer()
.price
.as_ref()
.ok_or_else(|| Error::NotFound("Price data not available".to_string()))?;
let spot = *price
.cents
.split
.dateindex
.close
.iter()
.get(dateindex)
.ok_or_else(|| Error::NotFound(format!("No price data for {date}")))?;
Ok(distribution.format(bucket, value, spot))
}
}
+2
View File
@@ -1,9 +1,11 @@
mod address;
mod block;
mod cost_basis;
mod mempool;
mod metrics;
mod metrics_legacy;
mod mining;
mod price;
mod transaction;
pub use block::BLOCK_TXS_PAGE_SIZE;
+28
View File
@@ -0,0 +1,28 @@
use brk_error::{Error, Result};
use brk_types::Dollars;
use crate::Query;
impl Query {
pub fn live_price(&self) -> Result<Dollars> {
let oracle_vecs = &self
.computer()
.price
.as_ref()
.ok_or_else(|| Error::OutOfRange("Oracle prices not computed yet".into()))?
.oracle;
let mut oracle = oracle_vecs.live_oracle(self.indexer())?;
if let Some(mempool) = self.mempool() {
let txs = mempool.get_txs();
oracle.process_outputs(
txs.values()
.flat_map(|tx| &tx.tx().output)
.map(|txout| (txout.value, txout.type_())),
);
}
Ok(oracle.price_dollars())
}
}
+14 -8
View File
@@ -1,7 +1,7 @@
use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::{Path, Query, State},
http::HeaderMap,
http::{HeaderMap, Uri},
response::Redirect,
routing::get,
};
@@ -26,11 +26,12 @@ impl AddressRoutes for ApiRouter<AppState> {
.api_route(
"/api/address/{address}",
get_with(async |
uri: Uri,
headers: HeaderMap,
Path(path): Path<AddressParam>,
State(state): State<AppState>
| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.address(path.address)).await
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address(path.address)).await
}, |op| op
.id("get_address")
.addresses_tag()
@@ -46,12 +47,13 @@ impl AddressRoutes for ApiRouter<AppState> {
.api_route(
"/api/address/{address}/txs",
get_with(async |
uri: Uri,
headers: HeaderMap,
Path(path): Path<AddressParam>,
Query(params): Query<AddressTxidsParam>,
State(state): State<AppState>
| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.address_txids(path.address, params.after_txid, params.limit)).await
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address_txids(path.address, params.after_txid, params.limit)).await
}, |op| op
.id("get_address_txs")
.addresses_tag()
@@ -67,11 +69,12 @@ impl AddressRoutes for ApiRouter<AppState> {
.api_route(
"/api/address/{address}/utxo",
get_with(async |
uri: Uri,
headers: HeaderMap,
Path(path): Path<AddressParam>,
State(state): State<AppState>
| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.address_utxos(path.address)).await
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address_utxos(path.address)).await
}, |op| op
.id("get_address_utxos")
.addresses_tag()
@@ -87,12 +90,13 @@ impl AddressRoutes for ApiRouter<AppState> {
.api_route(
"/api/address/{address}/txs/mempool",
get_with(async |
uri: Uri,
headers: HeaderMap,
Path(path): Path<AddressParam>,
State(state): State<AppState>
| {
// Mempool txs for an address - use MaxAge since it's volatile
state.cached_json(&headers, CacheStrategy::MaxAge(5), move |q| q.address_mempool_txids(path.address)).await
let hash = state.sync(|q| q.address_mempool_hash(&path.address));
state.cached_json(&headers, CacheStrategy::MempoolHash(hash), &uri, move |q| q.address_mempool_txids(path.address)).await
}, |op| op
.id("get_address_mempool_txs")
.addresses_tag()
@@ -107,12 +111,13 @@ impl AddressRoutes for ApiRouter<AppState> {
.api_route(
"/api/address/{address}/txs/chain",
get_with(async |
uri: Uri,
headers: HeaderMap,
Path(path): Path<AddressParam>,
Query(params): Query<AddressTxidsParam>,
State(state): State<AppState>
| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.address_txids(path.address, params.after_txid, 25)).await
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address_txids(path.address, params.after_txid, 25)).await
}, |op| op
.id("get_address_confirmed_txs")
.addresses_tag()
@@ -128,11 +133,12 @@ impl AddressRoutes for ApiRouter<AppState> {
.api_route(
"/api/v1/validate-address/{address}",
get_with(async |
uri: Uri,
headers: HeaderMap,
Path(path): Path<ValidateAddressParam>,
State(state): State<AppState>
| {
state.cached_json(&headers, CacheStrategy::Static, move |_q| Ok(AddressValidation::from_address(&path.address))).await
state.cached_json(&headers, CacheStrategy::Static, &uri, move |_q| Ok(AddressValidation::from_address(&path.address))).await
}, |op| op
.id("validate_address")
.addresses_tag()
+30 -21
View File
@@ -1,7 +1,7 @@
use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::{Path, State},
http::HeaderMap,
http::{HeaderMap, Uri},
};
use brk_query::BLOCK_TXS_PAGE_SIZE;
use brk_types::{
@@ -22,9 +22,9 @@ impl BlockRoutes for ApiRouter<AppState> {
self.api_route(
"/api/blocks",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state
.cached_json(&headers, CacheStrategy::Height, move |q| q.blocks(None))
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.blocks(None))
.await
},
|op| {
@@ -41,10 +41,11 @@ impl BlockRoutes for ApiRouter<AppState> {
.api_route(
"/api/block/{hash}",
get_with(
async |headers: HeaderMap,
async |uri: Uri,
headers: HeaderMap,
Path(path): Path<BlockHashParam>,
State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Static, move |q| q.block(&path.hash)).await
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| q.block(&path.hash)).await
},
|op| {
op.id("get_block")
@@ -64,10 +65,11 @@ impl BlockRoutes for ApiRouter<AppState> {
.api_route(
"/api/block/{hash}/status",
get_with(
async |headers: HeaderMap,
async |uri: Uri,
headers: HeaderMap,
Path(path): Path<BlockHashParam>,
State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_status(&path.hash)).await
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_status(&path.hash)).await
},
|op| {
op.id("get_block_status")
@@ -87,10 +89,11 @@ impl BlockRoutes for ApiRouter<AppState> {
.api_route(
"/api/block-height/{height}",
get_with(
async |headers: HeaderMap,
async |uri: Uri,
headers: HeaderMap,
Path(path): Path<HeightParam>,
State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_by_height(path.height)).await
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_by_height(path.height)).await
},
|op| {
op.id("get_block_by_height")
@@ -110,10 +113,11 @@ impl BlockRoutes for ApiRouter<AppState> {
.api_route(
"/api/blocks/{height}",
get_with(
async |headers: HeaderMap,
async |uri: Uri,
headers: HeaderMap,
Path(path): Path<HeightParam>,
State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.blocks(Some(path.height))).await
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.blocks(Some(path.height))).await
},
|op| {
op.id("get_blocks_from_height")
@@ -132,10 +136,11 @@ impl BlockRoutes for ApiRouter<AppState> {
.api_route(
"/api/block/{hash}/txids",
get_with(
async |headers: HeaderMap,
async |uri: Uri,
headers: HeaderMap,
Path(path): Path<BlockHashParam>,
State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Static, move |q| q.block_txids(&path.hash)).await
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| q.block_txids(&path.hash)).await
},
|op| {
op.id("get_block_txids")
@@ -155,10 +160,11 @@ impl BlockRoutes for ApiRouter<AppState> {
.api_route(
"/api/block/{hash}/txs/{start_index}",
get_with(
async |headers: HeaderMap,
async |uri: Uri,
headers: HeaderMap,
Path(path): Path<BlockHashStartIndex>,
State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Static, move |q| q.block_txs(&path.hash, path.start_index)).await
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| q.block_txs(&path.hash, path.start_index)).await
},
|op| {
op.id("get_block_txs")
@@ -179,10 +185,11 @@ impl BlockRoutes for ApiRouter<AppState> {
.api_route(
"/api/block/{hash}/txid/{index}",
get_with(
async |headers: HeaderMap,
async |uri: Uri,
headers: HeaderMap,
Path(path): Path<BlockHashTxIndex>,
State(state): State<AppState>| {
state.cached_text(&headers, CacheStrategy::Static, move |q| q.block_txid_at_index(&path.hash, path.index).map(|t| t.to_string())).await
state.cached_text(&headers, CacheStrategy::Static, &uri, move |q| q.block_txid_at_index(&path.hash, path.index).map(|t| t.to_string())).await
},
|op| {
op.id("get_block_txid")
@@ -202,10 +209,11 @@ impl BlockRoutes for ApiRouter<AppState> {
.api_route(
"/api/v1/mining/blocks/timestamp/{timestamp}",
get_with(
async |headers: HeaderMap,
async |uri: Uri,
headers: HeaderMap,
Path(path): Path<TimestampParam>,
State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_by_timestamp(path.timestamp)).await
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_by_timestamp(path.timestamp)).await
},
|op| {
op.id("get_block_by_timestamp")
@@ -223,10 +231,11 @@ impl BlockRoutes for ApiRouter<AppState> {
.api_route(
"/api/block/{hash}/raw",
get_with(
async |headers: HeaderMap,
async |uri: Uri,
headers: HeaderMap,
Path(path): Path<BlockHashParam>,
State(state): State<AppState>| {
state.cached_bytes(&headers, CacheStrategy::Static, move |q| q.block_raw(&path.hash)).await
state.cached_bytes(&headers, CacheStrategy::Static, &uri, move |q| q.block_raw(&path.hash)).await
},
|op| {
op.id("get_block_raw")
+31 -11
View File
@@ -1,8 +1,8 @@
use aide::axum::{ApiRouter, routing::get_with};
use axum::{extract::State, http::HeaderMap, response::Redirect, routing::get};
use brk_types::{MempoolBlock, MempoolInfo, RecommendedFees, Txid};
use axum::{extract::State, http::{HeaderMap, Uri}, response::Redirect, routing::get};
use brk_types::{Dollars, MempoolBlock, MempoolInfo, RecommendedFees, Txid};
use crate::{CacheStrategy, extended::TransformResponseExtended};
use crate::extended::TransformResponseExtended;
use super::AppState;
@@ -17,8 +17,8 @@ impl MempoolRoutes for ApiRouter<AppState> {
.api_route(
"/api/mempool/info",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::MaxAge(5), |q| q.mempool_info()).await
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.mempool_info()).await
},
|op| {
op.id("get_mempool")
@@ -33,8 +33,8 @@ impl MempoolRoutes for ApiRouter<AppState> {
.api_route(
"/api/mempool/txids",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::MaxAge(5), |q| q.mempool_txids()).await
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.mempool_txids()).await
},
|op| {
op.id("get_mempool_txids")
@@ -49,8 +49,8 @@ impl MempoolRoutes for ApiRouter<AppState> {
.api_route(
"/api/v1/fees/recommended",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::MaxAge(3), |q| q.recommended_fees()).await
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.recommended_fees()).await
},
|op| {
op.id("get_recommended_fees")
@@ -62,11 +62,31 @@ impl MempoolRoutes for ApiRouter<AppState> {
},
),
)
.api_route(
"/api/mempool/price",
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.live_price()).await
},
|op| {
op.id("get_live_price")
.mempool_tag()
.summary("Live BTC/USD price")
.description(
"Returns the current BTC/USD price in dollars, derived from \
on-chain round-dollar output patterns in the last 12 blocks \
plus mempool.",
)
.ok_response::<Dollars>()
.server_error()
},
),
)
.api_route(
"/api/v1/fees/mempool-blocks",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::MaxAge(5), |q| q.mempool_blocks()).await
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.mempool_blocks()).await
},
|op| {
op.id("get_mempool_blocks")
+23 -39
View File
@@ -1,14 +1,13 @@
use std::{net::SocketAddr, time::Duration};
use std::net::SocketAddr;
use axum::{
Extension,
body::Body,
body::{Body, Bytes},
extract::{Query, State},
http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
};
use brk_types::{Format, MetricSelection, Output};
use quick_cache::sync::GuardResult;
use crate::{
Result,
@@ -23,56 +22,41 @@ pub async fn handler(
headers: HeaderMap,
Extension(addr): Extension<SocketAddr>,
Query(params): Query<MetricSelection>,
State(AppState { query, cache, .. }): State<AppState>,
State(state): State<AppState>,
) -> Result<Response> {
// Phase 1: Search and resolve metadata (cheap)
let resolved = query.run(move |q| q.resolve(params, max_weight(&addr))).await?;
let resolved = state.run(move |q| q.resolve(params, max_weight(&addr))).await?;
let format = resolved.format();
let etag = resolved.etag();
// Check if client has fresh cache
if headers.has_etag(etag.as_str()) {
let response = (StatusCode::NOT_MODIFIED, "").into_response();
return Ok(response);
return Ok((StatusCode::NOT_MODIFIED, "").into_response());
}
// Check server-side cache
// Phase 2: Format (expensive, server-side cached)
let cache_key = format!("bulk-{}{}{}", uri.path(), uri.query().unwrap_or(""), etag);
let guard_res = cache.get_value_or_guard(&cache_key, Some(Duration::from_millis(50)));
let mut response = if let GuardResult::Value(v) = guard_res {
Response::new(Body::from(v))
} else {
// Phase 2: Format (expensive, only on cache miss)
let metric_output = query.run(move |q| q.format(resolved)).await?;
match metric_output.output {
Output::CSV(s) => {
if let GuardResult::Guard(g) = guard_res {
let _ = g.insert(s.clone().into());
}
s.into_response()
}
Output::Json(v) => {
if let GuardResult::Guard(g) = guard_res {
let _ = g.insert(v.clone().into());
}
Response::new(Body::from(v))
}
}
};
let headers = response.headers_mut();
headers.insert_etag(etag.as_str());
headers.insert_cache_control(CACHE_CONTROL);
let query = &state;
let bytes = state
.get_or_insert(&cache_key, async move {
let out = query.run(move |q| q.format(resolved)).await?;
Ok(match out.output {
Output::CSV(s) => Bytes::from(s),
Output::Json(v) => Bytes::from(v),
})
})
.await?;
let mut response = Response::new(Body::from(bytes));
let h = response.headers_mut();
h.insert_etag(etag.as_str());
h.insert_cache_control(CACHE_CONTROL);
match format {
Format::CSV => {
headers.insert_content_disposition_attachment();
headers.insert_content_type_text_csv()
h.insert_content_disposition_attachment();
h.insert_content_type_text_csv()
}
Format::JSON => headers.insert_content_type_application_json(),
Format::JSON => h.insert_content_type_application_json(),
}
Ok(response)
+23 -39
View File
@@ -1,14 +1,13 @@
use std::{net::SocketAddr, time::Duration};
use std::net::SocketAddr;
use axum::{
Extension,
body::Body,
body::{Body, Bytes},
extract::{Query, State},
http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
};
use brk_types::{Format, MetricSelection, Output};
use quick_cache::sync::GuardResult;
use crate::{
Result,
@@ -23,56 +22,41 @@ pub async fn handler(
headers: HeaderMap,
Extension(addr): Extension<SocketAddr>,
Query(params): Query<MetricSelection>,
State(AppState { query, cache, .. }): State<AppState>,
State(state): State<AppState>,
) -> Result<Response> {
// Phase 1: Search and resolve metadata (cheap)
let resolved = query.run(move |q| q.resolve(params, max_weight(&addr))).await?;
let resolved = state.run(move |q| q.resolve(params, max_weight(&addr))).await?;
let format = resolved.format();
let etag = resolved.etag();
// Check if client has fresh cache
if headers.has_etag(etag.as_str()) {
let response = (StatusCode::NOT_MODIFIED, "").into_response();
return Ok(response);
return Ok((StatusCode::NOT_MODIFIED, "").into_response());
}
// Check server-side cache
// Phase 2: Format (expensive, server-side cached)
let cache_key = format!("single-{}{}{}", uri.path(), uri.query().unwrap_or(""), etag);
let guard_res = cache.get_value_or_guard(&cache_key, Some(Duration::from_millis(50)));
let mut response = if let GuardResult::Value(v) = guard_res {
Response::new(Body::from(v))
} else {
// Phase 2: Format (expensive, only on cache miss)
let metric_output = query.run(move |q| q.format(resolved)).await?;
match metric_output.output {
Output::CSV(s) => {
if let GuardResult::Guard(g) = guard_res {
let _ = g.insert(s.clone().into());
}
s.into_response()
}
Output::Json(v) => {
if let GuardResult::Guard(g) = guard_res {
let _ = g.insert(v.clone().into());
}
Response::new(Body::from(v))
}
}
};
let headers = response.headers_mut();
headers.insert_etag(etag.as_str());
headers.insert_cache_control(CACHE_CONTROL);
let query = &state;
let bytes = state
.get_or_insert(&cache_key, async move {
let out = query.run(move |q| q.format(resolved)).await?;
Ok(match out.output {
Output::CSV(s) => Bytes::from(s),
Output::Json(v) => Bytes::from(v),
})
})
.await?;
let mut response = Response::new(Body::from(bytes));
let h = response.headers_mut();
h.insert_etag(etag.as_str());
h.insert_cache_control(CACHE_CONTROL);
match format {
Format::CSV => {
headers.insert_content_disposition_attachment();
headers.insert_content_type_text_csv()
h.insert_content_disposition_attachment();
h.insert_content_type_text_csv()
}
Format::JSON => headers.insert_content_type_application_json(),
Format::JSON => h.insert_content_type_application_json(),
}
Ok(response)
+23 -40
View File
@@ -1,14 +1,13 @@
use std::{net::SocketAddr, time::Duration};
use std::net::SocketAddr;
use axum::{
Extension,
body::Body,
body::{Body, Bytes},
extract::{Query, State},
http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
};
use brk_types::{Format, MetricSelection, OutputLegacy};
use quick_cache::sync::GuardResult;
use crate::{
Result,
@@ -23,57 +22,41 @@ pub async fn handler(
headers: HeaderMap,
Extension(addr): Extension<SocketAddr>,
Query(params): Query<MetricSelection>,
State(AppState { query, cache, .. }): State<AppState>,
State(state): State<AppState>,
) -> Result<Response> {
// Phase 1: Search and resolve metadata (cheap)
let resolved = query.run(move |q| q.resolve(params, max_weight(&addr))).await?;
let resolved = state.run(move |q| q.resolve(params, max_weight(&addr))).await?;
let format = resolved.format();
let etag = resolved.etag();
// Check if client has fresh cache
if headers.has_etag(etag.as_str()) {
let response = (StatusCode::NOT_MODIFIED, "").into_response();
return Ok(response);
return Ok((StatusCode::NOT_MODIFIED, "").into_response());
}
// Check server-side cache
// Phase 2: Format (expensive, server-side cached)
let cache_key = format!("legacy-{}{}{}", uri.path(), uri.query().unwrap_or(""), etag);
let guard_res = cache.get_value_or_guard(&cache_key, Some(Duration::from_millis(50)));
let mut response = if let GuardResult::Value(v) = guard_res {
Response::new(Body::from(v))
} else {
// Phase 2: Format (expensive, only on cache miss)
let metric_output = query.run(move |q| q.format_legacy(resolved)).await?;
match metric_output.output {
OutputLegacy::CSV(s) => {
if let GuardResult::Guard(g) = guard_res {
let _ = g.insert(s.clone().into());
}
s.into_response()
}
OutputLegacy::Json(v) => {
let json = v.to_vec();
if let GuardResult::Guard(g) = guard_res {
let _ = g.insert(json.clone().into());
}
json.into_response()
}
}
};
let headers = response.headers_mut();
headers.insert_etag(etag.as_str());
headers.insert_cache_control(CACHE_CONTROL);
let query = &state;
let bytes = state
.get_or_insert(&cache_key, async move {
let out = query.run(move |q| q.format_legacy(resolved)).await?;
Ok(match out.output {
OutputLegacy::CSV(s) => Bytes::from(s),
OutputLegacy::Json(v) => Bytes::from(v.to_vec()),
})
})
.await?;
let mut response = Response::new(Body::from(bytes));
let h = response.headers_mut();
h.insert_etag(etag.as_str());
h.insert_cache_control(CACHE_CONTROL);
match format {
Format::CSV => {
headers.insert_content_disposition_attachment();
headers.insert_content_type_text_csv()
h.insert_content_disposition_attachment();
h.insert_content_type_text_csv()
}
Format::JSON => headers.insert_content_type_application_json(),
Format::JSON => h.insert_content_type_application_json(),
}
Ok(response)
+92 -8
View File
@@ -9,7 +9,8 @@ use axum::{
};
use brk_traversable::TreeNode;
use brk_types::{
DataRangeFormat, Index, IndexInfo, LimitParam, Metric, MetricCount, MetricData, MetricParam,
CostBasisCohortParam, CostBasisFormatted, CostBasisParams, CostBasisQuery, DataRangeFormat,
Date, Index, IndexInfo, LimitParam, Metric, MetricCount, MetricData, MetricParam,
MetricSelection, MetricSelectionLegacy, MetricWithIndex, Metrics, PaginatedMetrics, Pagination,
};
@@ -47,8 +48,8 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
self.api_route(
"/api/metrics",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.metrics_catalog().clone())).await
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.metrics_catalog().clone())).await
},
|op| op
.id("get_metrics_tree")
@@ -66,10 +67,11 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
"/api/metrics/count",
get_with(
async |
uri: Uri,
headers: HeaderMap,
State(state): State<AppState>
| {
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.metric_count())).await
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.metric_count())).await
},
|op| op
.id("get_metrics_count")
@@ -84,10 +86,11 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
"/api/metrics/indexes",
get_with(
async |
uri: Uri,
headers: HeaderMap,
State(state): State<AppState>
| {
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.indexes().to_vec())).await
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.indexes().to_vec())).await
},
|op| op
.id("get_indexes")
@@ -104,11 +107,12 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
"/api/metrics/list",
get_with(
async |
uri: Uri,
headers: HeaderMap,
State(state): State<AppState>,
Query(pagination): Query<Pagination>
| {
state.cached_json(&headers, CacheStrategy::Static, move |q| Ok(q.metrics(pagination))).await
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| Ok(q.metrics(pagination))).await
},
|op| op
.id("list_metrics")
@@ -123,12 +127,13 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
"/api/metrics/search/{metric}",
get_with(
async |
uri: Uri,
headers: HeaderMap,
State(state): State<AppState>,
Path(path): Path<MetricParam>,
Query(query): Query<LimitParam>
| {
state.cached_json(&headers, CacheStrategy::Static, move |q| Ok(q.match_metric(&path.metric, query.limit))).await
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| Ok(q.match_metric(&path.metric, query.limit))).await
},
|op| op
.id("search_metrics")
@@ -144,11 +149,12 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
"/api/metric/{metric}",
get_with(
async |
uri: Uri,
headers: HeaderMap,
State(state): State<AppState>,
Path(path): Path<MetricParam>
| {
state.cached_json(&headers, CacheStrategy::Static, move |q| {
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| {
if let Some(indexes) = q.metric_to_indexes(path.metric.clone()) {
return Ok(indexes.clone())
}
@@ -291,5 +297,83 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
.not_modified(),
),
)
// Cost basis distribution endpoints
.api_route(
"/api/metrics/cost-basis",
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state
.cached_json(&headers, CacheStrategy::Static, &uri, |q| q.cost_basis_cohorts())
.await
},
|op| {
op.id("get_cost_basis_cohorts")
.metrics_tag()
.summary("Available cost basis cohorts")
.description("List available cohorts for cost basis distribution.")
.ok_response::<Vec<String>>()
.server_error()
},
),
)
.api_route(
"/api/metrics/cost-basis/{cohort}/dates",
get_with(
async |uri: Uri,
headers: HeaderMap,
Path(params): Path<CostBasisCohortParam>,
State(state): State<AppState>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
q.cost_basis_dates(&params.cohort)
})
.await
},
|op| {
op.id("get_cost_basis_dates")
.metrics_tag()
.summary("Available cost basis dates")
.description("List available dates for a cohort's cost basis distribution.")
.ok_response::<Vec<Date>>()
.not_found()
.server_error()
},
),
)
.api_route(
"/api/metrics/cost-basis/{cohort}/{date}",
get_with(
async |uri: Uri,
headers: HeaderMap,
Path(params): Path<CostBasisParams>,
Query(query): Query<CostBasisQuery>,
State(state): State<AppState>| {
state
.cached_json(&headers, CacheStrategy::Static, &uri, move |q| {
q.cost_basis_formatted(
&params.cohort,
params.date,
query.bucket,
query.value,
)
})
.await
},
|op| {
op.id("get_cost_basis")
.metrics_tag()
.summary("Cost basis distribution")
.description(
"Get the cost basis distribution for a cohort on a specific date.\n\n\
Query params:\n\
- `bucket`: raw (default), lin200, lin500, lin1000, log10, log50, log100\n\
- `value`: supply (default, in BTC), realized (USD), unrealized (USD)",
)
.ok_response::<CostBasisFormatted>()
.not_found()
.server_error()
},
),
)
}
}
+25 -25
View File
@@ -1,7 +1,7 @@
use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::{Path, State},
http::HeaderMap,
http::{HeaderMap, Uri},
response::Redirect,
routing::get,
};
@@ -28,8 +28,8 @@ impl MiningRoutes for ApiRouter<AppState> {
.api_route(
"/api/v1/difficulty-adjustment",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, |q| q.difficulty_adjustment()).await
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, |q| q.difficulty_adjustment()).await
},
|op| {
op.id("get_difficulty_adjustment")
@@ -45,9 +45,9 @@ impl MiningRoutes for ApiRouter<AppState> {
.api_route(
"/api/v1/mining/pools",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
// Pool list is static, only changes on code update
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.all_pools())).await
state.cached_json(&headers, CacheStrategy::Static, &uri, |q| Ok(q.all_pools())).await
},
|op| {
op.id("get_pools")
@@ -63,8 +63,8 @@ impl MiningRoutes for ApiRouter<AppState> {
.api_route(
"/api/v1/mining/pools/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.mining_pools(path.time_period)).await
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.mining_pools(path.time_period)).await
},
|op| {
op.id("get_pool_stats")
@@ -80,8 +80,8 @@ impl MiningRoutes for ApiRouter<AppState> {
.api_route(
"/api/v1/mining/pool/{slug}",
get_with(
async |headers: HeaderMap, Path(path): Path<PoolSlugParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.pool_detail(path.slug)).await
async |uri: Uri, headers: HeaderMap, Path(path): Path<PoolSlugParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.pool_detail(path.slug)).await
},
|op| {
op.id("get_pool")
@@ -98,8 +98,8 @@ impl MiningRoutes for ApiRouter<AppState> {
.api_route(
"/api/v1/mining/hashrate",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, |q| q.hashrate(None)).await
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, |q| q.hashrate(None)).await
},
|op| {
op.id("get_hashrate")
@@ -115,8 +115,8 @@ impl MiningRoutes for ApiRouter<AppState> {
.api_route(
"/api/v1/mining/hashrate/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.hashrate(Some(path.time_period))).await
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.hashrate(Some(path.time_period))).await
},
|op| {
op.id("get_hashrate_by_period")
@@ -132,8 +132,8 @@ impl MiningRoutes for ApiRouter<AppState> {
.api_route(
"/api/v1/mining/difficulty-adjustments",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, |q| q.difficulty_adjustments(None)).await
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, |q| q.difficulty_adjustments(None)).await
},
|op| {
op.id("get_difficulty_adjustments")
@@ -149,8 +149,8 @@ impl MiningRoutes for ApiRouter<AppState> {
.api_route(
"/api/v1/mining/difficulty-adjustments/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.difficulty_adjustments(Some(path.time_period))).await
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.difficulty_adjustments(Some(path.time_period))).await
},
|op| {
op.id("get_difficulty_adjustments_by_period")
@@ -166,8 +166,8 @@ impl MiningRoutes for ApiRouter<AppState> {
.api_route(
"/api/v1/mining/blocks/fees/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_fees(path.time_period)).await
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_fees(path.time_period)).await
},
|op| {
op.id("get_block_fees")
@@ -183,8 +183,8 @@ impl MiningRoutes for ApiRouter<AppState> {
.api_route(
"/api/v1/mining/blocks/rewards/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_rewards(path.time_period)).await
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_rewards(path.time_period)).await
},
|op| {
op.id("get_block_rewards")
@@ -218,8 +218,8 @@ impl MiningRoutes for ApiRouter<AppState> {
.api_route(
"/api/v1/mining/blocks/sizes-weights/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_sizes_weights(path.time_period)).await
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_sizes_weights(path.time_period)).await
},
|op| {
op.id("get_block_sizes_weights")
@@ -235,8 +235,8 @@ impl MiningRoutes for ApiRouter<AppState> {
.api_route(
"/api/v1/mining/reward-stats/{block_count}",
get_with(
async |headers: HeaderMap, Path(path): Path<BlockCountParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.reward_stats(path.block_count)).await
async |uri: Uri, headers: HeaderMap, Path(path): Path<BlockCountParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.reward_stats(path.block_count)).await
},
|op| {
op.id("get_reward_stats")
+7 -7
View File
@@ -1,7 +1,7 @@
use std::{borrow::Cow, fs, path};
use aide::axum::{ApiRouter, routing::get_with};
use axum::{extract::State, http::HeaderMap};
use axum::{extract::State, http::{HeaderMap, Uri}};
use brk_types::{DiskUsage, Health, Height, SyncStatus};
use vecdb::GenericStoredVec;
@@ -18,11 +18,11 @@ impl ServerRoutes for ApiRouter<AppState> {
self.api_route(
"/api/server/sync",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
let tip_height = state.client.get_last_height();
state
.cached_json(&headers, CacheStrategy::Height, move |q| {
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
let indexed_height = q.height();
let tip_height = tip_height?;
let blocks_behind = Height::from(tip_height.saturating_sub(*indexed_height));
@@ -59,10 +59,10 @@ impl ServerRoutes for ApiRouter<AppState> {
.api_route(
"/api/server/disk",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
let brk_path = state.data_path.clone();
state
.cached_json(&headers, CacheStrategy::Height, move |q| {
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
let brk_bytes = dir_size(&brk_path)?;
let bitcoin_bytes = dir_size(q.blocks_dir())?;
Ok(DiskUsage::new(brk_bytes, bitcoin_bytes))
@@ -106,9 +106,9 @@ impl ServerRoutes for ApiRouter<AppState> {
.api_route(
"/version",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state
.cached_json(&headers, CacheStrategy::Static, |_| {
.cached_json(&headers, CacheStrategy::Static, &uri, |_| {
Ok(env!("CARGO_PKG_VERSION"))
})
.await
+11 -6
View File
@@ -1,7 +1,7 @@
use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::{Path, State},
http::HeaderMap,
http::{HeaderMap, Uri},
response::Redirect,
routing::get,
};
@@ -24,11 +24,12 @@ impl TxRoutes for ApiRouter<AppState> {
"/api/tx/{txid}",
get_with(
async |
uri: Uri,
headers: HeaderMap,
Path(txid): Path<TxidParam>,
State(state): State<AppState>
| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.transaction(txid)).await
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.transaction(txid)).await
},
|op| op
.id("get_tx")
@@ -48,11 +49,12 @@ impl TxRoutes for ApiRouter<AppState> {
"/api/tx/{txid}/status",
get_with(
async |
uri: Uri,
headers: HeaderMap,
Path(txid): Path<TxidParam>,
State(state): State<AppState>
| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.transaction_status(txid)).await
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.transaction_status(txid)).await
},
|op| op
.id("get_tx_status")
@@ -72,11 +74,12 @@ impl TxRoutes for ApiRouter<AppState> {
"/api/tx/{txid}/hex",
get_with(
async |
uri: Uri,
headers: HeaderMap,
Path(txid): Path<TxidParam>,
State(state): State<AppState>
| {
state.cached_text(&headers, CacheStrategy::Height, move |q| q.transaction_hex(txid)).await
state.cached_text(&headers, CacheStrategy::Height, &uri, move |q| q.transaction_hex(txid)).await
},
|op| op
.id("get_tx_hex")
@@ -96,12 +99,13 @@ impl TxRoutes for ApiRouter<AppState> {
"/api/tx/{txid}/outspend/{vout}",
get_with(
async |
uri: Uri,
headers: HeaderMap,
Path(path): Path<TxidVout>,
State(state): State<AppState>
| {
let txid = TxidParam { txid: path.txid };
state.cached_json(&headers, CacheStrategy::Height, move |q| q.outspend(txid, path.vout)).await
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.outspend(txid, path.vout)).await
},
|op| op
.id("get_tx_outspend")
@@ -121,11 +125,12 @@ impl TxRoutes for ApiRouter<AppState> {
"/api/tx/{txid}/outspends",
get_with(
async |
uri: Uri,
headers: HeaderMap,
Path(txid): Path<TxidParam>,
State(state): State<AppState>
| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.outspends(txid)).await
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.outspends(txid)).await
},
|op| op
.id("get_tx_outspends")

Some files were not shown because too many files have changed in this diff Show More