global: snapshot

This commit is contained in:
nym21
2026-01-16 23:49:49 +01:00
parent 3b00a92fa4
commit 6bb1a2a311
34 changed files with 2600 additions and 5071 deletions
+3 -3
View File
@@ -13,10 +13,10 @@ fn load_catalog() -> TreeNode {
serde_json::from_str(&catalog_json).expect("Failed to parse catalog.json")
}
/// Load OpenAPI spec from api.json.
/// Load OpenAPI spec from openapi.json.
fn load_openapi_json() -> String {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/api.json");
std::fs::read_to_string(path).expect("Failed to read api.json")
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/openapi.json");
std::fs::read_to_string(path).expect("Failed to read openapi.json")
}
/// Load metadata from the catalog.
+3 -6
View File
@@ -11,7 +11,6 @@ use serde::{Deserialize, Deserializer, Serialize};
use crate::{default_brk_path, dot_brk_path, fix_user_path, website::Website};
#[derive(Parser, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
#[command(version, about)]
pub struct Config {
@@ -40,9 +39,9 @@ pub struct Config {
#[arg(long, value_name = "BOOL")]
exchanges: Option<bool>,
/// Website served by the server, default: default, saved
/// Website served by the server: true (default), false, or PATH, saved
#[serde(default, deserialize_with = "default_on_error")]
#[arg(short, long)]
#[arg(short, long, value_name = "BOOL|PATH")]
website: Option<Website>,
/// Bitcoin RPC ip, default: localhost, saved
@@ -232,9 +231,7 @@ Finally, you can run the program with '-h' for help."
pub fn bitcoindir(&self) -> PathBuf {
self.bitcoindir
.as_ref()
.map_or_else(Client::default_bitcoin_path, |s| {
fix_user_path(s.as_ref())
})
.map_or_else(Client::default_bitcoin_path, |s| fix_user_path(s.as_ref()))
}
pub fn blocksdir(&self) -> PathBuf {
+10 -17
View File
@@ -2,7 +2,6 @@
use std::{
fs,
path::PathBuf,
thread::{self, sleep},
time::Duration,
};
@@ -68,14 +67,17 @@ pub fn run() -> color_eyre::Result<()> {
let data_path = config.brkdir();
let website_source = match config.website() {
Website::Enabled(false) => WebsiteSource::Disabled,
Website::Path(p) => WebsiteSource::Filesystem(p),
Website::Enabled(false) => {
info!("Website: disabled");
WebsiteSource::Disabled
}
Website::Path(p) => {
info!("Website: filesystem ({})", p.display());
WebsiteSource::Filesystem(p)
}
Website::Enabled(true) => {
// Prefer local filesystem if available, otherwise use embedded
match find_local_website_dir() {
Some(path) => WebsiteSource::Filesystem(path),
None => WebsiteSource::Embedded,
}
info!("Website: embedded");
WebsiteSource::Embedded
}
};
@@ -119,12 +121,3 @@ pub fn run() -> color_eyre::Result<()> {
}
}
}
/// Path to website directory relative to this crate (only valid at dev machine)
const DEV_WEBSITE_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../website");
/// Returns local website path if it exists (dev mode)
fn find_local_website_dir() -> Option<PathBuf> {
let path = PathBuf::from(DEV_WEBSITE_DIR);
path.exists().then_some(path)
}
-1
View File
@@ -21,7 +21,6 @@ impl Default for Website {
}
}
impl FromStr for Website {
type Err = std::convert::Infallible;
+328 -300
View File
@@ -1268,56 +1268,6 @@ impl Price111dSmaPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct PercentilesPattern {
pub pct05: MetricPattern4<Dollars>,
pub pct10: MetricPattern4<Dollars>,
pub pct15: MetricPattern4<Dollars>,
pub pct20: MetricPattern4<Dollars>,
pub pct25: MetricPattern4<Dollars>,
pub pct30: MetricPattern4<Dollars>,
pub pct35: MetricPattern4<Dollars>,
pub pct40: MetricPattern4<Dollars>,
pub pct45: MetricPattern4<Dollars>,
pub pct50: MetricPattern4<Dollars>,
pub pct55: MetricPattern4<Dollars>,
pub pct60: MetricPattern4<Dollars>,
pub pct65: MetricPattern4<Dollars>,
pub pct70: MetricPattern4<Dollars>,
pub pct75: MetricPattern4<Dollars>,
pub pct80: MetricPattern4<Dollars>,
pub pct85: MetricPattern4<Dollars>,
pub pct90: MetricPattern4<Dollars>,
pub pct95: MetricPattern4<Dollars>,
}
impl PercentilesPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
pct05: MetricPattern4::new(client.clone(), _m(&acc, "pct05")),
pct10: MetricPattern4::new(client.clone(), _m(&acc, "pct10")),
pct15: MetricPattern4::new(client.clone(), _m(&acc, "pct15")),
pct20: MetricPattern4::new(client.clone(), _m(&acc, "pct20")),
pct25: MetricPattern4::new(client.clone(), _m(&acc, "pct25")),
pct30: MetricPattern4::new(client.clone(), _m(&acc, "pct30")),
pct35: MetricPattern4::new(client.clone(), _m(&acc, "pct35")),
pct40: MetricPattern4::new(client.clone(), _m(&acc, "pct40")),
pct45: MetricPattern4::new(client.clone(), _m(&acc, "pct45")),
pct50: MetricPattern4::new(client.clone(), _m(&acc, "pct50")),
pct55: MetricPattern4::new(client.clone(), _m(&acc, "pct55")),
pct60: MetricPattern4::new(client.clone(), _m(&acc, "pct60")),
pct65: MetricPattern4::new(client.clone(), _m(&acc, "pct65")),
pct70: MetricPattern4::new(client.clone(), _m(&acc, "pct70")),
pct75: MetricPattern4::new(client.clone(), _m(&acc, "pct75")),
pct80: MetricPattern4::new(client.clone(), _m(&acc, "pct80")),
pct85: MetricPattern4::new(client.clone(), _m(&acc, "pct85")),
pct90: MetricPattern4::new(client.clone(), _m(&acc, "pct90")),
pct95: MetricPattern4::new(client.clone(), _m(&acc, "pct95")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct ActivePriceRatioPattern {
pub ratio: MetricPattern4<StoredF32>,
@@ -1368,6 +1318,56 @@ impl ActivePriceRatioPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct PercentilesPattern {
pub pct05: MetricPattern4<Dollars>,
pub pct10: MetricPattern4<Dollars>,
pub pct15: MetricPattern4<Dollars>,
pub pct20: MetricPattern4<Dollars>,
pub pct25: MetricPattern4<Dollars>,
pub pct30: MetricPattern4<Dollars>,
pub pct35: MetricPattern4<Dollars>,
pub pct40: MetricPattern4<Dollars>,
pub pct45: MetricPattern4<Dollars>,
pub pct50: MetricPattern4<Dollars>,
pub pct55: MetricPattern4<Dollars>,
pub pct60: MetricPattern4<Dollars>,
pub pct65: MetricPattern4<Dollars>,
pub pct70: MetricPattern4<Dollars>,
pub pct75: MetricPattern4<Dollars>,
pub pct80: MetricPattern4<Dollars>,
pub pct85: MetricPattern4<Dollars>,
pub pct90: MetricPattern4<Dollars>,
pub pct95: MetricPattern4<Dollars>,
}
impl PercentilesPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
pct05: MetricPattern4::new(client.clone(), _m(&acc, "pct05")),
pct10: MetricPattern4::new(client.clone(), _m(&acc, "pct10")),
pct15: MetricPattern4::new(client.clone(), _m(&acc, "pct15")),
pct20: MetricPattern4::new(client.clone(), _m(&acc, "pct20")),
pct25: MetricPattern4::new(client.clone(), _m(&acc, "pct25")),
pct30: MetricPattern4::new(client.clone(), _m(&acc, "pct30")),
pct35: MetricPattern4::new(client.clone(), _m(&acc, "pct35")),
pct40: MetricPattern4::new(client.clone(), _m(&acc, "pct40")),
pct45: MetricPattern4::new(client.clone(), _m(&acc, "pct45")),
pct50: MetricPattern4::new(client.clone(), _m(&acc, "pct50")),
pct55: MetricPattern4::new(client.clone(), _m(&acc, "pct55")),
pct60: MetricPattern4::new(client.clone(), _m(&acc, "pct60")),
pct65: MetricPattern4::new(client.clone(), _m(&acc, "pct65")),
pct70: MetricPattern4::new(client.clone(), _m(&acc, "pct70")),
pct75: MetricPattern4::new(client.clone(), _m(&acc, "pct75")),
pct80: MetricPattern4::new(client.clone(), _m(&acc, "pct80")),
pct85: MetricPattern4::new(client.clone(), _m(&acc, "pct85")),
pct90: MetricPattern4::new(client.clone(), _m(&acc, "pct90")),
pct95: MetricPattern4::new(client.clone(), _m(&acc, "pct95")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct RelativePattern5 {
pub neg_unrealized_loss_rel_to_market_cap: MetricPattern1<StoredF32>,
@@ -1668,38 +1668,6 @@ impl<T: DeserializeOwned> DollarsPattern<T> {
}
}
/// Pattern struct for repeated tree structure.
pub struct RelativePattern2 {
pub neg_unrealized_loss_rel_to_own_market_cap: MetricPattern1<StoredF32>,
pub neg_unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1<StoredF32>,
pub net_unrealized_pnl_rel_to_own_market_cap: MetricPattern1<StoredF32>,
pub net_unrealized_pnl_rel_to_own_total_unrealized_pnl: MetricPattern1<StoredF32>,
pub supply_in_loss_rel_to_own_supply: MetricPattern1<StoredF64>,
pub supply_in_profit_rel_to_own_supply: MetricPattern1<StoredF64>,
pub unrealized_loss_rel_to_own_market_cap: MetricPattern1<StoredF32>,
pub unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1<StoredF32>,
pub unrealized_profit_rel_to_own_market_cap: MetricPattern1<StoredF32>,
pub unrealized_profit_rel_to_own_total_unrealized_pnl: MetricPattern1<StoredF32>,
}
impl RelativePattern2 {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
neg_unrealized_loss_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "neg_unrealized_loss_rel_to_own_market_cap")),
neg_unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "neg_unrealized_loss_rel_to_own_total_unrealized_pnl")),
net_unrealized_pnl_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "net_unrealized_pnl_rel_to_own_market_cap")),
net_unrealized_pnl_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "net_unrealized_pnl_rel_to_own_total_unrealized_pnl")),
supply_in_loss_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "supply_in_loss_rel_to_own_supply")),
supply_in_profit_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "supply_in_profit_rel_to_own_supply")),
unrealized_loss_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_loss_rel_to_own_market_cap")),
unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_loss_rel_to_own_total_unrealized_pnl")),
unrealized_profit_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_profit_rel_to_own_market_cap")),
unrealized_profit_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_profit_rel_to_own_total_unrealized_pnl")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct RelativePattern {
pub neg_unrealized_loss_rel_to_market_cap: MetricPattern1<StoredF32>,
@@ -1732,6 +1700,38 @@ impl RelativePattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct RelativePattern2 {
pub neg_unrealized_loss_rel_to_own_market_cap: MetricPattern1<StoredF32>,
pub neg_unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1<StoredF32>,
pub net_unrealized_pnl_rel_to_own_market_cap: MetricPattern1<StoredF32>,
pub net_unrealized_pnl_rel_to_own_total_unrealized_pnl: MetricPattern1<StoredF32>,
pub supply_in_loss_rel_to_own_supply: MetricPattern1<StoredF64>,
pub supply_in_profit_rel_to_own_supply: MetricPattern1<StoredF64>,
pub unrealized_loss_rel_to_own_market_cap: MetricPattern1<StoredF32>,
pub unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1<StoredF32>,
pub unrealized_profit_rel_to_own_market_cap: MetricPattern1<StoredF32>,
pub unrealized_profit_rel_to_own_total_unrealized_pnl: MetricPattern1<StoredF32>,
}
impl RelativePattern2 {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
neg_unrealized_loss_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "neg_unrealized_loss_rel_to_own_market_cap")),
neg_unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "neg_unrealized_loss_rel_to_own_total_unrealized_pnl")),
net_unrealized_pnl_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "net_unrealized_pnl_rel_to_own_market_cap")),
net_unrealized_pnl_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "net_unrealized_pnl_rel_to_own_total_unrealized_pnl")),
supply_in_loss_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "supply_in_loss_rel_to_own_supply")),
supply_in_profit_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "supply_in_profit_rel_to_own_supply")),
unrealized_loss_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_loss_rel_to_own_market_cap")),
unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_loss_rel_to_own_total_unrealized_pnl")),
unrealized_profit_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_profit_rel_to_own_market_cap")),
unrealized_profit_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_profit_rel_to_own_total_unrealized_pnl")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct CountPattern2<T> {
pub average: MetricPattern1<T>,
@@ -1794,36 +1794,6 @@ impl AddrCountPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct FullnessPattern<T> {
pub average: MetricPattern2<T>,
pub base: MetricPattern11<T>,
pub max: MetricPattern2<T>,
pub median: MetricPattern6<T>,
pub min: MetricPattern2<T>,
pub pct10: MetricPattern6<T>,
pub pct25: MetricPattern6<T>,
pub pct75: MetricPattern6<T>,
pub pct90: MetricPattern6<T>,
}
impl<T: DeserializeOwned> FullnessPattern<T> {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
average: MetricPattern2::new(client.clone(), _m(&acc, "average")),
base: MetricPattern11::new(client.clone(), acc.clone()),
max: MetricPattern2::new(client.clone(), _m(&acc, "max")),
median: MetricPattern6::new(client.clone(), _m(&acc, "median")),
min: MetricPattern2::new(client.clone(), _m(&acc, "min")),
pct10: MetricPattern6::new(client.clone(), _m(&acc, "pct10")),
pct25: MetricPattern6::new(client.clone(), _m(&acc, "pct25")),
pct75: MetricPattern6::new(client.clone(), _m(&acc, "pct75")),
pct90: MetricPattern6::new(client.clone(), _m(&acc, "pct90")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct FeeRatePattern<T> {
pub average: MetricPattern1<T>,
@@ -1854,6 +1824,36 @@ impl<T: DeserializeOwned> FeeRatePattern<T> {
}
}
/// Pattern struct for repeated tree structure.
pub struct FullnessPattern<T> {
pub average: MetricPattern2<T>,
pub base: MetricPattern11<T>,
pub max: MetricPattern2<T>,
pub median: MetricPattern6<T>,
pub min: MetricPattern2<T>,
pub pct10: MetricPattern6<T>,
pub pct25: MetricPattern6<T>,
pub pct75: MetricPattern6<T>,
pub pct90: MetricPattern6<T>,
}
impl<T: DeserializeOwned> FullnessPattern<T> {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
average: MetricPattern2::new(client.clone(), _m(&acc, "average")),
base: MetricPattern11::new(client.clone(), acc.clone()),
max: MetricPattern2::new(client.clone(), _m(&acc, "max")),
median: MetricPattern6::new(client.clone(), _m(&acc, "median")),
min: MetricPattern2::new(client.clone(), _m(&acc, "min")),
pct10: MetricPattern6::new(client.clone(), _m(&acc, "pct10")),
pct25: MetricPattern6::new(client.clone(), _m(&acc, "pct25")),
pct75: MetricPattern6::new(client.clone(), _m(&acc, "pct75")),
pct90: MetricPattern6::new(client.clone(), _m(&acc, "pct90")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct _0satsPattern {
pub activity: ActivityPattern2,
@@ -1910,6 +1910,84 @@ impl<T: DeserializeOwned> PhaseDailyCentsPattern<T> {
}
}
/// Pattern struct for repeated tree structure.
pub struct PeriodCagrPattern {
pub _10y: MetricPattern4<StoredF32>,
pub _2y: MetricPattern4<StoredF32>,
pub _3y: MetricPattern4<StoredF32>,
pub _4y: MetricPattern4<StoredF32>,
pub _5y: MetricPattern4<StoredF32>,
pub _6y: MetricPattern4<StoredF32>,
pub _8y: MetricPattern4<StoredF32>,
}
impl PeriodCagrPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
_10y: MetricPattern4::new(client.clone(), _p("10y", &acc)),
_2y: MetricPattern4::new(client.clone(), _p("2y", &acc)),
_3y: MetricPattern4::new(client.clone(), _p("3y", &acc)),
_4y: MetricPattern4::new(client.clone(), _p("4y", &acc)),
_5y: MetricPattern4::new(client.clone(), _p("5y", &acc)),
_6y: MetricPattern4::new(client.clone(), _p("6y", &acc)),
_8y: MetricPattern4::new(client.clone(), _p("8y", &acc)),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct _0satsPattern2 {
pub activity: ActivityPattern2,
pub cost_basis: CostBasisPattern,
pub outputs: OutputsPattern,
pub realized: RealizedPattern,
pub relative: RelativePattern4,
pub supply: SupplyPattern2,
pub unrealized: UnrealizedPattern,
}
impl _0satsPattern2 {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
activity: ActivityPattern2::new(client.clone(), acc.clone()),
cost_basis: CostBasisPattern::new(client.clone(), acc.clone()),
outputs: OutputsPattern::new(client.clone(), _m(&acc, "utxo_count")),
realized: RealizedPattern::new(client.clone(), acc.clone()),
relative: RelativePattern4::new(client.clone(), _m(&acc, "supply_in")),
supply: SupplyPattern2::new(client.clone(), _m(&acc, "supply")),
unrealized: UnrealizedPattern::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct _100btcPattern {
pub activity: ActivityPattern2,
pub cost_basis: CostBasisPattern,
pub outputs: OutputsPattern,
pub realized: RealizedPattern,
pub relative: RelativePattern,
pub supply: SupplyPattern2,
pub unrealized: UnrealizedPattern,
}
impl _100btcPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
activity: ActivityPattern2::new(client.clone(), acc.clone()),
cost_basis: CostBasisPattern::new(client.clone(), acc.clone()),
outputs: OutputsPattern::new(client.clone(), _m(&acc, "utxo_count")),
realized: RealizedPattern::new(client.clone(), acc.clone()),
relative: RelativePattern::new(client.clone(), acc.clone()),
supply: SupplyPattern2::new(client.clone(), _m(&acc, "supply")),
unrealized: UnrealizedPattern::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct UnrealizedPattern {
pub neg_unrealized_loss: MetricPattern1<Dollars>,
@@ -1962,32 +2040,6 @@ impl _10yTo12yPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct PeriodCagrPattern {
pub _10y: MetricPattern4<StoredF32>,
pub _2y: MetricPattern4<StoredF32>,
pub _3y: MetricPattern4<StoredF32>,
pub _4y: MetricPattern4<StoredF32>,
pub _5y: MetricPattern4<StoredF32>,
pub _6y: MetricPattern4<StoredF32>,
pub _8y: MetricPattern4<StoredF32>,
}
impl PeriodCagrPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
_10y: MetricPattern4::new(client.clone(), _p("10y", &acc)),
_2y: MetricPattern4::new(client.clone(), _p("2y", &acc)),
_3y: MetricPattern4::new(client.clone(), _p("3y", &acc)),
_4y: MetricPattern4::new(client.clone(), _p("4y", &acc)),
_5y: MetricPattern4::new(client.clone(), _p("5y", &acc)),
_6y: MetricPattern4::new(client.clone(), _p("6y", &acc)),
_8y: MetricPattern4::new(client.clone(), _p("8y", &acc)),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct _10yPattern {
pub activity: ActivityPattern2,
@@ -2014,58 +2066,6 @@ impl _10yPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct _100btcPattern {
pub activity: ActivityPattern2,
pub cost_basis: CostBasisPattern,
pub outputs: OutputsPattern,
pub realized: RealizedPattern,
pub relative: RelativePattern,
pub supply: SupplyPattern2,
pub unrealized: UnrealizedPattern,
}
impl _100btcPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
activity: ActivityPattern2::new(client.clone(), acc.clone()),
cost_basis: CostBasisPattern::new(client.clone(), acc.clone()),
outputs: OutputsPattern::new(client.clone(), _m(&acc, "utxo_count")),
realized: RealizedPattern::new(client.clone(), acc.clone()),
relative: RelativePattern::new(client.clone(), acc.clone()),
supply: SupplyPattern2::new(client.clone(), _m(&acc, "supply")),
unrealized: UnrealizedPattern::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct _0satsPattern2 {
pub activity: ActivityPattern2,
pub cost_basis: CostBasisPattern,
pub outputs: OutputsPattern,
pub realized: RealizedPattern,
pub relative: RelativePattern4,
pub supply: SupplyPattern2,
pub unrealized: UnrealizedPattern,
}
impl _0satsPattern2 {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
activity: ActivityPattern2::new(client.clone(), acc.clone()),
cost_basis: CostBasisPattern::new(client.clone(), acc.clone()),
outputs: OutputsPattern::new(client.clone(), _m(&acc, "utxo_count")),
realized: RealizedPattern::new(client.clone(), acc.clone()),
relative: RelativePattern4::new(client.clone(), _m(&acc, "supply_in")),
supply: SupplyPattern2::new(client.clone(), _m(&acc, "supply")),
unrealized: UnrealizedPattern::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct ActivityPattern2 {
pub coinblocks_destroyed: BlockCountPattern<StoredF64>,
@@ -2108,24 +2108,6 @@ impl<T: DeserializeOwned> SplitPattern2<T> {
}
}
/// Pattern struct for repeated tree structure.
pub struct _2015Pattern {
pub bitcoin: MetricPattern4<Bitcoin>,
pub dollars: MetricPattern4<Dollars>,
pub sats: MetricPattern4<Sats>,
}
impl _2015Pattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
bitcoin: MetricPattern4::new(client.clone(), _m(&acc, "btc")),
dollars: MetricPattern4::new(client.clone(), _m(&acc, "usd")),
sats: MetricPattern4::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct ActiveSupplyPattern {
pub bitcoin: MetricPattern1<Bitcoin>,
@@ -2145,19 +2127,19 @@ impl ActiveSupplyPattern {
}
/// Pattern struct for repeated tree structure.
pub struct CostBasisPattern2 {
pub max: MetricPattern1<Dollars>,
pub min: MetricPattern1<Dollars>,
pub percentiles: PercentilesPattern,
pub struct _2015Pattern {
pub bitcoin: MetricPattern4<Bitcoin>,
pub dollars: MetricPattern4<Dollars>,
pub sats: MetricPattern4<Sats>,
}
impl CostBasisPattern2 {
impl _2015Pattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
max: MetricPattern1::new(client.clone(), _m(&acc, "max_cost_basis")),
min: MetricPattern1::new(client.clone(), _m(&acc, "min_cost_basis")),
percentiles: PercentilesPattern::new(client.clone(), _m(&acc, "cost_basis")),
bitcoin: MetricPattern4::new(client.clone(), _m(&acc, "btc")),
dollars: MetricPattern4::new(client.clone(), _m(&acc, "usd")),
sats: MetricPattern4::new(client.clone(), acc.clone()),
}
}
}
@@ -2180,6 +2162,24 @@ impl CoinbasePattern2 {
}
}
/// Pattern struct for repeated tree structure.
pub struct CostBasisPattern2 {
pub max: MetricPattern1<Dollars>,
pub min: MetricPattern1<Dollars>,
pub percentiles: PercentilesPattern,
}
impl CostBasisPattern2 {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
max: MetricPattern1::new(client.clone(), _m(&acc, "max_cost_basis")),
min: MetricPattern1::new(client.clone(), _m(&acc, "min_cost_basis")),
percentiles: PercentilesPattern::new(client.clone(), _m(&acc, "cost_basis")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct SegwitAdoptionPattern {
pub base: MetricPattern11<StoredF32>,
@@ -2198,24 +2198,6 @@ impl SegwitAdoptionPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct CoinbasePattern {
pub bitcoin: BitcoinPattern,
pub dollars: DollarsPattern<Dollars>,
pub sats: DollarsPattern<Sats>,
}
impl CoinbasePattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
bitcoin: BitcoinPattern::new(client.clone(), _m(&acc, "btc")),
dollars: DollarsPattern::new(client.clone(), _m(&acc, "usd")),
sats: DollarsPattern::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct UnclaimedRewardsPattern {
pub bitcoin: BitcoinPattern2<Bitcoin>,
@@ -2235,17 +2217,19 @@ impl UnclaimedRewardsPattern {
}
/// Pattern struct for repeated tree structure.
pub struct SupplyPattern2 {
pub halved: ActiveSupplyPattern,
pub total: ActiveSupplyPattern,
pub struct CoinbasePattern {
pub bitcoin: BitcoinPattern,
pub dollars: DollarsPattern<Dollars>,
pub sats: DollarsPattern<Sats>,
}
impl SupplyPattern2 {
impl CoinbasePattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
halved: ActiveSupplyPattern::new(client.clone(), _m(&acc, "halved")),
total: ActiveSupplyPattern::new(client.clone(), acc.clone()),
bitcoin: BitcoinPattern::new(client.clone(), _m(&acc, "btc")),
dollars: DollarsPattern::new(client.clone(), _m(&acc, "usd")),
sats: DollarsPattern::new(client.clone(), acc.clone()),
}
}
}
@@ -2266,22 +2250,6 @@ impl RelativePattern4 {
}
}
/// Pattern struct for repeated tree structure.
pub struct CostBasisPattern {
pub max: MetricPattern1<Dollars>,
pub min: MetricPattern1<Dollars>,
}
impl CostBasisPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
max: MetricPattern1::new(client.clone(), _m(&acc, "max_cost_basis")),
min: MetricPattern1::new(client.clone(), _m(&acc, "min_cost_basis")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct _1dReturns1mSdPattern {
pub sd: MetricPattern4<StoredF32>,
@@ -2299,17 +2267,33 @@ impl _1dReturns1mSdPattern {
}
/// Pattern struct for repeated tree structure.
pub struct BitcoinPattern2<T> {
pub cumulative: MetricPattern2<T>,
pub sum: MetricPattern1<T>,
pub struct CostBasisPattern {
pub max: MetricPattern1<Dollars>,
pub min: MetricPattern1<Dollars>,
}
impl<T: DeserializeOwned> BitcoinPattern2<T> {
impl CostBasisPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
cumulative: MetricPattern2::new(client.clone(), _m(&acc, "cumulative")),
sum: MetricPattern1::new(client.clone(), acc.clone()),
max: MetricPattern1::new(client.clone(), _m(&acc, "max_cost_basis")),
min: MetricPattern1::new(client.clone(), _m(&acc, "min_cost_basis")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct SupplyPattern2 {
pub halved: ActiveSupplyPattern,
pub total: ActiveSupplyPattern,
}
impl SupplyPattern2 {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
halved: ActiveSupplyPattern::new(client.clone(), _m(&acc, "halved")),
total: ActiveSupplyPattern::new(client.clone(), acc.clone()),
}
}
}
@@ -2330,6 +2314,22 @@ impl<T: DeserializeOwned> BlockCountPattern<T> {
}
}
/// Pattern struct for repeated tree structure.
pub struct BitcoinPattern2<T> {
pub cumulative: MetricPattern2<T>,
pub sum: MetricPattern1<T>,
}
impl<T: DeserializeOwned> BitcoinPattern2<T> {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
cumulative: MetricPattern2::new(client.clone(), _m(&acc, "cumulative")),
sum: MetricPattern1::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct SatsPattern<T> {
pub ohlc: MetricPattern1<T>,
@@ -2340,22 +2340,8 @@ impl<T: DeserializeOwned> SatsPattern<T> {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
ohlc: MetricPattern1::new(client.clone(), _m(&acc, "ohlc")),
split: SplitPattern2::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct RealizedPriceExtraPattern {
pub ratio: MetricPattern4<StoredF32>,
}
impl RealizedPriceExtraPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
ratio: MetricPattern4::new(client.clone(), acc.clone()),
ohlc: MetricPattern1::new(client.clone(), _m(&acc, "ohlc_sats")),
split: SplitPattern2::new(client.clone(), _m(&acc, "sats")),
}
}
}
@@ -2374,6 +2360,20 @@ impl OutputsPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct RealizedPriceExtraPattern {
pub ratio: MetricPattern4<StoredF32>,
}
impl RealizedPriceExtraPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
ratio: MetricPattern4::new(client.clone(), acc.clone()),
}
}
}
// Metrics tree
/// Metrics tree node.
@@ -4942,8 +4942,8 @@ impl MetricsTree_Positions {
pub struct MetricsTree_Price {
pub cents: MetricsTree_Price_Cents,
pub oracle: MetricsTree_Price_Oracle,
pub sats: MetricsTree_Price_Sats,
pub usd: SatsPattern<OHLCDollars>,
pub sats: SatsPattern<OHLCSats>,
pub usd: MetricsTree_Price_Usd,
}
impl MetricsTree_Price {
@@ -4951,8 +4951,8 @@ impl MetricsTree_Price {
Self {
cents: MetricsTree_Price_Cents::new(client.clone(), format!("{base_path}_cents")),
oracle: MetricsTree_Price_Oracle::new(client.clone(), format!("{base_path}_oracle")),
sats: MetricsTree_Price_Sats::new(client.clone(), format!("{base_path}_sats")),
usd: SatsPattern::new(client.clone(), "price".to_string()),
sats: SatsPattern::new(client.clone(), "price".to_string()),
usd: MetricsTree_Price_Usd::new(client.clone(), format!("{base_path}_usd")),
}
}
}
@@ -5003,6 +5003,20 @@ pub struct MetricsTree_Price_Oracle {
pub phase_daily_dollars: PhaseDailyCentsPattern<Dollars>,
pub phase_histogram: MetricPattern11<OracleBins>,
pub phase_price_cents: MetricPattern11<Cents>,
pub phase_v2_daily_cents: PhaseDailyCentsPattern<Cents>,
pub phase_v2_daily_dollars: PhaseDailyCentsPattern<Dollars>,
pub phase_v2_histogram: MetricPattern11<OracleBinsV2>,
pub phase_v2_peak_daily_cents: PhaseDailyCentsPattern<Cents>,
pub phase_v2_peak_daily_dollars: PhaseDailyCentsPattern<Dollars>,
pub phase_v2_peak_price_cents: MetricPattern11<Cents>,
pub phase_v2_price_cents: MetricPattern11<Cents>,
pub phase_v3_daily_cents: PhaseDailyCentsPattern<Cents>,
pub phase_v3_daily_dollars: PhaseDailyCentsPattern<Dollars>,
pub phase_v3_histogram: MetricPattern11<OracleBinsV2>,
pub phase_v3_peak_daily_cents: PhaseDailyCentsPattern<Cents>,
pub phase_v3_peak_daily_dollars: PhaseDailyCentsPattern<Dollars>,
pub phase_v3_peak_price_cents: MetricPattern11<Cents>,
pub phase_v3_price_cents: MetricPattern11<Cents>,
pub price_cents: MetricPattern11<Cents>,
pub tx_count: MetricPattern6<StoredU32>,
}
@@ -5020,6 +5034,20 @@ impl MetricsTree_Price_Oracle {
phase_daily_dollars: PhaseDailyCentsPattern::new(client.clone(), "phase_daily_dollars".to_string()),
phase_histogram: MetricPattern11::new(client.clone(), "phase_histogram".to_string()),
phase_price_cents: MetricPattern11::new(client.clone(), "phase_price_cents".to_string()),
phase_v2_daily_cents: PhaseDailyCentsPattern::new(client.clone(), "phase_v2_daily".to_string()),
phase_v2_daily_dollars: PhaseDailyCentsPattern::new(client.clone(), "phase_v2_daily_dollars".to_string()),
phase_v2_histogram: MetricPattern11::new(client.clone(), "phase_v2_histogram".to_string()),
phase_v2_peak_daily_cents: PhaseDailyCentsPattern::new(client.clone(), "phase_v2_peak_daily".to_string()),
phase_v2_peak_daily_dollars: PhaseDailyCentsPattern::new(client.clone(), "phase_v2_peak_daily_dollars".to_string()),
phase_v2_peak_price_cents: MetricPattern11::new(client.clone(), "phase_v2_peak_price_cents".to_string()),
phase_v2_price_cents: MetricPattern11::new(client.clone(), "phase_v2_price_cents".to_string()),
phase_v3_daily_cents: PhaseDailyCentsPattern::new(client.clone(), "phase_v3_daily".to_string()),
phase_v3_daily_dollars: PhaseDailyCentsPattern::new(client.clone(), "phase_v3_daily_dollars".to_string()),
phase_v3_histogram: MetricPattern11::new(client.clone(), "phase_v3_histogram".to_string()),
phase_v3_peak_daily_cents: PhaseDailyCentsPattern::new(client.clone(), "phase_v3_peak_daily".to_string()),
phase_v3_peak_daily_dollars: PhaseDailyCentsPattern::new(client.clone(), "phase_v3_peak_daily_dollars".to_string()),
phase_v3_peak_price_cents: MetricPattern11::new(client.clone(), "phase_v3_peak_price_cents".to_string()),
phase_v3_price_cents: MetricPattern11::new(client.clone(), "phase_v3_price_cents".to_string()),
price_cents: MetricPattern11::new(client.clone(), "oracle_price_cents".to_string()),
tx_count: MetricPattern6::new(client.clone(), "oracle_tx_count".to_string()),
}
@@ -5027,16 +5055,16 @@ impl MetricsTree_Price_Oracle {
}
/// Metrics tree node.
pub struct MetricsTree_Price_Sats {
pub ohlc: MetricPattern1<OHLCSats>,
pub split: SplitPattern2<Sats>,
pub struct MetricsTree_Price_Usd {
pub ohlc: MetricPattern1<OHLCDollars>,
pub split: SplitPattern2<Dollars>,
}
impl MetricsTree_Price_Sats {
impl MetricsTree_Price_Usd {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
ohlc: MetricPattern1::new(client.clone(), "price_ohlc_sats".to_string()),
split: SplitPattern2::new(client.clone(), "price_sats".to_string()),
ohlc: MetricPattern1::new(client.clone(), "price_ohlc".to_string()),
split: SplitPattern2::new(client.clone(), "price".to_string()),
}
}
}
@@ -5422,24 +5450,15 @@ impl BrkClient {
)
}
/// OpenAPI specification
/// Compact OpenAPI specification
///
/// Full OpenAPI 3.1 specification for this API.
/// Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information. Full spec available at `/openapi.json`.
///
/// Endpoint: `GET /api.json`
pub fn get_openapi(&self) -> Result<serde_json::Value> {
pub fn get_api(&self) -> Result<serde_json::Value> {
self.base.get_json(&format!("/api.json"))
}
/// Trimmed OpenAPI specification
///
/// Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information.
///
/// Endpoint: `GET /api.trimmed.json`
pub fn get_openapi_trimmed(&self) -> Result<serde_json::Value> {
self.base.get_json(&format!("/api.trimmed.json"))
}
/// Address information
///
/// Retrieve address information including balance and transaction counts. Supports all standard Bitcoin address types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR).
@@ -5999,6 +6018,15 @@ impl BrkClient {
self.base.get_json(&format!("/health"))
}
/// OpenAPI specification
///
/// Full OpenAPI 3.1 specification for this API.
///
/// Endpoint: `GET /openapi.json`
pub fn get_openapi(&self) -> Result<serde_json::Value> {
self.base.get_json(&format!("/openapi.json"))
}
/// API version
///
/// Returns the current version of the API server
+2 -1
View File
@@ -26,7 +26,8 @@ impl Vecs {
info!("Computing oracle prices...");
let i = Instant::now();
self.oracle.compute(indexer, indexes, starting_indexes, exit)?;
self.oracle
.compute(indexer, indexes, &self.cents, starting_indexes, exit)?;
info!("Computed oracle prices in {:?}", i.elapsed());
}
+925 -3
View File
@@ -53,8 +53,8 @@ use std::collections::VecDeque;
use brk_error::Result;
use brk_indexer::Indexer;
use brk_types::{
Cents, Close, Date, DateIndex, Height, High, Low, OHLCCents, Open, OracleBins, OutputType,
PHASE_BINS, PairOutputIndex, Sats, StoredU32, StoredU64, TxIndex,
Cents, Close, Date, DateIndex, Height, High, Low, OHLCCents, Open, OracleBins, OracleBinsV2,
OutputType, PHASE_BINS, PairOutputIndex, Sats, StoredU32, StoredU64, TxIndex,
};
use tracing::info;
use vecdb::{
@@ -66,9 +66,10 @@ use super::{
Vecs,
config::OracleConfig,
histogram::{Histogram, TOTAL_BINS},
phase_v2::{PhaseHistogramV2, find_best_phase, phase_range_from_anchor, phase_to_price},
stencil::{find_best_price, is_round_sats, refine_price},
};
use crate::{ComputeIndexes, indexes};
use crate::{ComputeIndexes, indexes, price::cents};
/// Flush interval for periodic writes during oracle computation.
const FLUSH_INTERVAL: usize = 10_000;
@@ -79,6 +80,7 @@ impl Vecs {
&mut self,
indexer: &Indexer,
indexes: &indexes::Vecs,
price_cents: &cents::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
@@ -100,6 +102,32 @@ impl Vecs {
// Step 7: Aggregate to daily OHLC
self.compute_daily_ohlc(indexes, starting_indexes, exit)?;
// Step 8: Compute Phase Oracle V2 (round USD template matching)
// 8a: Per-block 200-bin histograms (uses ALL outputs, not pair-filtered)
self.compute_phase_v2_histograms(indexer, indexes, starting_indexes, exit)?;
// 8b: Per-block prices using cross-correlation with weekly anchors
self.compute_phase_v2_prices(indexes, price_cents, starting_indexes, exit)?;
// 8c: Per-block prices using direct peak finding (like V1)
self.compute_phase_v2_peak_prices(indexes, price_cents, starting_indexes, exit)?;
// 8d: Daily distributions from per-block prices
self.compute_phase_v2_daily(indexes, starting_indexes, exit)?;
// Step 9: Compute Phase Oracle V3 (BASE + uniqueVal filter)
// 9a: Per-block histograms with uniqueVal filtering (only outputs with unique values in tx)
self.compute_phase_v3_histograms(indexer, indexes, starting_indexes, exit)?;
// 9b: Per-block prices using cross-correlation
self.compute_phase_v3_prices(indexes, price_cents, starting_indexes, exit)?;
// 9c: Per-block prices using direct peak finding (like V1)
self.compute_phase_v3_peak_prices(indexes, price_cents, starting_indexes, exit)?;
// 9d: Daily distributions from per-block prices
self.compute_phase_v3_daily(indexes, starting_indexes, exit)?;
Ok(())
}
@@ -1091,4 +1119,898 @@ impl Vecs {
Ok(())
}
/// Compute Phase Oracle V2 - Step 1: Per-block 200-bin phase histograms
///
/// Uses ALL outputs (like Python test), filtered only by sats range (1k-100k BTC).
/// This is different from the pair-filtered approach used by UTXOracle.
fn compute_phase_v2_histograms(
&mut self,
indexer: &Indexer,
indexes: &indexes::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
let source_version = indexer.vecs.outputs.value.version();
self.phase_v2_histogram
.validate_computed_version_or_reset(source_version)?;
let total_heights = indexer.vecs.blocks.timestamp.len();
let start_height = self
.phase_v2_histogram
.len()
.min(starting_indexes.height.to_usize());
self.phase_v2_histogram
.truncate_if_needed_at(start_height)?;
if start_height >= total_heights {
return Ok(());
}
info!(
"Computing phase V2 histograms from height {} to {}",
start_height, total_heights
);
let mut height_to_first_txindex_iter = indexer.vecs.transactions.first_txindex.into_iter();
let mut txindex_to_first_txoutindex_iter =
indexer.vecs.transactions.first_txoutindex.into_iter();
let mut txindex_to_output_count_iter = indexes.txindex.output_count.iter();
let mut txoutindex_to_value_iter = indexer.vecs.outputs.value.into_iter();
let total_txs = indexer.vecs.transactions.height.len();
let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8;
for height in start_height..total_heights {
// Get transaction range for this block
let first_txindex = height_to_first_txindex_iter.get_at_unwrap(height);
let next_first_txindex = height_to_first_txindex_iter
.get_at(height + 1)
.unwrap_or(TxIndex::from(total_txs));
// Build phase histogram from ALL outputs in this block
let mut histogram = OracleBinsV2::ZERO;
for txindex in first_txindex.to_usize()..next_first_txindex.to_usize() {
// Get output count and first output for this transaction
let first_txoutindex = txindex_to_first_txoutindex_iter.get_at_unwrap(txindex);
let output_count: StoredU64 =
txindex_to_output_count_iter.get_unwrap(TxIndex::from(txindex));
for i in 0..*output_count as usize {
let txoutindex = first_txoutindex.to_usize() + i;
let sats: Sats = txoutindex_to_value_iter.get_at_unwrap(txoutindex);
// OracleBinsV2::add already filters by sats range (1k to 100k BTC)
histogram.add(sats);
}
}
self.phase_v2_histogram.push(histogram);
// Progress logging
let progress = (height * 100 / total_heights.max(1)) as u8;
if progress > last_progress {
last_progress = progress;
info!("Phase V2 histogram computation: {}%", progress);
let _lock = exit.lock();
self.phase_v2_histogram.write()?;
}
}
// Final write
{
let _lock = exit.lock();
self.phase_v2_histogram.write()?;
}
info!(
"Phase V2 histograms complete: {} blocks",
self.phase_v2_histogram.len()
);
Ok(())
}
/// Compute Phase Oracle V2 - Step 2: Per-block prices using cross-correlation
fn compute_phase_v2_prices(
&mut self,
indexes: &indexes::Vecs,
price_cents: &cents::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
let source_version = self.phase_v2_histogram.version();
self.phase_v2_price_cents
.validate_computed_version_or_reset(source_version)?;
let total_heights = self.phase_v2_histogram.len();
let start_height = self
.phase_v2_price_cents
.len()
.min(starting_indexes.height.to_usize());
self.phase_v2_price_cents
.truncate_if_needed_at(start_height)?;
if start_height >= total_heights {
return Ok(());
}
info!(
"Computing phase V2 prices from height {} to {}",
start_height, total_heights
);
let mut histogram_iter = self.phase_v2_histogram.iter()?;
let mut height_to_dateindex_iter = indexes.height.dateindex.iter();
// For weekly OHLC anchors
let mut price_ohlc_iter = price_cents.ohlc.dateindex.iter()?;
let mut dateindex_to_weekindex_iter = indexes.dateindex.weekindex.iter();
let mut weekindex_to_first_dateindex_iter = indexes.weekindex.first_dateindex.iter();
let mut weekindex_dateindex_count_iter = indexes.weekindex.dateindex_count.iter();
let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8;
// Track previous price for fallback
let mut prev_price_cents = if start_height > 0 {
self.phase_v2_price_cents
.iter()?
.get(Height::from(start_height - 1))
.unwrap_or(Cents::from(10_000_000i64))
} else {
Cents::from(10_000_000i64) // Default ~$100k
};
for height in start_height..total_heights {
let height_idx = Height::from(height);
let histogram: OracleBinsV2 = histogram_iter.get_unwrap(height_idx);
// Get weekly anchor for this block's date
let dateindex = height_to_dateindex_iter.get(height_idx);
let weekly_bounds: Option<(f64, f64)> = dateindex.and_then(|di| {
let wi = dateindex_to_weekindex_iter.get(di)?;
let first_di = weekindex_to_first_dateindex_iter.get(wi)?;
let count = weekindex_dateindex_count_iter
.get(wi)
.map(|c| *c as usize)?;
let mut low = Cents::from(i64::MAX);
let mut high = Cents::from(0i64);
for i in 0..count {
let di = DateIndex::from(first_di.to_usize() + i);
if let Some(ohlc) = price_ohlc_iter.get(di) {
if *ohlc.low < low {
low = *ohlc.low;
}
if *ohlc.high > high {
high = *ohlc.high;
}
}
}
if i64::from(low) > 0 && i64::from(high) > 0 {
Some((
i64::from(low) as f64 / 100.0,
i64::from(high) as f64 / 100.0,
))
} else {
None
}
});
// Compute price using cross-correlation
let price_cents = if histogram.total_count() >= 10 {
// Convert OracleBinsV2 to PhaseHistogramV2
let mut phase_hist = PhaseHistogramV2::new();
for (i, &count) in histogram.bins.iter().enumerate() {
if count > 0 {
let phase = (i as f64 + 0.5) / 200.0;
let log_sats = 6.0 + phase;
let sats = 10.0_f64.powf(log_sats);
for _ in 0..count {
phase_hist.add(Sats::from(sats as u64));
}
}
}
if let Some((low, high)) = weekly_bounds {
// Have weekly anchor - constrained search
let (phase_min, phase_max) = phase_range_from_anchor(low, high, 0.05);
let (best_phase, _corr) =
find_best_phase(&phase_hist, 2, Some(phase_min), Some(phase_max));
let price = phase_to_price(best_phase, low, high);
Cents::from((price * 100.0) as i64)
} else {
// No anchor - use previous price as reference
let anchor_low = (i64::from(prev_price_cents) as f64 / 100.0) * 0.5;
let anchor_high = (i64::from(prev_price_cents) as f64 / 100.0) * 2.0;
let (best_phase, _corr) = find_best_phase(&phase_hist, 2, None, None);
let price = phase_to_price(best_phase, anchor_low, anchor_high);
Cents::from((price * 100.0) as i64)
}
} else {
// Too few outputs - use previous price
prev_price_cents
};
prev_price_cents = price_cents;
self.phase_v2_price_cents.push(price_cents);
// Progress logging
let progress = (height * 100 / total_heights.max(1)) as u8;
if progress > last_progress {
last_progress = progress;
info!("Phase V2 price computation: {}%", progress);
let _lock = exit.lock();
self.phase_v2_price_cents.write()?;
}
}
// Final write
{
let _lock = exit.lock();
self.phase_v2_price_cents.write()?;
}
info!(
"Phase V2 prices complete: {} blocks",
self.phase_v2_price_cents.len()
);
Ok(())
}
/// Compute Phase Oracle V2 - Peak prices using direct peak finding (like V1)
fn compute_phase_v2_peak_prices(
&mut self,
indexes: &indexes::Vecs,
price_cents: &cents::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
let source_version = self.phase_v2_histogram.version();
self.phase_v2_peak_price_cents
.validate_computed_version_or_reset(source_version)?;
let total_heights = self.phase_v2_histogram.len();
let start_height = self
.phase_v2_peak_price_cents
.len()
.min(starting_indexes.height.to_usize());
self.phase_v2_peak_price_cents
.truncate_if_needed_at(start_height)?;
if start_height >= total_heights {
return Ok(());
}
info!(
"Computing phase V2 peak prices from height {} to {}",
start_height, total_heights
);
let mut histogram_iter = self.phase_v2_histogram.iter()?;
let mut height_to_dateindex_iter = indexes.height.dateindex.iter();
// For weekly OHLC anchors
let mut price_ohlc_iter = price_cents.ohlc.dateindex.iter()?;
let mut dateindex_to_weekindex_iter = indexes.dateindex.weekindex.iter();
let mut weekindex_to_first_dateindex_iter = indexes.weekindex.first_dateindex.iter();
let mut weekindex_dateindex_count_iter = indexes.weekindex.dateindex_count.iter();
let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8;
// Track previous price for fallback
let mut prev_price_cents = if start_height > 0 {
self.phase_v2_peak_price_cents
.iter()?
.get(Height::from(start_height - 1))
.unwrap_or(Cents::from(10_000_000i64))
} else {
Cents::from(10_000_000i64)
};
for height in start_height..total_heights {
let height_idx = Height::from(height);
let histogram: OracleBinsV2 = histogram_iter.get_unwrap(height_idx);
// Get weekly anchor for decade selection
let dateindex = height_to_dateindex_iter.get(height_idx);
let anchor_price: Option<f64> = dateindex.and_then(|di| {
let wi = dateindex_to_weekindex_iter.get(di)?;
let first_di = weekindex_to_first_dateindex_iter.get(wi)?;
let count = weekindex_dateindex_count_iter
.get(wi)
.map(|c| *c as usize)?;
let mut sum = 0i64;
let mut cnt = 0;
for i in 0..count {
let di = DateIndex::from(first_di.to_usize() + i);
if let Some(ohlc) = price_ohlc_iter.get(di) {
sum += i64::from(*ohlc.close);
cnt += 1;
}
}
if cnt > 0 {
Some(sum as f64 / cnt as f64 / 100.0)
} else {
None
}
});
// Use anchor or previous price for decade selection
let anchor = anchor_price.unwrap_or(i64::from(prev_price_cents) as f64 / 100.0);
// Find peak bin directly (like V1) using 100 bins (downsample from 200)
let price_cents = if histogram.total_count() >= 10 {
// Downsample 200 bins to 100 bins
let mut bins100 = [0u32; 100];
for i in 0..100 {
bins100[i] = histogram.bins[i * 2] as u32 + histogram.bins[i * 2 + 1] as u32;
}
// Find peak bin, skipping bin 0 (round BTC amounts cluster there)
let peak_bin = bins100
.iter()
.enumerate()
.filter(|(bin, _)| *bin != 0)
.max_by_key(|(_, count)| *count)
.map(|(bin, _)| bin)
.unwrap_or(0);
// Convert bin to price using anchor for decade (100 bins)
let phase = (peak_bin as f64 + 0.5) / 100.0;
let base_price = 10.0_f64.powf(phase);
// Find best decade
let mut best_price = base_price;
let mut best_dist = f64::MAX;
for decade in -2..=6 {
let candidate = base_price * 10.0_f64.powi(decade);
let dist = (candidate - anchor).abs();
if dist < best_dist {
best_dist = dist;
best_price = candidate;
}
}
Cents::from((best_price.clamp(0.01, 10_000_000.0) * 100.0) as i64)
} else {
prev_price_cents
};
prev_price_cents = price_cents;
self.phase_v2_peak_price_cents.push(price_cents);
// Progress logging
let progress = (height * 100 / total_heights.max(1)) as u8;
if progress > last_progress {
last_progress = progress;
info!("Phase V2 peak price computation: {}%", progress);
let _lock = exit.lock();
self.phase_v2_peak_price_cents.write()?;
}
}
// Final write
{
let _lock = exit.lock();
self.phase_v2_peak_price_cents.write()?;
}
info!(
"Phase V2 peak prices complete: {} blocks",
self.phase_v2_peak_price_cents.len()
);
Ok(())
}
/// Compute Phase Oracle V2 - Daily distributions from per-block prices
fn compute_phase_v2_daily(
&mut self,
indexes: &indexes::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
info!("Computing phase V2 daily distributions");
// Cross-correlation based
self.phase_v2_daily_cents.compute(
starting_indexes.dateindex,
&self.phase_v2_price_cents,
&indexes.dateindex.first_height,
&indexes.dateindex.height_count,
exit,
)?;
// Peak-based
self.phase_v2_peak_daily_cents.compute(
starting_indexes.dateindex,
&self.phase_v2_peak_price_cents,
&indexes.dateindex.first_height,
&indexes.dateindex.height_count,
exit,
)?;
info!(
"Phase V2 daily distributions complete: {} days",
self.phase_v2_daily_cents.len()
);
Ok(())
}
/// Compute Phase Oracle V3 - Step 1: Per-block histograms with uniqueVal filtering
///
/// Filters: >= 1000 sats, only outputs with unique values within their transaction.
/// This reduces spurious peaks from exchange batched payouts and inscription spam.
fn compute_phase_v3_histograms(
&mut self,
indexer: &Indexer,
indexes: &indexes::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
let source_version = indexer.vecs.outputs.value.version();
self.phase_v3_histogram
.validate_computed_version_or_reset(source_version)?;
let total_heights = indexer.vecs.blocks.timestamp.len();
let start_height = self
.phase_v3_histogram
.len()
.min(starting_indexes.height.to_usize());
self.phase_v3_histogram
.truncate_if_needed_at(start_height)?;
if start_height >= total_heights {
return Ok(());
}
info!(
"Computing phase V3 histograms from height {} to {}",
start_height, total_heights
);
let mut height_to_first_txindex_iter = indexer.vecs.transactions.first_txindex.into_iter();
let mut txindex_to_first_txoutindex_iter =
indexer.vecs.transactions.first_txoutindex.into_iter();
let mut txindex_to_output_count_iter = indexes.txindex.output_count.iter();
let mut txoutindex_to_value_iter = indexer.vecs.outputs.value.into_iter();
let total_txs = indexer.vecs.transactions.height.len();
let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8;
// Reusable buffer for collecting output values per transaction
let mut tx_values: Vec<Sats> = Vec::with_capacity(16);
for height in start_height..total_heights {
// Get transaction range for this block
let first_txindex = height_to_first_txindex_iter.get_at_unwrap(height);
let next_first_txindex = height_to_first_txindex_iter
.get_at(height + 1)
.unwrap_or(TxIndex::from(total_txs));
// Build phase histogram with uniqueVal filtering
let mut histogram = OracleBinsV2::ZERO;
// Skip coinbase (first tx in block)
for txindex in (first_txindex.to_usize() + 1)..next_first_txindex.to_usize() {
// Get output count and first output for this transaction
let first_txoutindex = txindex_to_first_txoutindex_iter.get_at_unwrap(txindex);
let output_count: StoredU64 =
txindex_to_output_count_iter.get_unwrap(TxIndex::from(txindex));
// Collect all output values for this transaction
tx_values.clear();
for i in 0..*output_count as usize {
let txoutindex = first_txoutindex.to_usize() + i;
let sats: Sats = txoutindex_to_value_iter.get_at_unwrap(txoutindex);
tx_values.push(sats);
}
// Count occurrences of each value to determine uniqueness
// For small output counts, simple nested loop is faster than HashMap
for (i, &sats) in tx_values.iter().enumerate() {
// Skip if below minimum (BASE filter: >= 1000 sats)
if sats < Sats::_1K {
continue;
}
// Check if this value is unique within the transaction
let mut is_unique = true;
for (j, &other_sats) in tx_values.iter().enumerate() {
if i != j && sats == other_sats {
is_unique = false;
break;
}
}
// Only add unique values to histogram
if is_unique {
histogram.add(sats);
}
}
}
self.phase_v3_histogram.push(histogram);
// Progress logging
let progress = (height * 100 / total_heights.max(1)) as u8;
if progress > last_progress {
last_progress = progress;
info!("Phase V3 histogram computation: {}%", progress);
let _lock = exit.lock();
self.phase_v3_histogram.write()?;
}
}
// Final write
{
let _lock = exit.lock();
self.phase_v3_histogram.write()?;
}
info!(
"Phase V3 histograms complete: {} blocks",
self.phase_v3_histogram.len()
);
Ok(())
}
/// Compute Phase Oracle V3 - Step 2: Per-block prices using cross-correlation
fn compute_phase_v3_prices(
&mut self,
indexes: &indexes::Vecs,
price_cents: &cents::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
let source_version = self.phase_v3_histogram.version();
self.phase_v3_price_cents
.validate_computed_version_or_reset(source_version)?;
let total_heights = self.phase_v3_histogram.len();
let start_height = self
.phase_v3_price_cents
.len()
.min(starting_indexes.height.to_usize());
self.phase_v3_price_cents
.truncate_if_needed_at(start_height)?;
if start_height >= total_heights {
return Ok(());
}
info!(
"Computing phase V3 prices from height {} to {}",
start_height, total_heights
);
let mut histogram_iter = self.phase_v3_histogram.iter()?;
let mut height_to_dateindex_iter = indexes.height.dateindex.iter();
// For weekly OHLC anchors
let mut price_ohlc_iter = price_cents.ohlc.dateindex.iter()?;
let mut dateindex_to_weekindex_iter = indexes.dateindex.weekindex.iter();
let mut weekindex_to_first_dateindex_iter = indexes.weekindex.first_dateindex.iter();
let mut weekindex_dateindex_count_iter = indexes.weekindex.dateindex_count.iter();
let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8;
// Track previous price for fallback
let mut prev_price_cents = if start_height > 0 {
self.phase_v3_price_cents
.iter()?
.get(Height::from(start_height - 1))
.unwrap_or(Cents::from(10_000_000i64))
} else {
Cents::from(10_000_000i64) // Default ~$100k
};
for height in start_height..total_heights {
let height_idx = Height::from(height);
let histogram: OracleBinsV2 = histogram_iter.get_unwrap(height_idx);
// Get weekly anchor for this block's date
let dateindex = height_to_dateindex_iter.get(height_idx);
let weekly_bounds: Option<(f64, f64)> = dateindex.and_then(|di| {
let wi = dateindex_to_weekindex_iter.get(di)?;
let first_di = weekindex_to_first_dateindex_iter.get(wi)?;
let count = weekindex_dateindex_count_iter
.get(wi)
.map(|c| *c as usize)?;
let mut low = Cents::from(i64::MAX);
let mut high = Cents::from(0i64);
for i in 0..count {
let di = DateIndex::from(first_di.to_usize() + i);
if let Some(ohlc) = price_ohlc_iter.get(di) {
if *ohlc.low < low {
low = *ohlc.low;
}
if *ohlc.high > high {
high = *ohlc.high;
}
}
}
if i64::from(low) > 0 && i64::from(high) > 0 {
Some((
i64::from(low) as f64 / 100.0,
i64::from(high) as f64 / 100.0,
))
} else {
None
}
});
// Compute price using cross-correlation
let price_cents = if histogram.total_count() >= 10 {
// Convert OracleBinsV2 to PhaseHistogramV2
let mut phase_hist = PhaseHistogramV2::new();
for (i, &count) in histogram.bins.iter().enumerate() {
if count > 0 {
let phase = (i as f64 + 0.5) / 200.0;
let log_sats = 6.0 + phase;
let sats = 10.0_f64.powf(log_sats);
for _ in 0..count {
phase_hist.add(Sats::from(sats as u64));
}
}
}
if let Some((low, high)) = weekly_bounds {
// Have weekly anchor - constrained search
let (phase_min, phase_max) = phase_range_from_anchor(low, high, 0.05);
let (best_phase, _corr) =
find_best_phase(&phase_hist, 2, Some(phase_min), Some(phase_max));
let price = phase_to_price(best_phase, low, high);
Cents::from((price * 100.0) as i64)
} else {
// No anchor - use previous price as reference
let anchor_low = (i64::from(prev_price_cents) as f64 / 100.0) * 0.5;
let anchor_high = (i64::from(prev_price_cents) as f64 / 100.0) * 2.0;
let (best_phase, _corr) = find_best_phase(&phase_hist, 2, None, None);
let price = phase_to_price(best_phase, anchor_low, anchor_high);
Cents::from((price * 100.0) as i64)
}
} else {
// Too few outputs - use previous price
prev_price_cents
};
prev_price_cents = price_cents;
self.phase_v3_price_cents.push(price_cents);
// Progress logging
let progress = (height * 100 / total_heights.max(1)) as u8;
if progress > last_progress {
last_progress = progress;
info!("Phase V3 price computation: {}%", progress);
let _lock = exit.lock();
self.phase_v3_price_cents.write()?;
}
}
// Final write
{
let _lock = exit.lock();
self.phase_v3_price_cents.write()?;
}
info!(
"Phase V3 prices complete: {} blocks",
self.phase_v3_price_cents.len()
);
Ok(())
}
/// Compute Phase Oracle V3 - Peak prices using direct peak finding (like V1)
fn compute_phase_v3_peak_prices(
&mut self,
indexes: &indexes::Vecs,
price_cents: &cents::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
let source_version = self.phase_v3_histogram.version();
self.phase_v3_peak_price_cents
.validate_computed_version_or_reset(source_version)?;
let total_heights = self.phase_v3_histogram.len();
let start_height = self
.phase_v3_peak_price_cents
.len()
.min(starting_indexes.height.to_usize());
self.phase_v3_peak_price_cents
.truncate_if_needed_at(start_height)?;
if start_height >= total_heights {
return Ok(());
}
info!(
"Computing phase V3 peak prices from height {} to {}",
start_height, total_heights
);
let mut histogram_iter = self.phase_v3_histogram.iter()?;
let mut height_to_dateindex_iter = indexes.height.dateindex.iter();
// For weekly OHLC anchors
let mut price_ohlc_iter = price_cents.ohlc.dateindex.iter()?;
let mut dateindex_to_weekindex_iter = indexes.dateindex.weekindex.iter();
let mut weekindex_to_first_dateindex_iter = indexes.weekindex.first_dateindex.iter();
let mut weekindex_dateindex_count_iter = indexes.weekindex.dateindex_count.iter();
let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8;
// Track previous price for fallback
let mut prev_price_cents = if start_height > 0 {
self.phase_v3_peak_price_cents
.iter()?
.get(Height::from(start_height - 1))
.unwrap_or(Cents::from(10_000_000i64))
} else {
Cents::from(10_000_000i64)
};
for height in start_height..total_heights {
let height_idx = Height::from(height);
let histogram: OracleBinsV2 = histogram_iter.get_unwrap(height_idx);
// Get weekly anchor for decade selection
let dateindex = height_to_dateindex_iter.get(height_idx);
let anchor_price: Option<f64> = dateindex.and_then(|di| {
let wi = dateindex_to_weekindex_iter.get(di)?;
let first_di = weekindex_to_first_dateindex_iter.get(wi)?;
let count = weekindex_dateindex_count_iter
.get(wi)
.map(|c| *c as usize)?;
let mut sum = 0i64;
let mut cnt = 0;
for i in 0..count {
let di = DateIndex::from(first_di.to_usize() + i);
if let Some(ohlc) = price_ohlc_iter.get(di) {
sum += i64::from(*ohlc.close);
cnt += 1;
}
}
if cnt > 0 {
Some(sum as f64 / cnt as f64 / 100.0)
} else {
None
}
});
// Use anchor or previous price for decade selection
let anchor = anchor_price.unwrap_or(i64::from(prev_price_cents) as f64 / 100.0);
// Find peak bin directly (like V1) using 100 bins (downsample from 200)
let price_cents = if histogram.total_count() >= 10 {
// Downsample 200 bins to 100 bins
let mut bins100 = [0u32; 100];
(0..100).for_each(|i| {
bins100[i] = histogram.bins[i * 2] as u32 + histogram.bins[i * 2 + 1] as u32;
});
// Find peak bin, skipping bin 0 (round BTC amounts cluster there)
let peak_bin = bins100
.iter()
.enumerate()
.filter(|(bin, _)| *bin != 0)
.max_by_key(|(_, count)| *count)
.map(|(bin, _)| bin)
.unwrap_or(0);
// Convert bin to price using anchor for decade (100 bins)
let phase = (peak_bin as f64 + 0.5) / 100.0;
let base_price = 10.0_f64.powf(phase);
// Find best decade
let mut best_price = base_price;
let mut best_dist = f64::MAX;
for decade in -2..=6 {
let candidate = base_price * 10.0_f64.powi(decade);
let dist = (candidate - anchor).abs();
if dist < best_dist {
best_dist = dist;
best_price = candidate;
}
}
Cents::from((best_price.clamp(0.01, 10_000_000.0) * 100.0) as i64)
} else {
prev_price_cents
};
prev_price_cents = price_cents;
self.phase_v3_peak_price_cents.push(price_cents);
// Progress logging
let progress = (height * 100 / total_heights.max(1)) as u8;
if progress > last_progress {
last_progress = progress;
info!("Phase V3 peak price computation: {}%", progress);
let _lock = exit.lock();
self.phase_v3_peak_price_cents.write()?;
}
}
// Final write
{
let _lock = exit.lock();
self.phase_v3_peak_price_cents.write()?;
}
info!(
"Phase V3 peak prices complete: {} blocks",
self.phase_v3_peak_price_cents.len()
);
Ok(())
}
/// Compute Phase Oracle V3 - Daily distributions from per-block prices
fn compute_phase_v3_daily(
&mut self,
indexes: &indexes::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
info!("Computing phase V3 daily distributions");
// Cross-correlation based
self.phase_v3_daily_cents.compute(
starting_indexes.dateindex,
&self.phase_v3_price_cents,
&indexes.dateindex.first_height,
&indexes.dateindex.height_count,
exit,
)?;
// Peak-based
self.phase_v3_peak_daily_cents.compute(
starting_indexes.dateindex,
&self.phase_v3_peak_price_cents,
&indexes.dateindex.first_height,
&indexes.dateindex.height_count,
exit,
)?;
info!(
"Phase V3 daily distributions complete: {} days",
self.phase_v3_daily_cents.len()
);
Ok(())
}
}
@@ -46,6 +46,58 @@ impl Vecs {
|di: DateIndex, iter| iter.get(di).map(|o: OHLCCents| OHLCDollars::from(o)),
);
// Phase Oracle V2 (round USD template matching)
// v3: Peak prices use 100 bins (downsampled from 200)
let phase_v2_version = version + Version::new(3);
let phase_v2_histogram =
BytesVec::forced_import(db, "phase_v2_histogram", phase_v2_version)?;
let phase_v2_price_cents =
PcoVec::forced_import(db, "phase_v2_price_cents", phase_v2_version)?;
let phase_v2_peak_price_cents =
PcoVec::forced_import(db, "phase_v2_peak_price_cents", phase_v2_version)?;
let phase_v2_daily_cents =
Distribution::forced_import(db, "phase_v2_daily", phase_v2_version)?;
let phase_v2_daily_dollars =
LazyTransformDistribution::from_distribution::<CentsToDollars>(
"phase_v2_daily_dollars",
phase_v2_version,
&phase_v2_daily_cents,
);
let phase_v2_peak_daily_cents =
Distribution::forced_import(db, "phase_v2_peak_daily", phase_v2_version)?;
let phase_v2_peak_daily_dollars =
LazyTransformDistribution::from_distribution::<CentsToDollars>(
"phase_v2_peak_daily_dollars",
phase_v2_version,
&phase_v2_peak_daily_cents,
);
// Phase Oracle V3 (BASE + uniqueVal filter)
// v4: Peak prices use 100 bins (downsampled from 200)
let phase_v3_version = version + Version::new(4);
let phase_v3_histogram =
BytesVec::forced_import(db, "phase_v3_histogram", phase_v3_version)?;
let phase_v3_price_cents =
PcoVec::forced_import(db, "phase_v3_price_cents", phase_v3_version)?;
let phase_v3_peak_price_cents =
PcoVec::forced_import(db, "phase_v3_peak_price_cents", phase_v3_version)?;
let phase_v3_daily_cents =
Distribution::forced_import(db, "phase_v3_daily", phase_v3_version)?;
let phase_v3_daily_dollars =
LazyTransformDistribution::from_distribution::<CentsToDollars>(
"phase_v3_daily_dollars",
phase_v3_version,
&phase_v3_daily_cents,
);
let phase_v3_peak_daily_cents =
Distribution::forced_import(db, "phase_v3_peak_daily", phase_v3_version)?;
let phase_v3_peak_daily_dollars =
LazyTransformDistribution::from_distribution::<CentsToDollars>(
"phase_v3_peak_daily_dollars",
phase_v3_version,
&phase_v3_peak_daily_cents,
);
Ok(Self {
pairoutputindex_to_txindex,
height_to_first_pairoutputindex,
@@ -59,6 +111,20 @@ impl Vecs {
ohlc_cents,
ohlc_dollars,
tx_count,
phase_v2_histogram,
phase_v2_price_cents,
phase_v2_peak_price_cents,
phase_v2_daily_cents,
phase_v2_daily_dollars,
phase_v2_peak_daily_cents,
phase_v2_peak_daily_dollars,
phase_v3_histogram,
phase_v3_price_cents,
phase_v3_peak_price_cents,
phase_v3_daily_cents,
phase_v3_daily_dollars,
phase_v3_peak_daily_cents,
phase_v3_peak_daily_dollars,
})
}
}
@@ -158,6 +158,7 @@ mod compute;
mod config;
mod histogram;
mod import;
mod phase_v2;
mod stencil;
mod vecs;
@@ -0,0 +1,296 @@
//! Phase Oracle V2 - Round USD Template Cross-Correlation
//!
//! Detects Bitcoin prices by finding where round USD amounts ($1, $5, $10, etc.)
//! cluster in the phase histogram. Uses weekly OHLC anchors to constrain search.
//!
//! ## Algorithm
//!
//! 1. Build 200-bin phase histogram: bin = frac(log10(sats)) * 200
//! 2. Cross-correlate with weighted round USD template
//! 3. Use weekly OHLC anchor to constrain phase search range
//! 4. Return best-matching phase, convert to price
//!
//! ## Key Insight
//!
//! Round USD amounts create a fixed "fingerprint" pattern in phase space:
//! - $1, $10, $100, $1000 → phase 0.00 (weight 10)
//! - $5, $50, $500 → phase 0.70 (weight 9)
//! - $2, $20, $200 → phase 0.30 (weight 7)
//! - etc.
//!
//! The pattern shifts based on price: sats_phase = usd_phase - price_phase (mod 1)
//! Finding the shift that best matches the template reveals the price phase.
use brk_types::Sats;
/// Number of phase bins (0.5% resolution)
pub const PHASE_BINS_V2: usize = 200;
/// Round USD template: (phase, weight) pairs
/// Phase = frac(log10(usd_cents)) for round USD values
/// Weight reflects expected popularity (higher = more common)
pub const ROUND_USD_TEMPLATE: [(f64, u32); 11] = [
(0.00, 10), // $1, $10, $100, $1000 - VERY common
(0.18, 3), // $1.50, $15, $150 - uncommon
(0.30, 7), // $2, $20, $200 - common
(0.40, 4), // $2.50, $25, $250 - moderate
(0.48, 5), // $3, $30, $300 - moderate
(0.60, 4), // $4, $40, $400 - moderate
(0.70, 9), // $5, $50, $500 - VERY common
(0.78, 2), // $6, $60, $600 - rare
(0.85, 2), // $7, $70, $700 - rare
(0.90, 2), // $8, $80, $800 - rare
(0.95, 2), // $9, $90, $900 - rare
];
/// Pre-computed template bins: (bin_index, weight)
pub fn template_bins() -> Vec<(usize, u32)> {
ROUND_USD_TEMPLATE
.iter()
.map(|&(phase, weight)| {
let bin = ((phase * PHASE_BINS_V2 as f64) as usize) % PHASE_BINS_V2;
(bin, weight)
})
.collect()
}
/// Phase histogram for V2 oracle (200 bins)
#[derive(Clone)]
pub struct PhaseHistogramV2 {
bins: [u32; PHASE_BINS_V2],
total: u32,
}
impl Default for PhaseHistogramV2 {
fn default() -> Self {
Self::new()
}
}
impl PhaseHistogramV2 {
pub fn new() -> Self {
Self {
bins: [0; PHASE_BINS_V2],
total: 0,
}
}
/// Convert sats value to phase bin index
/// Filters: min 1k sats, max 100k BTC
#[inline]
pub fn sats_to_bin(sats: Sats) -> Option<usize> {
if sats < Sats::_1K || sats > Sats::_100K_BTC {
return None;
}
let log_sats = f64::from(sats).log10();
let phase = log_sats.fract();
let phase = if phase < 0.0 { phase + 1.0 } else { phase };
Some(((phase * PHASE_BINS_V2 as f64) as usize).min(PHASE_BINS_V2 - 1))
}
/// Add a sats value to the histogram
#[inline]
pub fn add(&mut self, sats: Sats) {
if let Some(bin) = Self::sats_to_bin(sats) {
self.bins[bin] = self.bins[bin].saturating_add(1);
self.total += 1;
}
}
/// Add another histogram to this one
pub fn add_histogram(&mut self, other: &PhaseHistogramV2) {
for (i, &count) in other.bins.iter().enumerate() {
self.bins[i] = self.bins[i].saturating_add(count);
}
self.total = self.total.saturating_add(other.total);
}
/// Get total count
pub fn total(&self) -> u32 {
self.total
}
/// Get bins array
pub fn bins(&self) -> &[u32; PHASE_BINS_V2] {
&self.bins
}
/// Clear the histogram
pub fn clear(&mut self) {
self.bins.fill(0);
self.total = 0;
}
}
/// Find the best price phase using cross-correlation with weighted template
///
/// # Arguments
/// * `histogram` - Phase histogram to analyze
/// * `tolerance_bins` - Number of bins tolerance for template matching (e.g., 4 = ±2%)
/// * `phase_min` - Optional minimum phase from anchor (0.0-1.0)
/// * `phase_max` - Optional maximum phase from anchor (0.0-1.0)
///
/// # Returns
/// * `(best_phase, best_correlation)` - Best matching phase (0.0-1.0) and correlation score
pub fn find_best_phase(
histogram: &PhaseHistogramV2,
tolerance_bins: usize,
phase_min: Option<f64>,
phase_max: Option<f64>,
) -> (f64, u64) {
let template = template_bins();
let bins = histogram.bins();
let mut best_phase = 0.0;
let mut best_corr: u64 = 0;
// Determine valid shifts based on anchor constraints
let valid_shifts: Vec<usize> = if let (Some(p_min), Some(p_max)) = (phase_min, phase_max) {
let min_bin = ((p_min * PHASE_BINS_V2 as f64) as usize) % PHASE_BINS_V2;
let max_bin = ((p_max * PHASE_BINS_V2 as f64) as usize) % PHASE_BINS_V2;
if min_bin <= max_bin {
(min_bin..=max_bin).collect()
} else {
// Wraps around
(min_bin..PHASE_BINS_V2)
.chain(0..=max_bin)
.collect()
}
} else {
(0..PHASE_BINS_V2).collect()
};
// Cross-correlation: slide template across histogram
for shift in valid_shifts {
let mut corr: u64 = 0;
for &(template_bin, weight) in &template {
// Where would this template bin appear at this price phase shift?
let expected_bin = (template_bin + PHASE_BINS_V2 - shift) % PHASE_BINS_V2;
// Sum bins within tolerance, weighted
for t in 0..=(2 * tolerance_bins) {
let check_bin = (expected_bin + PHASE_BINS_V2 - tolerance_bins + t) % PHASE_BINS_V2;
corr += bins[check_bin] as u64 * weight as u64;
}
}
if corr > best_corr {
best_corr = corr;
best_phase = shift as f64 / PHASE_BINS_V2 as f64;
}
}
(best_phase, best_corr)
}
/// Get phase range from price anchor (low, high)
///
/// Returns (phase_min, phase_max) with tolerance added
pub fn phase_range_from_anchor(price_low: f64, price_high: f64, tolerance_pct: f64) -> (f64, f64) {
let low_adj = price_low * (1.0 - tolerance_pct);
let high_adj = price_high * (1.0 + tolerance_pct);
let phase_low = low_adj.log10().fract();
let phase_high = high_adj.log10().fract();
let phase_low = if phase_low < 0.0 {
phase_low + 1.0
} else {
phase_low
};
let phase_high = if phase_high < 0.0 {
phase_high + 1.0
} else {
phase_high
};
(phase_low, phase_high)
}
/// Convert detected phase to price using anchor for decade selection
///
/// The phase alone is ambiguous ($6.3, $63, $630, $6300 all have same phase).
/// Use the anchor price range to select the correct decade.
pub fn phase_to_price(phase: f64, anchor_low: f64, anchor_high: f64) -> f64 {
// Base price from phase (arbitrary decade, we'll adjust)
// phase = frac(log10(price)), so price = 10^(decade + phase)
// Start with decade 0 (prices 1-10)
let base_price = 10.0_f64.powf(phase);
// Find which decade puts us in the anchor range
let anchor_mid = (anchor_low + anchor_high) / 2.0;
// Try decades -2 to 6 ($0.01 to $1,000,000)
let mut best_price = base_price;
let mut best_dist = f64::MAX;
for decade in -2..=6 {
let candidate = base_price * 10.0_f64.powi(decade);
let dist = (candidate - anchor_mid).abs();
if dist < best_dist {
best_dist = dist;
best_price = candidate;
}
}
// Clamp to reasonable range
best_price.clamp(0.01, 10_000_000.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_template_bins() {
let template = template_bins();
assert_eq!(template.len(), 11);
// Check $1/$10/$100 maps to bin 0
assert_eq!(template[0].0, 0);
assert_eq!(template[0].1, 10);
// Check $5/$50 maps to bin 140 (0.70 * 200)
assert_eq!(template[6].0, 140);
assert_eq!(template[6].1, 9);
}
#[test]
fn test_sats_to_bin() {
// 1 BTC = 100M sats, log10(100M) = 8.0, frac = 0.0 → bin 0
let bin = PhaseHistogramV2::sats_to_bin(Sats::_1BTC).unwrap();
assert_eq!(bin, 0);
// 10M sats, log10(10M) = 7.0, frac = 0.0 → bin 0
let bin = PhaseHistogramV2::sats_to_bin(Sats::_10M).unwrap();
assert_eq!(bin, 0);
// 5M sats, log10(5M) ≈ 6.699, frac ≈ 0.699 → bin ~140
let bin = PhaseHistogramV2::sats_to_bin(Sats::from(5_000_000u64)).unwrap();
assert!((138..=142).contains(&bin), "5M sats bin = {}", bin);
}
#[test]
fn test_phase_range_from_anchor() {
// $6000-$8000 range
let (p_min, p_max) = phase_range_from_anchor(6000.0, 8000.0, 0.05);
// $6000 → log10 = 3.778, phase = 0.778
// $8000 → log10 = 3.903, phase = 0.903
assert!(p_min > 0.7 && p_min < 0.8, "p_min = {}", p_min);
assert!(p_max > 0.85 && p_max < 0.95, "p_max = {}", p_max);
}
#[test]
fn test_phase_to_price() {
// Phase 0.0 with anchor $50-150 should give ~$100
let price = phase_to_price(0.0, 50.0, 150.0);
assert!(price > 80.0 && price < 120.0, "price = {}", price);
// Phase 0.70 with anchor $4000-6000 should give ~$5000
let price = phase_to_price(0.70, 4000.0, 6000.0);
assert!(price > 4000.0 && price < 6000.0, "price = {}", price);
}
}
+47 -2
View File
@@ -1,7 +1,7 @@
use brk_traversable::Traversable;
use brk_types::{
Cents, DateIndex, Dollars, Height, OHLCCents, OHLCDollars, OracleBins, PairOutputIndex, Sats,
StoredU32, TxIndex,
Cents, DateIndex, Dollars, Height, OHLCCents, OHLCDollars, OracleBins, OracleBinsV2,
PairOutputIndex, Sats, StoredU32, TxIndex,
};
use vecdb::{BytesVec, LazyVecFrom1, PcoVec};
@@ -55,4 +55,49 @@ pub struct Vecs {
/// Number of qualifying transactions per day (for confidence)
pub tx_count: PcoVec<DateIndex, StoredU32>,
// ========== Phase Oracle V2 (round USD template matching) ==========
/// Per-block 200-bin phase histogram
pub phase_v2_histogram: BytesVec<Height, OracleBinsV2>,
/// Per-block price in cents from phase oracle V2 (cross-correlation with round USD template)
pub phase_v2_price_cents: PcoVec<Height, Cents>,
/// Per-block price in cents using direct peak finding (like V1)
pub phase_v2_peak_price_cents: PcoVec<Height, Cents>,
/// Daily distribution (min, max, average, percentiles) from phase oracle V2
pub phase_v2_daily_cents: Distribution<DateIndex, Cents>,
/// Daily distribution in dollars (lazy conversion from cents)
pub phase_v2_daily_dollars: LazyTransformDistribution<DateIndex, Dollars, Cents>,
/// Daily distribution from peak-based prices
pub phase_v2_peak_daily_cents: Distribution<DateIndex, Cents>,
/// Daily distribution in dollars (lazy conversion from cents)
pub phase_v2_peak_daily_dollars: LazyTransformDistribution<DateIndex, Dollars, Cents>,
// ========== Phase Oracle V3 (BASE + uniqueVal filter) ==========
/// Per-block 200-bin phase histogram with uniqueVal filtering
/// Only includes outputs with unique values within their transaction
pub phase_v3_histogram: BytesVec<Height, OracleBinsV2>,
/// Per-block price in cents from phase oracle V3 (cross-correlation)
pub phase_v3_price_cents: PcoVec<Height, Cents>,
/// Per-block price in cents using direct peak finding (like V1)
pub phase_v3_peak_price_cents: PcoVec<Height, Cents>,
/// Daily distribution from phase oracle V3
pub phase_v3_daily_cents: Distribution<DateIndex, Cents>,
/// Daily distribution in dollars (lazy conversion from cents)
pub phase_v3_daily_dollars: LazyTransformDistribution<DateIndex, Dollars, Cents>,
/// Daily distribution from peak-based prices
pub phase_v3_peak_daily_cents: Distribution<DateIndex, Cents>,
/// Daily distribution in dollars (lazy conversion from cents)
pub phase_v3_peak_daily_dollars: LazyTransformDistribution<DateIndex, Dollars, Cents>,
}
+4 -4
View File
@@ -4,8 +4,8 @@ HTTP API server for Bitcoin on-chain analytics.
## Features
- **OpenAPI spec**: Auto-generated docs at `/api` with full spec at `/api.json`
- **LLM-optimized**: Compact spec at `/api.trimmed.json` for AI tools
- **OpenAPI spec**: Auto-generated docs at `/api` with full spec at `/openapi.json`
- **LLM-optimized**: Compact spec at `/api.json` for AI tools
- **Response caching**: ETag-based with LRU cache (5000 entries)
- **Compression**: Brotli, gzip, deflate, zstd
- **Static files**: Optional web interface hosting
@@ -23,8 +23,8 @@ server.serve().await?;
| Path | Description |
|------|-------------|
| `/api` | Interactive API documentation |
| `/api.json` | Full OpenAPI specification |
| `/api.trimmed.json` | Compact OpenAPI for LLMs |
| `/openapi.json` | Full OpenAPI specification |
| `/api.json` | Compact OpenAPI for LLMs |
| `/api/address/{address}` | Address stats, transactions, UTXOs |
| `/api/block/{hash}` | Block info, transactions, status |
| `/api/block-height/{height}` | Block by height |
+21 -5
View File
@@ -5,19 +5,35 @@ fn main() {
// Generate importmap for website (updates index.html in place)
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let website_path = Path::new(&manifest_dir).join("../../website");
// Use ./website (symlink in repo, real dir in published crate)
let website_path = Path::new(&manifest_dir).join("website");
println!("cargo:rerun-if-changed=../../website");
println!("cargo:rerun-if-changed=website");
println!("cargo::warning=build.rs: website_path={website_path:?}, exists={}", website_path.exists());
if website_path.exists() {
// Skip importmap hashing in dev mode (files change often)
let map = if is_dev {
println!("cargo::warning=build.rs: dev mode, skipping importmap");
importmap::ImportMap::empty()
} else {
importmap::ImportMap::scan(&website_path, "")
.unwrap_or_else(|_| importmap::ImportMap::empty())
match importmap::ImportMap::scan(&website_path, "") {
Ok(map) => {
println!("cargo::warning=build.rs: importmap scanned {} entries", map.imports.len());
map
}
Err(e) => {
println!("cargo::warning=build.rs: importmap scan failed: {e}");
importmap::ImportMap::empty()
}
}
};
let _ = map.update_html_file(&website_path.join("index.html"));
let index_path = website_path.join("index.html");
if let Err(e) = map.update_html_file(&index_path) {
println!("cargo::warning=build.rs: failed to update index.html: {e}");
}
} else {
println!("cargo::warning=build.rs: website path does not exist!");
}
}
+2 -14
View File
@@ -6,11 +6,11 @@ use axum::{
http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
};
use brk_error::Result;
use brk_types::{Format, MetricSelection, Output};
use quick_cache::sync::GuardResult;
use crate::{
Result,
api::metrics::{CACHE_CONTROL, MAX_WEIGHT},
extended::HeaderMapExtended,
};
@@ -18,22 +18,10 @@ use crate::{
use super::AppState;
pub async fn handler(
uri: Uri,
headers: HeaderMap,
query: Query<MetricSelection>,
State(state): State<AppState>,
) -> Response {
match req_to_response_res(uri, headers, query, state).await {
Ok(response) => response,
Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(),
}
}
async fn req_to_response_res(
uri: Uri,
headers: HeaderMap,
Query(params): Query<MetricSelection>,
AppState { query, cache, .. }: AppState,
State(AppState { query, cache, .. }): State<AppState>,
) -> Result<Response> {
// Phase 1: Search and resolve metadata (cheap)
let resolved = query.run(move |q| q.resolve(params, MAX_WEIGHT)).await?;
+2 -14
View File
@@ -6,11 +6,11 @@ use axum::{
http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
};
use brk_error::Result;
use brk_types::{Format, MetricSelection, Output};
use quick_cache::sync::GuardResult;
use crate::{
Result,
api::metrics::{CACHE_CONTROL, MAX_WEIGHT},
extended::HeaderMapExtended,
};
@@ -18,22 +18,10 @@ use crate::{
use super::AppState;
pub async fn handler(
uri: Uri,
headers: HeaderMap,
query: Query<MetricSelection>,
State(state): State<AppState>,
) -> Response {
match req_to_response_res(uri, headers, query, state).await {
Ok(response) => response,
Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(),
}
}
async fn req_to_response_res(
uri: Uri,
headers: HeaderMap,
Query(params): Query<MetricSelection>,
AppState { query, cache, .. }: AppState,
State(AppState { query, cache, .. }): State<AppState>,
) -> Result<Response> {
// Phase 1: Search and resolve metadata (cheap)
let resolved = query.run(move |q| q.resolve(params, MAX_WEIGHT)).await?;
+2 -14
View File
@@ -6,11 +6,11 @@ use axum::{
http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
};
use brk_error::Result;
use brk_types::{Format, MetricSelection, OutputLegacy};
use quick_cache::sync::GuardResult;
use crate::{
Result,
api::metrics::{CACHE_CONTROL, MAX_WEIGHT},
extended::HeaderMapExtended,
};
@@ -18,22 +18,10 @@ use crate::{
use super::AppState;
pub async fn handler(
uri: Uri,
headers: HeaderMap,
query: Query<MetricSelection>,
State(state): State<AppState>,
) -> Response {
match req_to_response_res(uri, headers, query, state).await {
Ok(response) => response,
Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(),
}
}
async fn req_to_response_res(
uri: Uri,
headers: HeaderMap,
Query(params): Query<MetricSelection>,
AppState { query, cache, .. }: AppState,
State(AppState { query, cache, .. }): State<AppState>,
) -> Result<Response> {
// Phase 1: Search and resolve metadata (cheap)
let resolved = query.run(move |q| q.resolve(params, MAX_WEIGHT)).await?;
+10 -3
View File
@@ -170,6 +170,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
state,
)
.await
.into_response()
},
|op| op
.id("get_metric")
@@ -188,7 +189,9 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
.api_route(
"/api/metrics/bulk",
get_with(
bulk::handler,
|uri, headers, query, state| async move {
bulk::handler(uri, headers, query, state).await.into_response()
},
|op| op
.id("get_metrics")
.metrics_tag()
@@ -225,7 +228,9 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
Metrics::from(split.collect::<Vec<_>>().join(separator)),
range,
));
legacy::handler(uri, headers, Query(params), state).await
legacy::handler(uri, headers, Query(params), state)
.await
.into_response()
},
|op| op
.metrics_tag()
@@ -250,7 +255,9 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
state: State<AppState>|
-> Response {
let params: MetricSelection = params.into();
legacy::handler(uri, headers, Query(params), state).await
legacy::handler(uri, headers, Query(params), state)
.await
.into_response()
},
|op| op
.metrics_tag()
+6 -5
View File
@@ -48,7 +48,7 @@ impl ApiRoutes for ApiRouter<AppState> {
.add_server_routes()
.route("/api/server", get(Redirect::temporary("/api#tag/server")))
.api_route(
"/api.json",
"/openapi.json",
get_with(
async |headers: HeaderMap,
Extension(api): Extension<Arc<OpenApi>>|
@@ -62,7 +62,7 @@ impl ApiRoutes for ApiRouter<AppState> {
),
)
.api_route(
"/api.trimmed.json",
"/api.json",
get_with(
async |headers: HeaderMap,
Extension(api_trimmed): Extension<Arc<String>>|
@@ -72,12 +72,13 @@ impl ApiRoutes for ApiRouter<AppState> {
Response::static_json(&headers, &value)
},
|op| {
op.id("get_openapi_trimmed")
op.id("get_api")
.server_tag()
.summary("Trimmed OpenAPI specification")
.summary("Compact OpenAPI specification")
.description(
"Compact OpenAPI specification optimized for LLM consumption. \
Removes redundant fields while preserving essential API information.",
Removes redundant fields while preserving essential API information. \
Full spec available at `/openapi.json`.",
)
.ok_response::<serde_json::Value>()
},
+1 -1
View File
@@ -29,7 +29,7 @@ pub fn create_openapi() -> OpenApi {
- **Metrics**: Thousands of time-series metrics across multiple indexes (date, block height, etc.)
- **[Mempool.space](https://mempool.space/docs/api/rest) compatible** (WIP): Most non-metrics endpoints follow the mempool.space API format
- **Multiple formats**: JSON and CSV output
- **LLM-optimized**: Compact OpenAPI spec at [`/api.trimmed.json`](/api.trimmed.json) for AI tools
- **LLM-optimized**: Compact OpenAPI spec at [`/api.json`](/api.json) for AI tools (full spec at [`/openapi.json`](/openapi.json))
### Client Libraries
+1 -1
View File
@@ -18,7 +18,7 @@
<script>
Scalar.createApiReference("#app", {
url: "/api.json",
url: "/openapi.json",
hideClientButton: true,
telemetry: false,
// showToolbar: "never",
+58
View File
@@ -0,0 +1,58 @@
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use brk_error::Error as BrkError;
/// Server result type with Error that implements IntoResponse.
pub type Result<T> = std::result::Result<T, Error>;
/// Server error type that maps to HTTP status codes.
pub struct Error(StatusCode, String);
impl Error {
pub fn bad_request(msg: impl Into<String>) -> Self {
Self(StatusCode::BAD_REQUEST, msg.into())
}
pub fn forbidden(msg: impl Into<String>) -> Self {
Self(StatusCode::FORBIDDEN, msg.into())
}
pub fn not_found(msg: impl Into<String>) -> Self {
Self(StatusCode::NOT_FOUND, msg.into())
}
pub fn internal(msg: impl Into<String>) -> Self {
Self(StatusCode::INTERNAL_SERVER_ERROR, msg.into())
}
}
impl From<BrkError> for Error {
fn from(e: BrkError) -> Self {
let status = match &e {
BrkError::InvalidTxid
| BrkError::InvalidNetwork
| BrkError::InvalidAddress
| BrkError::UnsupportedType(_)
| BrkError::Parse(_)
| BrkError::NoMetrics
| BrkError::MetricUnsupportedIndex { .. }
| BrkError::WeightExceeded { .. } => StatusCode::BAD_REQUEST,
BrkError::UnknownAddress
| BrkError::UnknownTxid
| BrkError::NotFound(_)
| BrkError::MetricNotFound { .. } => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
Self(status, e.to_string())
}
}
impl IntoResponse for Error {
fn into_response(self) -> Response {
(self.0, self.1).into_response()
}
}
+15 -31
View File
@@ -7,30 +7,30 @@ use std::{
use axum::{
body::Body,
extract::{self, State},
http::{HeaderMap, StatusCode},
response::{IntoResponse, Response},
http::HeaderMap,
response::Response,
};
use brk_error::Result;
use quick_cache::sync::GuardResult;
use tracing::{error, info};
use crate::{
AppState, EMBEDDED_WEBSITE, HeaderMapExtended, ModifiedState, ResponseExtended, WebsiteSource,
AppState, EMBEDDED_WEBSITE, Error, HeaderMapExtended, ModifiedState, ResponseExtended, Result,
WebsiteSource,
};
pub async fn file_handler(
headers: HeaderMap,
State(state): State<AppState>,
path: extract::Path<String>,
) -> Response {
) -> Result<Response> {
any_handler(headers, state, Some(path.0))
}
pub async fn index_handler(headers: HeaderMap, State(state): State<AppState>) -> Response {
pub async fn index_handler(headers: HeaderMap, State(state): State<AppState>) -> Result<Response> {
any_handler(headers, state, None)
}
fn any_handler(headers: HeaderMap, state: AppState, path: Option<String>) -> Response {
fn any_handler(headers: HeaderMap, state: AppState, path: Option<String>) -> Result<Response> {
match &state.website {
WebsiteSource::Disabled => unreachable!("routes not added when disabled"),
WebsiteSource::Embedded => embedded_handler(&state, path),
@@ -92,7 +92,7 @@ fn build_response(state: &AppState, path: &Path, content: Vec<u8>, cache_key: &s
response
}
fn embedded_handler(state: &AppState, path: Option<String>) -> Response {
fn embedded_handler(state: &AppState, path: Option<String>) -> Result<Response> {
let path = path.unwrap_or_else(|| "index.html".to_string());
let sanitized = sanitize_path(&path);
@@ -113,17 +113,15 @@ fn embedded_handler(state: &AppState, path: Option<String>) -> Response {
});
let Some(file) = file else {
let response: Response<Body> =
(StatusCode::NOT_FOUND, "File not found".to_string()).into_response();
return response;
return Err(Error::not_found("File not found"));
};
build_response(
Ok(build_response(
state,
Path::new(file.path()),
file.contents().to_vec(),
&file.path().to_string_lossy(),
)
))
}
fn filesystem_handler(
@@ -131,7 +129,7 @@ fn filesystem_handler(
state: &AppState,
files_path: &Path,
path: Option<String>,
) -> Response {
) -> Result<Response> {
let path = if let Some(path) = path {
let sanitized = sanitize_path(&path);
let mut path = files_path.join(&sanitized);
@@ -145,9 +143,7 @@ fn filesystem_handler(
let allowed = canonical.starts_with(&canonical_base)
|| project_root.is_some_and(|root| canonical.starts_with(root));
if !allowed {
let response: Response<Body> =
(StatusCode::FORBIDDEN, "Access denied".to_string()).into_response();
return response;
return Err(Error::forbidden("Access denied"));
}
}
@@ -162,12 +158,7 @@ fn filesystem_handler(
// SPA fallback
if !path.exists() || path.is_dir() {
if path.extension().is_some() {
let response: Response<Body> = (
StatusCode::INTERNAL_SERVER_ERROR,
"File doesn't exist".to_string(),
)
.into_response();
return response;
return Err(Error::not_found("File doesn't exist"));
} else {
path = files_path.join("index.html");
}
@@ -181,14 +172,7 @@ fn filesystem_handler(
path_to_response(&headers, state, &path)
}
fn path_to_response(headers: &HeaderMap, state: &AppState, path: &Path) -> Response {
match path_to_response_(headers, state, path) {
Ok(response) => response,
Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(),
}
}
fn path_to_response_(headers: &HeaderMap, state: &AppState, path: &Path) -> Result<Response> {
fn path_to_response(headers: &HeaderMap, state: &AppState, path: &Path) -> Result<Response> {
let (modified, date) = headers.check_if_modified_since(path)?;
if !cfg!(debug_assertions) && modified == ModifiedState::NotModifiedSince {
return Ok(Response::new_not_modified());
+3 -2
View File
@@ -17,7 +17,6 @@ use axum::{
routing::get,
serve,
};
use brk_error::Result;
use brk_query::AsyncQuery;
use include_dir::{Dir, include_dir};
use quick_cache::sync::Cache;
@@ -48,12 +47,14 @@ impl WebsiteSource {
mod api;
pub mod cache;
mod error;
mod extended;
mod files;
mod state;
use api::*;
pub use cache::{CacheParams, CacheStrategy};
pub use error::{Error, Result};
use extended::*;
use files::FilesRoutes;
use state::*;
@@ -75,7 +76,7 @@ impl Server {
})
}
pub async fn serve(self) -> Result<()> {
pub async fn serve(self) -> brk_error::Result<()> {
let state = self.0;
let compression_layer = CompressionLayer::new()
+154
View File
@@ -148,3 +148,157 @@ impl Formattable for OracleBins {
false
}
}
// ============================================================================
// OracleBinsV2: 200-bin phase histogram for V2 phase oracle
// ============================================================================
/// Number of bins for V2 phase histogram (0.5% resolution)
pub const PHASE_BINS_V2: usize = 200;
/// V2 Phase histogram: counts per bin for frac(log10(sats))
///
/// Used for phase oracle V2 with round USD template matching.
/// Each bin represents 0.5% of the log10 fractional range [0, 1).
/// Values are u16 (max 65535 per bin).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OracleBinsV2 {
pub bins: [u16; PHASE_BINS_V2],
}
impl Default for OracleBinsV2 {
fn default() -> Self {
Self::ZERO
}
}
impl Display for OracleBinsV2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "OracleBinsV2(peak={})", self.peak_bin())
}
}
impl Serialize for OracleBinsV2 {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.bins.as_slice().serialize(serializer)
}
}
impl<'de> Deserialize<'de> for OracleBinsV2 {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct BinsVisitor;
impl<'de> Visitor<'de> for BinsVisitor {
type Value = OracleBinsV2;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "an array of {} u16 values", PHASE_BINS_V2)
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut bins = [0u16; PHASE_BINS_V2];
for (i, bin) in bins.iter_mut().enumerate() {
*bin = seq
.next_element()?
.ok_or_else(|| serde::de::Error::invalid_length(i, &self))?;
}
Ok(OracleBinsV2 { bins })
}
}
deserializer.deserialize_seq(BinsVisitor)
}
}
impl JsonSchema for OracleBinsV2 {
fn schema_name() -> std::borrow::Cow<'static, str> {
"OracleBinsV2".into()
}
fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
Vec::<u16>::json_schema(_gen)
}
}
impl OracleBinsV2 {
pub const ZERO: Self = Self {
bins: [0; PHASE_BINS_V2],
};
/// Get the bin index for a sats value
/// Filters: min 1k sats, max 100k BTC
#[inline]
pub fn sats_to_bin(sats: Sats) -> Option<usize> {
if sats < Sats::_1K || sats > Sats::_100K_BTC {
return None;
}
let log_sats = f64::from(sats).log10();
let phase = log_sats.fract();
let phase = if phase < 0.0 { phase + 1.0 } else { phase };
Some(((phase * PHASE_BINS_V2 as f64) as usize).min(PHASE_BINS_V2 - 1))
}
/// Add a count to the bin for this sats value
#[inline]
pub fn add(&mut self, sats: Sats) {
if let Some(bin) = Self::sats_to_bin(sats) {
self.bins[bin] = self.bins[bin].saturating_add(1);
}
}
/// Add another histogram to this one
pub fn add_histogram(&mut self, other: &OracleBinsV2) {
for (i, &count) in other.bins.iter().enumerate() {
self.bins[i] = self.bins[i].saturating_add(count);
}
}
/// Find the peak bin (index with highest count)
pub fn peak_bin(&self) -> usize {
self.bins
.iter()
.enumerate()
.max_by_key(|(_, count)| *count)
.map(|(idx, _)| idx)
.unwrap_or(0)
}
/// Get total count across all bins
pub fn total_count(&self) -> u32 {
self.bins.iter().map(|&c| c as u32).sum()
}
}
impl Bytes for OracleBinsV2 {
type Array = [u8; size_of::<Self>()];
fn to_bytes(&self) -> Self::Array {
let mut arr = [0u8; size_of::<Self>()];
for (i, &count) in self.bins.iter().enumerate() {
let bytes = count.to_le_bytes();
arr[i * 2] = bytes[0];
arr[i * 2 + 1] = bytes[1];
}
arr
}
fn from_bytes(bytes: &[u8]) -> vecdb::Result<Self> {
if bytes.len() < size_of::<Self>() {
return Err(vecdb::Error::WrongLength {
received: bytes.len(),
expected: size_of::<Self>(),
});
}
let mut bins = [0u16; PHASE_BINS_V2];
for (i, bin) in bins.iter_mut().enumerate() {
*bin = u16::from_le_bytes([bytes[i * 2], bytes[i * 2 + 1]]);
}
Ok(Self { bins })
}
}
impl Formattable for OracleBinsV2 {
fn may_need_escaping() -> bool {
false
}
}