From 3b00a92fa40abf715a167bca82a638e9b49acac4 Mon Sep 17 00:00:00 2001 From: nym21 Date: Fri, 16 Jan 2026 15:17:42 +0100 Subject: [PATCH] global: snapshot --- Cargo.lock | 2 + crates/brk_client/src/lib.rs | 550 ++-- crates/brk_computer/src/indexes/height.rs | 2 +- .../brk_computer/src/price/oracle/import.rs | 4 +- crates/brk_server/Cargo.toml | 2 +- crates/brk_server/build.rs | 3 +- crates/brk_server/src/api/mempool/mod.rs | 7 +- crates/brk_server/src/api/metrics/bulk.rs | 18 +- crates/brk_server/src/api/metrics/data.rs | 18 +- crates/brk_server/src/api/metrics/legacy.rs | 22 +- crates/brk_server/src/api/mining/mod.rs | 6 +- crates/brk_server/src/api/openapi/trim.rs | 5 +- crates/brk_server/src/extended/header_map.rs | 7 - crates/brk_server/src/extended/response.rs | 17 +- crates/brk_server/src/files/file.rs | 17 +- crates/brk_server/src/lib.rs | 51 +- modules/brk-client/index.js | 622 ++-- packages/brk_client/brk_client/__init__.py | 278 +- research/analyze_price_signals.py | 1081 +++++++ research/oracle_filter_analysis.md | 174 ++ research/price_signal_analysis_report.txt | 2549 +++++++++++++++++ research/test_phase_detection.py | 282 ++ website/scripts/options/market/index.js | 32 +- 23 files changed, 4904 insertions(+), 845 deletions(-) create mode 100644 research/analyze_price_signals.py create mode 100644 research/oracle_filter_analysis.md create mode 100644 research/price_signal_analysis_report.txt create mode 100644 research/test_phase_detection.py diff --git a/Cargo.lock b/Cargo.lock index b197207e1..36934a0da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3325,8 +3325,10 @@ dependencies = [ "bitflags 2.10.0", "bytes", "futures-core", + "futures-util", "http", "http-body", + "http-body-util", "pin-project-lite", "tokio", "tokio-util", diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index acb648605..6945292a8 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -1268,56 +1268,6 @@ impl Price111dSmaPattern { } } -/// Pattern struct for repeated tree structure. -pub struct ActivePriceRatioPattern { - pub ratio: MetricPattern4, - pub ratio_1m_sma: MetricPattern4, - pub ratio_1w_sma: MetricPattern4, - pub ratio_1y_sd: Ratio1ySdPattern, - pub ratio_2y_sd: Ratio1ySdPattern, - pub ratio_4y_sd: Ratio1ySdPattern, - pub ratio_pct1: MetricPattern4, - pub ratio_pct1_usd: MetricPattern4, - pub ratio_pct2: MetricPattern4, - pub ratio_pct2_usd: MetricPattern4, - pub ratio_pct5: MetricPattern4, - pub ratio_pct5_usd: MetricPattern4, - pub ratio_pct95: MetricPattern4, - pub ratio_pct95_usd: MetricPattern4, - pub ratio_pct98: MetricPattern4, - pub ratio_pct98_usd: MetricPattern4, - pub ratio_pct99: MetricPattern4, - pub ratio_pct99_usd: MetricPattern4, - pub ratio_sd: Ratio1ySdPattern, -} - -impl ActivePriceRatioPattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - ratio: MetricPattern4::new(client.clone(), acc.clone()), - ratio_1m_sma: MetricPattern4::new(client.clone(), _m(&acc, "1m_sma")), - ratio_1w_sma: MetricPattern4::new(client.clone(), _m(&acc, "1w_sma")), - ratio_1y_sd: Ratio1ySdPattern::new(client.clone(), _m(&acc, "1y")), - ratio_2y_sd: Ratio1ySdPattern::new(client.clone(), _m(&acc, "2y")), - ratio_4y_sd: Ratio1ySdPattern::new(client.clone(), _m(&acc, "4y")), - ratio_pct1: MetricPattern4::new(client.clone(), _m(&acc, "pct1")), - ratio_pct1_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct1_usd")), - ratio_pct2: MetricPattern4::new(client.clone(), _m(&acc, "pct2")), - ratio_pct2_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct2_usd")), - ratio_pct5: MetricPattern4::new(client.clone(), _m(&acc, "pct5")), - ratio_pct5_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct5_usd")), - ratio_pct95: MetricPattern4::new(client.clone(), _m(&acc, "pct95")), - ratio_pct95_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct95_usd")), - ratio_pct98: MetricPattern4::new(client.clone(), _m(&acc, "pct98")), - ratio_pct98_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct98_usd")), - ratio_pct99: MetricPattern4::new(client.clone(), _m(&acc, "pct99")), - ratio_pct99_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct99_usd")), - ratio_sd: Ratio1ySdPattern::new(client.clone(), acc.clone()), - } - } -} - /// Pattern struct for repeated tree structure. pub struct PercentilesPattern { pub pct05: MetricPattern4, @@ -1368,6 +1318,56 @@ impl PercentilesPattern { } } +/// Pattern struct for repeated tree structure. +pub struct ActivePriceRatioPattern { + pub ratio: MetricPattern4, + pub ratio_1m_sma: MetricPattern4, + pub ratio_1w_sma: MetricPattern4, + pub ratio_1y_sd: Ratio1ySdPattern, + pub ratio_2y_sd: Ratio1ySdPattern, + pub ratio_4y_sd: Ratio1ySdPattern, + pub ratio_pct1: MetricPattern4, + pub ratio_pct1_usd: MetricPattern4, + pub ratio_pct2: MetricPattern4, + pub ratio_pct2_usd: MetricPattern4, + pub ratio_pct5: MetricPattern4, + pub ratio_pct5_usd: MetricPattern4, + pub ratio_pct95: MetricPattern4, + pub ratio_pct95_usd: MetricPattern4, + pub ratio_pct98: MetricPattern4, + pub ratio_pct98_usd: MetricPattern4, + pub ratio_pct99: MetricPattern4, + pub ratio_pct99_usd: MetricPattern4, + pub ratio_sd: Ratio1ySdPattern, +} + +impl ActivePriceRatioPattern { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + ratio: MetricPattern4::new(client.clone(), acc.clone()), + ratio_1m_sma: MetricPattern4::new(client.clone(), _m(&acc, "1m_sma")), + ratio_1w_sma: MetricPattern4::new(client.clone(), _m(&acc, "1w_sma")), + ratio_1y_sd: Ratio1ySdPattern::new(client.clone(), _m(&acc, "1y")), + ratio_2y_sd: Ratio1ySdPattern::new(client.clone(), _m(&acc, "2y")), + ratio_4y_sd: Ratio1ySdPattern::new(client.clone(), _m(&acc, "4y")), + ratio_pct1: MetricPattern4::new(client.clone(), _m(&acc, "pct1")), + ratio_pct1_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct1_usd")), + ratio_pct2: MetricPattern4::new(client.clone(), _m(&acc, "pct2")), + ratio_pct2_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct2_usd")), + ratio_pct5: MetricPattern4::new(client.clone(), _m(&acc, "pct5")), + ratio_pct5_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct5_usd")), + ratio_pct95: MetricPattern4::new(client.clone(), _m(&acc, "pct95")), + ratio_pct95_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct95_usd")), + ratio_pct98: MetricPattern4::new(client.clone(), _m(&acc, "pct98")), + ratio_pct98_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct98_usd")), + ratio_pct99: MetricPattern4::new(client.clone(), _m(&acc, "pct99")), + ratio_pct99_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct99_usd")), + ratio_sd: Ratio1ySdPattern::new(client.clone(), acc.clone()), + } + } +} + /// Pattern struct for repeated tree structure. pub struct RelativePattern5 { pub neg_unrealized_loss_rel_to_market_cap: MetricPattern1, @@ -1600,40 +1600,6 @@ impl BitcoinPattern { } } -/// Pattern struct for repeated tree structure. -pub struct DollarsPattern { - pub average: MetricPattern2, - pub base: MetricPattern11, - pub cumulative: MetricPattern1, - pub max: MetricPattern2, - pub median: MetricPattern6, - pub min: MetricPattern2, - pub pct10: MetricPattern6, - pub pct25: MetricPattern6, - pub pct75: MetricPattern6, - pub pct90: MetricPattern6, - pub sum: MetricPattern2, -} - -impl DollarsPattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - average: MetricPattern2::new(client.clone(), _m(&acc, "average")), - base: MetricPattern11::new(client.clone(), acc.clone()), - cumulative: MetricPattern1::new(client.clone(), _m(&acc, "cumulative")), - 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")), - sum: MetricPattern2::new(client.clone(), _m(&acc, "sum")), - } - } -} - /// Pattern struct for repeated tree structure. pub struct ClassAveragePricePattern { pub _2015: MetricPattern4, @@ -1669,33 +1635,35 @@ impl ClassAveragePricePattern { } /// Pattern struct for repeated tree structure. -pub struct RelativePattern { - pub neg_unrealized_loss_rel_to_market_cap: MetricPattern1, - pub net_unrealized_pnl_rel_to_market_cap: MetricPattern1, - pub nupl: MetricPattern1, - pub supply_in_loss_rel_to_circulating_supply: MetricPattern1, - pub supply_in_loss_rel_to_own_supply: MetricPattern1, - pub supply_in_profit_rel_to_circulating_supply: MetricPattern1, - pub supply_in_profit_rel_to_own_supply: MetricPattern1, - pub supply_rel_to_circulating_supply: MetricPattern4, - pub unrealized_loss_rel_to_market_cap: MetricPattern1, - pub unrealized_profit_rel_to_market_cap: MetricPattern1, +pub struct DollarsPattern { + pub average: MetricPattern2, + pub base: MetricPattern11, + pub cumulative: MetricPattern1, + pub max: MetricPattern2, + pub median: MetricPattern6, + pub min: MetricPattern2, + pub pct10: MetricPattern6, + pub pct25: MetricPattern6, + pub pct75: MetricPattern6, + pub pct90: MetricPattern6, + pub sum: MetricPattern2, } -impl RelativePattern { +impl DollarsPattern { /// Create a new pattern node with accumulated metric name. pub fn new(client: Arc, acc: String) -> Self { Self { - neg_unrealized_loss_rel_to_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "neg_unrealized_loss_rel_to_market_cap")), - net_unrealized_pnl_rel_to_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "net_unrealized_pnl_rel_to_market_cap")), - nupl: MetricPattern1::new(client.clone(), _m(&acc, "nupl")), - supply_in_loss_rel_to_circulating_supply: MetricPattern1::new(client.clone(), _m(&acc, "supply_in_loss_rel_to_circulating_supply")), - 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_circulating_supply: MetricPattern1::new(client.clone(), _m(&acc, "supply_in_profit_rel_to_circulating_supply")), - supply_in_profit_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "supply_in_profit_rel_to_own_supply")), - supply_rel_to_circulating_supply: MetricPattern4::new(client.clone(), _m(&acc, "supply_rel_to_circulating_supply")), - unrealized_loss_rel_to_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_loss_rel_to_market_cap")), - unrealized_profit_rel_to_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_profit_rel_to_market_cap")), + average: MetricPattern2::new(client.clone(), _m(&acc, "average")), + base: MetricPattern11::new(client.clone(), acc.clone()), + cumulative: MetricPattern1::new(client.clone(), _m(&acc, "cumulative")), + 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")), + sum: MetricPattern2::new(client.clone(), _m(&acc, "sum")), } } } @@ -1732,6 +1700,38 @@ impl RelativePattern2 { } } +/// Pattern struct for repeated tree structure. +pub struct RelativePattern { + pub neg_unrealized_loss_rel_to_market_cap: MetricPattern1, + pub net_unrealized_pnl_rel_to_market_cap: MetricPattern1, + pub nupl: MetricPattern1, + pub supply_in_loss_rel_to_circulating_supply: MetricPattern1, + pub supply_in_loss_rel_to_own_supply: MetricPattern1, + pub supply_in_profit_rel_to_circulating_supply: MetricPattern1, + pub supply_in_profit_rel_to_own_supply: MetricPattern1, + pub supply_rel_to_circulating_supply: MetricPattern4, + pub unrealized_loss_rel_to_market_cap: MetricPattern1, + pub unrealized_profit_rel_to_market_cap: MetricPattern1, +} + +impl RelativePattern { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + neg_unrealized_loss_rel_to_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "neg_unrealized_loss_rel_to_market_cap")), + net_unrealized_pnl_rel_to_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "net_unrealized_pnl_rel_to_market_cap")), + nupl: MetricPattern1::new(client.clone(), _m(&acc, "nupl")), + supply_in_loss_rel_to_circulating_supply: MetricPattern1::new(client.clone(), _m(&acc, "supply_in_loss_rel_to_circulating_supply")), + 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_circulating_supply: MetricPattern1::new(client.clone(), _m(&acc, "supply_in_profit_rel_to_circulating_supply")), + supply_in_profit_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "supply_in_profit_rel_to_own_supply")), + supply_rel_to_circulating_supply: MetricPattern4::new(client.clone(), _m(&acc, "supply_rel_to_circulating_supply")), + unrealized_loss_rel_to_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_loss_rel_to_market_cap")), + unrealized_profit_rel_to_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_profit_rel_to_market_cap")), + } + } +} + /// Pattern struct for repeated tree structure. pub struct CountPattern2 { pub average: MetricPattern1, @@ -1794,36 +1794,6 @@ impl AddrCountPattern { } } -/// Pattern struct for repeated tree structure. -pub struct FeeRatePattern { - pub average: MetricPattern1, - pub max: MetricPattern1, - pub median: MetricPattern11, - pub min: MetricPattern1, - pub pct10: MetricPattern11, - pub pct25: MetricPattern11, - pub pct75: MetricPattern11, - pub pct90: MetricPattern11, - pub txindex: MetricPattern27, -} - -impl FeeRatePattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - average: MetricPattern1::new(client.clone(), _m(&acc, "average")), - max: MetricPattern1::new(client.clone(), _m(&acc, "max")), - median: MetricPattern11::new(client.clone(), _m(&acc, "median")), - min: MetricPattern1::new(client.clone(), _m(&acc, "min")), - pct10: MetricPattern11::new(client.clone(), _m(&acc, "pct10")), - pct25: MetricPattern11::new(client.clone(), _m(&acc, "pct25")), - pct75: MetricPattern11::new(client.clone(), _m(&acc, "pct75")), - pct90: MetricPattern11::new(client.clone(), _m(&acc, "pct90")), - txindex: MetricPattern27::new(client.clone(), acc.clone()), - } - } -} - /// Pattern struct for repeated tree structure. pub struct FullnessPattern { pub average: MetricPattern2, @@ -1854,6 +1824,36 @@ impl FullnessPattern { } } +/// Pattern struct for repeated tree structure. +pub struct FeeRatePattern { + pub average: MetricPattern1, + pub max: MetricPattern1, + pub median: MetricPattern11, + pub min: MetricPattern1, + pub pct10: MetricPattern11, + pub pct25: MetricPattern11, + pub pct75: MetricPattern11, + pub pct90: MetricPattern11, + pub txindex: MetricPattern27, +} + +impl FeeRatePattern { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + average: MetricPattern1::new(client.clone(), _m(&acc, "average")), + max: MetricPattern1::new(client.clone(), _m(&acc, "max")), + median: MetricPattern11::new(client.clone(), _m(&acc, "median")), + min: MetricPattern1::new(client.clone(), _m(&acc, "min")), + pct10: MetricPattern11::new(client.clone(), _m(&acc, "pct10")), + pct25: MetricPattern11::new(client.clone(), _m(&acc, "pct25")), + pct75: MetricPattern11::new(client.clone(), _m(&acc, "pct75")), + pct90: MetricPattern11::new(client.clone(), _m(&acc, "pct90")), + txindex: MetricPattern27::new(client.clone(), acc.clone()), + } + } +} + /// Pattern struct for repeated tree structure. pub struct _0satsPattern { pub activity: ActivityPattern2, @@ -1910,32 +1910,6 @@ impl PhaseDailyCentsPattern { } } -/// Pattern struct for repeated tree structure. -pub struct _10yPattern { - pub activity: ActivityPattern2, - pub cost_basis: CostBasisPattern, - pub outputs: OutputsPattern, - pub realized: RealizedPattern4, - pub relative: RelativePattern, - pub supply: SupplyPattern2, - pub unrealized: UnrealizedPattern, -} - -impl _10yPattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, 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: RealizedPattern4::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, @@ -1963,25 +1937,25 @@ impl UnrealizedPattern { } /// Pattern struct for repeated tree structure. -pub struct _0satsPattern2 { +pub struct _10yTo12yPattern { pub activity: ActivityPattern2, - pub cost_basis: CostBasisPattern, + pub cost_basis: CostBasisPattern2, pub outputs: OutputsPattern, - pub realized: RealizedPattern, - pub relative: RelativePattern4, + pub realized: RealizedPattern2, + pub relative: RelativePattern2, pub supply: SupplyPattern2, pub unrealized: UnrealizedPattern, } -impl _0satsPattern2 { +impl _10yTo12yPattern { /// Create a new pattern node with accumulated metric name. pub fn new(client: Arc, acc: String) -> Self { Self { activity: ActivityPattern2::new(client.clone(), acc.clone()), - cost_basis: CostBasisPattern::new(client.clone(), acc.clone()), + cost_basis: CostBasisPattern2::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")), + realized: RealizedPattern2::new(client.clone(), acc.clone()), + relative: RelativePattern2::new(client.clone(), acc.clone()), supply: SupplyPattern2::new(client.clone(), _m(&acc, "supply")), unrealized: UnrealizedPattern::new(client.clone(), acc.clone()), } @@ -2014,6 +1988,32 @@ impl PeriodCagrPattern { } } +/// Pattern struct for repeated tree structure. +pub struct _10yPattern { + pub activity: ActivityPattern2, + pub cost_basis: CostBasisPattern, + pub outputs: OutputsPattern, + pub realized: RealizedPattern4, + pub relative: RelativePattern, + pub supply: SupplyPattern2, + pub unrealized: UnrealizedPattern, +} + +impl _10yPattern { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, 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: RealizedPattern4::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 _100btcPattern { pub activity: ActivityPattern2, @@ -2041,25 +2041,25 @@ impl _100btcPattern { } /// Pattern struct for repeated tree structure. -pub struct _10yTo12yPattern { +pub struct _0satsPattern2 { pub activity: ActivityPattern2, - pub cost_basis: CostBasisPattern2, + pub cost_basis: CostBasisPattern, pub outputs: OutputsPattern, - pub realized: RealizedPattern2, - pub relative: RelativePattern2, + pub realized: RealizedPattern, + pub relative: RelativePattern4, pub supply: SupplyPattern2, pub unrealized: UnrealizedPattern, } -impl _10yTo12yPattern { +impl _0satsPattern2 { /// Create a new pattern node with accumulated metric name. pub fn new(client: Arc, acc: String) -> Self { Self { activity: ActivityPattern2::new(client.clone(), acc.clone()), - cost_basis: CostBasisPattern2::new(client.clone(), acc.clone()), + cost_basis: CostBasisPattern::new(client.clone(), acc.clone()), outputs: OutputsPattern::new(client.clone(), _m(&acc, "utxo_count")), - realized: RealizedPattern2::new(client.clone(), acc.clone()), - relative: RelativePattern2::new(client.clone(), acc.clone()), + 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()), } @@ -2108,6 +2108,42 @@ impl SplitPattern2 { } } +/// Pattern struct for repeated tree structure. +pub struct _2015Pattern { + pub bitcoin: MetricPattern4, + pub dollars: MetricPattern4, + pub sats: MetricPattern4, +} + +impl _2015Pattern { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, 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, + pub dollars: MetricPattern1, + pub sats: MetricPattern1, +} + +impl ActiveSupplyPattern { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + bitcoin: MetricPattern1::new(client.clone(), _m(&acc, "btc")), + dollars: MetricPattern1::new(client.clone(), _m(&acc, "usd")), + sats: MetricPattern1::new(client.clone(), acc.clone()), + } + } +} + /// Pattern struct for repeated tree structure. pub struct CostBasisPattern2 { pub max: MetricPattern1, @@ -2126,6 +2162,24 @@ impl CostBasisPattern2 { } } +/// Pattern struct for repeated tree structure. +pub struct CoinbasePattern2 { + pub bitcoin: BlockCountPattern, + pub dollars: BlockCountPattern, + pub sats: BlockCountPattern, +} + +impl CoinbasePattern2 { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + bitcoin: BlockCountPattern::new(client.clone(), _m(&acc, "btc")), + dollars: BlockCountPattern::new(client.clone(), _m(&acc, "usd")), + sats: BlockCountPattern::new(client.clone(), acc.clone()), + } + } +} + /// Pattern struct for repeated tree structure. pub struct SegwitAdoptionPattern { pub base: MetricPattern11, @@ -2162,60 +2216,6 @@ impl CoinbasePattern { } } -/// Pattern struct for repeated tree structure. -pub struct _2015Pattern { - pub bitcoin: MetricPattern4, - pub dollars: MetricPattern4, - pub sats: MetricPattern4, -} - -impl _2015Pattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, 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 CoinbasePattern2 { - pub bitcoin: BlockCountPattern, - pub dollars: BlockCountPattern, - pub sats: BlockCountPattern, -} - -impl CoinbasePattern2 { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - bitcoin: BlockCountPattern::new(client.clone(), _m(&acc, "btc")), - dollars: BlockCountPattern::new(client.clone(), _m(&acc, "usd")), - sats: BlockCountPattern::new(client.clone(), acc.clone()), - } - } -} - -/// Pattern struct for repeated tree structure. -pub struct ActiveSupplyPattern { - pub bitcoin: MetricPattern1, - pub dollars: MetricPattern1, - pub sats: MetricPattern1, -} - -impl ActiveSupplyPattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - bitcoin: MetricPattern1::new(client.clone(), _m(&acc, "btc")), - dollars: MetricPattern1::new(client.clone(), _m(&acc, "usd")), - sats: MetricPattern1::new(client.clone(), acc.clone()), - } - } -} - /// Pattern struct for repeated tree structure. pub struct UnclaimedRewardsPattern { pub bitcoin: BitcoinPattern2, @@ -2234,22 +2234,6 @@ impl UnclaimedRewardsPattern { } } -/// Pattern struct for repeated tree structure. -pub struct _1dReturns1mSdPattern { - pub sd: MetricPattern4, - pub sma: MetricPattern4, -} - -impl _1dReturns1mSdPattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - sd: MetricPattern4::new(client.clone(), _m(&acc, "sd")), - sma: MetricPattern4::new(client.clone(), _m(&acc, "sma")), - } - } -} - /// Pattern struct for repeated tree structure. pub struct SupplyPattern2 { pub halved: ActiveSupplyPattern, @@ -2298,6 +2282,22 @@ impl CostBasisPattern { } } +/// Pattern struct for repeated tree structure. +pub struct _1dReturns1mSdPattern { + pub sd: MetricPattern4, + pub sma: MetricPattern4, +} + +impl _1dReturns1mSdPattern { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + sd: MetricPattern4::new(client.clone(), _m(&acc, "sd")), + sma: MetricPattern4::new(client.clone(), _m(&acc, "sma")), + } + } +} + /// Pattern struct for repeated tree structure. pub struct BitcoinPattern2 { pub cumulative: MetricPattern2, @@ -2340,22 +2340,8 @@ impl SatsPattern { /// Create a new pattern node with accumulated metric name. pub fn new(client: Arc, acc: String) -> Self { Self { - ohlc: MetricPattern1::new(client.clone(), _m(&acc, "ohlc_sats")), - split: SplitPattern2::new(client.clone(), _m(&acc, "sats")), - } - } -} - -/// Pattern struct for repeated tree structure. -pub struct OutputsPattern { - pub utxo_count: MetricPattern1, -} - -impl OutputsPattern { - /// Create a new pattern node with accumulated metric name. - pub fn new(client: Arc, acc: String) -> Self { - Self { - utxo_count: MetricPattern1::new(client.clone(), acc.clone()), + ohlc: MetricPattern1::new(client.clone(), _m(&acc, "ohlc")), + split: SplitPattern2::new(client.clone(), acc.clone()), } } } @@ -2374,6 +2360,20 @@ impl RealizedPriceExtraPattern { } } +/// Pattern struct for repeated tree structure. +pub struct OutputsPattern { + pub utxo_count: MetricPattern1, +} + +impl OutputsPattern { + /// Create a new pattern node with accumulated metric name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + utxo_count: MetricPattern1::new(client.clone(), acc.clone()), + } + } +} + // Metrics tree /// Metrics tree node. @@ -3932,7 +3932,7 @@ pub struct MetricsTree_Indexes_Height { impl MetricsTree_Indexes_Height { pub fn new(client: Arc, base_path: String) -> Self { Self { - dateindex: MetricPattern11::new(client.clone(), "height_dateindex".to_string()), + dateindex: MetricPattern11::new(client.clone(), "dateindex".to_string()), difficultyepoch: MetricPattern11::new(client.clone(), "difficultyepoch".to_string()), halvingepoch: MetricPattern11::new(client.clone(), "halvingepoch".to_string()), identity: MetricPattern11::new(client.clone(), "height".to_string()), @@ -4942,8 +4942,8 @@ impl MetricsTree_Positions { pub struct MetricsTree_Price { pub cents: MetricsTree_Price_Cents, pub oracle: MetricsTree_Price_Oracle, - pub sats: SatsPattern, - pub usd: MetricsTree_Price_Usd, + pub sats: MetricsTree_Price_Sats, + pub usd: SatsPattern, } 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: SatsPattern::new(client.clone(), "price".to_string()), - usd: MetricsTree_Price_Usd::new(client.clone(), format!("{base_path}_usd")), + sats: MetricsTree_Price_Sats::new(client.clone(), format!("{base_path}_sats")), + usd: SatsPattern::new(client.clone(), "price".to_string()), } } } @@ -5027,16 +5027,16 @@ impl MetricsTree_Price_Oracle { } /// Metrics tree node. -pub struct MetricsTree_Price_Usd { - pub ohlc: MetricPattern1, - pub split: SplitPattern2, +pub struct MetricsTree_Price_Sats { + pub ohlc: MetricPattern1, + pub split: SplitPattern2, } -impl MetricsTree_Price_Usd { +impl MetricsTree_Price_Sats { pub fn new(client: Arc, base_path: String) -> Self { Self { - ohlc: MetricPattern1::new(client.clone(), "price_ohlc".to_string()), - split: SplitPattern2::new(client.clone(), "price".to_string()), + ohlc: MetricPattern1::new(client.clone(), "price_ohlc_sats".to_string()), + split: SplitPattern2::new(client.clone(), "price_sats".to_string()), } } } diff --git a/crates/brk_computer/src/indexes/height.rs b/crates/brk_computer/src/indexes/height.rs index 070335009..365c24155 100644 --- a/crates/brk_computer/src/indexes/height.rs +++ b/crates/brk_computer/src/indexes/height.rs @@ -17,7 +17,7 @@ impl Vecs { pub fn forced_import(db: &Database, version: Version) -> Result { Ok(Self { identity: EagerVec::forced_import(db, "height", version)?, - dateindex: EagerVec::forced_import(db, "height_dateindex", version)?, + dateindex: EagerVec::forced_import(db, "dateindex", version)?, difficultyepoch: EagerVec::forced_import(db, "difficultyepoch", version)?, halvingepoch: EagerVec::forced_import(db, "halvingepoch", version)?, txindex_count: EagerVec::forced_import(db, "txindex_count", version)?, diff --git a/crates/brk_computer/src/price/oracle/import.rs b/crates/brk_computer/src/price/oracle/import.rs index 28e041fd1..9e34a5a91 100644 --- a/crates/brk_computer/src/price/oracle/import.rs +++ b/crates/brk_computer/src/price/oracle/import.rs @@ -24,8 +24,8 @@ impl Vecs { let phase_histogram = BytesVec::forced_import(db, "phase_histogram", version)?; // Layer 5: Phase Oracle prices - // v32: Revert to simple anchor-based decade selection (no prev_price tracking) - let phase_version = version + Version::new(25); + // v45: Back to decades (10x) + anchor only + let phase_version = version + Version::new(38); let phase_price_cents = PcoVec::forced_import(db, "phase_price_cents", phase_version)?; let phase_daily_cents = Distribution::forced_import(db, "phase_daily", phase_version)?; let phase_daily_dollars = LazyTransformDistribution::from_distribution::( diff --git a/crates/brk_server/Cargo.toml b/crates/brk_server/Cargo.toml index be2c9edcf..5762e6dab 100644 --- a/crates/brk_server/Cargo.toml +++ b/crates/brk_server/Cargo.toml @@ -33,7 +33,7 @@ serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } -tower-http = { version = "0.6.8", features = ["compression-full", "trace"] } +tower-http = { version = "0.6.8", features = ["catch-panic", "compression-full", "cors", "normalize-path", "timeout", "trace"] } [build-dependencies] # importmap = { path = "../../../importmap" } diff --git a/crates/brk_server/build.rs b/crates/brk_server/build.rs index 4dac4fecf..3edd508b1 100644 --- a/crates/brk_server/build.rs +++ b/crates/brk_server/build.rs @@ -14,7 +14,8 @@ fn main() { let map = if is_dev { importmap::ImportMap::empty() } else { - importmap::ImportMap::scan(&website_path, "").unwrap_or_else(|_| importmap::ImportMap::empty()) + importmap::ImportMap::scan(&website_path, "") + .unwrap_or_else(|_| importmap::ImportMap::empty()) }; let _ = map.update_html_file(&website_path.join("index.html")); diff --git a/crates/brk_server/src/api/mempool/mod.rs b/crates/brk_server/src/api/mempool/mod.rs index d02b1fe7f..666762fb5 100644 --- a/crates/brk_server/src/api/mempool/mod.rs +++ b/crates/brk_server/src/api/mempool/mod.rs @@ -1,10 +1,5 @@ use aide::axum::{ApiRouter, routing::get_with}; -use axum::{ - extract::State, - http::HeaderMap, - response::Redirect, - routing::get, -}; +use axum::{extract::State, http::HeaderMap, response::Redirect, routing::get}; use brk_types::{MempoolBlock, MempoolInfo, RecommendedFees, Txid}; use crate::{CacheStrategy, extended::TransformResponseExtended}; diff --git a/crates/brk_server/src/api/metrics/bulk.rs b/crates/brk_server/src/api/metrics/bulk.rs index bb703ec05..84241fe13 100644 --- a/crates/brk_server/src/api/metrics/bulk.rs +++ b/crates/brk_server/src/api/metrics/bulk.rs @@ -6,6 +6,7 @@ use axum::{ http::{HeaderMap, StatusCode, Uri}, response::{IntoResponse, Response}, }; +use brk_error::Result; use brk_types::{Format, MetricSelection, Output}; use quick_cache::sync::GuardResult; @@ -24,12 +25,7 @@ pub async fn handler( ) -> Response { match req_to_response_res(uri, headers, query, state).await { Ok(response) => response, - Err(error) => { - let mut response = - (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(); - response.headers_mut().insert_cors(); - response - } + Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(), } } @@ -38,19 +34,16 @@ async fn req_to_response_res( headers: HeaderMap, Query(params): Query, AppState { query, cache, .. }: AppState, -) -> brk_error::Result { +) -> Result { // Phase 1: Search and resolve metadata (cheap) - let resolved = query - .run(move |q| q.resolve(params, MAX_WEIGHT)) - .await?; + let resolved = query.run(move |q| q.resolve(params, MAX_WEIGHT)).await?; let format = resolved.format(); let etag = resolved.etag(); // Check if client has fresh cache if headers.has_etag(etag.as_str()) { - let mut response = (StatusCode::NOT_MODIFIED, "").into_response(); - response.headers_mut().insert_cors(); + let response = (StatusCode::NOT_MODIFIED, "").into_response(); return Ok(response); } @@ -81,7 +74,6 @@ async fn req_to_response_res( }; let headers = response.headers_mut(); - headers.insert_cors(); headers.insert_etag(etag.as_str()); headers.insert_cache_control(CACHE_CONTROL); diff --git a/crates/brk_server/src/api/metrics/data.rs b/crates/brk_server/src/api/metrics/data.rs index 0dd4ba3d7..236055b94 100644 --- a/crates/brk_server/src/api/metrics/data.rs +++ b/crates/brk_server/src/api/metrics/data.rs @@ -6,6 +6,7 @@ use axum::{ http::{HeaderMap, StatusCode, Uri}, response::{IntoResponse, Response}, }; +use brk_error::Result; use brk_types::{Format, MetricSelection, Output}; use quick_cache::sync::GuardResult; @@ -24,12 +25,7 @@ pub async fn handler( ) -> Response { match req_to_response_res(uri, headers, query, state).await { Ok(response) => response, - Err(error) => { - let mut response = - (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(); - response.headers_mut().insert_cors(); - response - } + Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(), } } @@ -38,19 +34,16 @@ async fn req_to_response_res( headers: HeaderMap, Query(params): Query, AppState { query, cache, .. }: AppState, -) -> brk_error::Result { +) -> Result { // Phase 1: Search and resolve metadata (cheap) - let resolved = query - .run(move |q| q.resolve(params, MAX_WEIGHT)) - .await?; + let resolved = query.run(move |q| q.resolve(params, MAX_WEIGHT)).await?; let format = resolved.format(); let etag = resolved.etag(); // Check if client has fresh cache if headers.has_etag(etag.as_str()) { - let mut response = (StatusCode::NOT_MODIFIED, "").into_response(); - response.headers_mut().insert_cors(); + let response = (StatusCode::NOT_MODIFIED, "").into_response(); return Ok(response); } @@ -81,7 +74,6 @@ async fn req_to_response_res( }; let headers = response.headers_mut(); - headers.insert_cors(); headers.insert_etag(etag.as_str()); headers.insert_cache_control(CACHE_CONTROL); diff --git a/crates/brk_server/src/api/metrics/legacy.rs b/crates/brk_server/src/api/metrics/legacy.rs index 4ced894a1..643b7ed4d 100644 --- a/crates/brk_server/src/api/metrics/legacy.rs +++ b/crates/brk_server/src/api/metrics/legacy.rs @@ -6,6 +6,7 @@ use axum::{ http::{HeaderMap, StatusCode, Uri}, response::{IntoResponse, Response}, }; +use brk_error::Result; use brk_types::{Format, MetricSelection, OutputLegacy}; use quick_cache::sync::GuardResult; @@ -24,12 +25,7 @@ pub async fn handler( ) -> Response { match req_to_response_res(uri, headers, query, state).await { Ok(response) => response, - Err(error) => { - let mut response = - (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(); - response.headers_mut().insert_cors(); - response - } + Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(), } } @@ -38,19 +34,16 @@ async fn req_to_response_res( headers: HeaderMap, Query(params): Query, AppState { query, cache, .. }: AppState, -) -> brk_error::Result { +) -> Result { // Phase 1: Search and resolve metadata (cheap) - let resolved = query - .run(move |q| q.resolve(params, MAX_WEIGHT)) - .await?; + let resolved = query.run(move |q| q.resolve(params, MAX_WEIGHT)).await?; let format = resolved.format(); let etag = resolved.etag(); // Check if client has fresh cache if headers.has_etag(etag.as_str()) { - let mut response = (StatusCode::NOT_MODIFIED, "").into_response(); - response.headers_mut().insert_cors(); + let response = (StatusCode::NOT_MODIFIED, "").into_response(); return Ok(response); } @@ -62,9 +55,7 @@ async fn req_to_response_res( Response::new(Body::from(v)) } else { // Phase 2: Format (expensive, only on cache miss) - let metric_output = query - .run(move |q| q.format_legacy(resolved)) - .await?; + let metric_output = query.run(move |q| q.format_legacy(resolved)).await?; match metric_output.output { OutputLegacy::CSV(s) => { @@ -84,7 +75,6 @@ async fn req_to_response_res( }; let headers = response.headers_mut(); - headers.insert_cors(); headers.insert_etag(etag.as_str()); headers.insert_cache_control(CACHE_CONTROL); diff --git a/crates/brk_server/src/api/mining/mod.rs b/crates/brk_server/src/api/mining/mod.rs index 9701df3ad..c88565510 100644 --- a/crates/brk_server/src/api/mining/mod.rs +++ b/crates/brk_server/src/api/mining/mod.rs @@ -6,9 +6,9 @@ use axum::{ routing::get, }; use brk_types::{ - BlockCountParam, BlockFeesEntry, BlockRewardsEntry, BlockSizesWeights, - DifficultyAdjustment, DifficultyAdjustmentEntry, HashrateSummary, PoolDetail, PoolInfo, - PoolSlugParam, PoolsSummary, RewardStats, TimePeriodParam, + BlockCountParam, BlockFeesEntry, BlockRewardsEntry, BlockSizesWeights, DifficultyAdjustment, + DifficultyAdjustmentEntry, HashrateSummary, PoolDetail, PoolInfo, PoolSlugParam, PoolsSummary, + RewardStats, TimePeriodParam, }; use crate::{CacheStrategy, extended::TransformResponseExtended}; diff --git a/crates/brk_server/src/api/openapi/trim.rs b/crates/brk_server/src/api/openapi/trim.rs index f1f783a78..13eb11d16 100644 --- a/crates/brk_server/src/api/openapi/trim.rs +++ b/crates/brk_server/src/api/openapi/trim.rs @@ -377,7 +377,10 @@ mod tests { let props = &parsed["components"]["schemas"]["AddressStats"]["properties"]; assert_eq!(props["address"], "Address", "address should be simplified"); - assert_eq!(props["chain_stats"], "AddressChainStats", "chain_stats should be simplified"); + assert_eq!( + props["chain_stats"], "AddressChainStats", + "chain_stats should be simplified" + ); } #[test] diff --git a/crates/brk_server/src/extended/header_map.rs b/crates/brk_server/src/extended/header_map.rs index 186f972d0..271709c95 100644 --- a/crates/brk_server/src/extended/header_map.rs +++ b/crates/brk_server/src/extended/header_map.rs @@ -19,8 +19,6 @@ pub enum ModifiedState { } pub trait HeaderMapExtended { - fn insert_cors(&mut self); - fn has_etag(&self, etag: &str) -> bool; fn get_if_modified_since(&self) -> Option; @@ -52,11 +50,6 @@ pub trait HeaderMapExtended { } impl HeaderMapExtended for HeaderMap { - fn insert_cors(&mut self) { - self.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*".parse().unwrap()); - self.insert(header::ACCESS_CONTROL_ALLOW_HEADERS, "*".parse().unwrap()); - } - fn insert_cache_control(&mut self, value: &str) { self.insert(header::CACHE_CONTROL, value.parse().unwrap()); } diff --git a/crates/brk_server/src/extended/response.rs b/crates/brk_server/src/extended/response.rs index 2895812fc..dec423dc7 100644 --- a/crates/brk_server/src/extended/response.rs +++ b/crates/brk_server/src/extended/response.rs @@ -36,8 +36,7 @@ where impl ResponseExtended for Response { fn new_not_modified() -> Response { let mut response = (StatusCode::NOT_MODIFIED, "").into_response(); - let headers = response.headers_mut(); - headers.insert_cors(); + let _headers = response.headers_mut(); response } @@ -56,7 +55,6 @@ impl ResponseExtended for Response { let mut response = Response::builder().body(bytes.into()).unwrap(); *response.status_mut() = status; let headers = response.headers_mut(); - headers.insert_cors(); headers.insert_content_type_application_json(); headers.insert_cache_control_must_revalidate(); headers.insert_etag(etag); @@ -68,12 +66,9 @@ impl ResponseExtended for Response { } fn new_text_with(status: StatusCode, value: &str, etag: &str) -> Self { - let mut response = Response::builder() - .body(value.to_string().into()) - .unwrap(); + let mut response = Response::builder().body(value.to_string().into()).unwrap(); *response.status_mut() = status; let headers = response.headers_mut(); - headers.insert_cors(); headers.insert_content_type_text_plain(); headers.insert_cache_control_must_revalidate(); headers.insert_etag(etag); @@ -88,7 +83,6 @@ impl ResponseExtended for Response { let mut response = Response::builder().body(value.into()).unwrap(); *response.status_mut() = status; let headers = response.headers_mut(); - headers.insert_cors(); headers.insert_content_type_octet_stream(); headers.insert_cache_control_must_revalidate(); headers.insert_etag(etag); @@ -102,7 +96,6 @@ impl ResponseExtended for Response { let bytes = serde_json::to_vec(&value).unwrap(); let mut response = Response::builder().body(bytes.into()).unwrap(); let headers = response.headers_mut(); - headers.insert_cors(); headers.insert_content_type_application_json(); headers.insert_cache_control(¶ms.cache_control); if let Some(etag) = ¶ms.etag { @@ -123,11 +116,8 @@ impl ResponseExtended for Response { } fn new_text_cached(value: &str, params: &CacheParams) -> Self { - let mut response = Response::builder() - .body(value.to_string().into()) - .unwrap(); + let mut response = Response::builder().body(value.to_string().into()).unwrap(); let headers = response.headers_mut(); - headers.insert_cors(); headers.insert_content_type_text_plain(); headers.insert_cache_control(¶ms.cache_control); if let Some(etag) = ¶ms.etag { @@ -139,7 +129,6 @@ impl ResponseExtended for Response { fn new_bytes_cached(value: Vec, params: &CacheParams) -> Self { let mut response = Response::builder().body(value.into()).unwrap(); let headers = response.headers_mut(); - headers.insert_cors(); headers.insert_content_type_octet_stream(); headers.insert_cache_control(¶ms.cache_control); if let Some(etag) = ¶ms.etag { diff --git a/crates/brk_server/src/files/file.rs b/crates/brk_server/src/files/file.rs index dde1107fb..80670118f 100644 --- a/crates/brk_server/src/files/file.rs +++ b/crates/brk_server/src/files/file.rs @@ -81,7 +81,6 @@ fn build_response(state: &AppState, path: &Path, content: Vec, cache_key: &s }; let headers = response.headers_mut(); - headers.insert_cors(); headers.insert_content_type(path); if cfg!(debug_assertions) || must_revalidate { @@ -114,9 +113,8 @@ fn embedded_handler(state: &AppState, path: Option) -> Response { }); let Some(file) = file else { - let mut response: Response = + let response: Response = (StatusCode::NOT_FOUND, "File not found".to_string()).into_response(); - response.headers_mut().insert_cors(); return response; }; @@ -147,9 +145,8 @@ fn filesystem_handler( let allowed = canonical.starts_with(&canonical_base) || project_root.is_some_and(|root| canonical.starts_with(root)); if !allowed { - let mut response: Response = + let response: Response = (StatusCode::FORBIDDEN, "Access denied".to_string()).into_response(); - response.headers_mut().insert_cors(); return response; } } @@ -165,12 +162,11 @@ fn filesystem_handler( // SPA fallback if !path.exists() || path.is_dir() { if path.extension().is_some() { - let mut response: Response = ( + let response: Response = ( StatusCode::INTERNAL_SERVER_ERROR, "File doesn't exist".to_string(), ) .into_response(); - response.headers_mut().insert_cors(); return response; } else { path = files_path.join("index.html"); @@ -188,12 +184,7 @@ fn filesystem_handler( fn path_to_response(headers: &HeaderMap, state: &AppState, path: &Path) -> Response { match path_to_response_(headers, state, path) { Ok(response) => response, - Err(error) => { - let mut response = - (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(); - response.headers_mut().insert_cors(); - response - } + Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(), } } diff --git a/crates/brk_server/src/lib.rs b/crates/brk_server/src/lib.rs index da27107e2..679431f76 100644 --- a/crates/brk_server/src/lib.rs +++ b/crates/brk_server/src/lib.rs @@ -1,6 +1,11 @@ #![doc = include_str!("../README.md")] -use std::{panic, path::PathBuf, sync::Arc, time::{Duration, Instant}}; +use std::{ + panic, + path::PathBuf, + sync::Arc, + time::{Duration, Instant}, +}; use aide::axum::ApiRouter; use axum::{ @@ -14,10 +19,14 @@ use axum::{ }; use brk_error::Result; use brk_query::AsyncQuery; -use include_dir::{include_dir, Dir}; +use include_dir::{Dir, include_dir}; use quick_cache::sync::Cache; use tokio::net::TcpListener; -use tower_http::{compression::CompressionLayer, trace::TraceLayer}; +use tower_http::{ + catch_panic::CatchPanicLayer, classify::ServerErrorsFailureClass, + compression::CompressionLayer, cors::CorsLayer, normalize_path::NormalizePathLayer, + timeout::TimeoutLayer, trace::TraceLayer, +}; use tracing::{error, info}; /// Embedded website assets @@ -86,19 +95,25 @@ impl Server { let trace_layer = TraceLayer::new_for_http() .on_request(()) - .on_response(|response: &Response, latency: Duration, _: &tracing::Span| { - let status = response.status().as_u16(); - let uri = response.extensions().get::().unwrap(); - match response.status() { - StatusCode::OK => info!(status, %uri, ?latency), - StatusCode::NOT_MODIFIED - | StatusCode::TEMPORARY_REDIRECT - | StatusCode::PERMANENT_REDIRECT => info!(status, %uri, ?latency), - _ => error!(status, %uri, ?latency), - } - }) + .on_response( + |response: &Response, latency: Duration, _: &tracing::Span| { + let status = response.status().as_u16(); + let uri = response.extensions().get::().unwrap(); + match response.status() { + StatusCode::OK => info!(status, %uri, ?latency), + StatusCode::NOT_MODIFIED + | StatusCode::TEMPORARY_REDIRECT + | StatusCode::PERMANENT_REDIRECT => info!(status, %uri, ?latency), + _ => error!(status, %uri, ?latency), + } + }, + ) .on_body_chunk(()) - .on_failure(()) + .on_failure( + |error: ServerErrorsFailureClass, latency: Duration, _: &tracing::Span| { + error!(?error, ?latency, "request failed"); + }, + ) .on_eos(()); let vecs = state.query.inner().vecs(); @@ -126,9 +141,13 @@ impl Server { ) .route("/nostr", get(Redirect::temporary("https://primal.net/p/npub1jagmm3x39lmwfnrtvxcs9ac7g300y3dusv9lgzhk2e4x5frpxlrqa73v44"))) .with_state(state) + .layer(CatchPanicLayer::new()) .layer(compression_layer) .layer(response_uri_layer) - .layer(trace_layer); + .layer(trace_layer) + .layer(TimeoutLayer::with_status_code(StatusCode::GATEWAY_TIMEOUT, Duration::from_secs(5))) + .layer(CorsLayer::permissive()) + .layer(NormalizePathLayer::trim_trailing_slash()); const BASE_PORT: u16 = 3110; const MAX_PORT: u16 = BASE_PORT + 100; diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index be16fb7ac..8d0c19966 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -1644,59 +1644,6 @@ function createPrice111dSmaPattern(client, acc) { }; } -/** - * @typedef {Object} ActivePriceRatioPattern - * @property {MetricPattern4} ratio - * @property {MetricPattern4} ratio1mSma - * @property {MetricPattern4} ratio1wSma - * @property {Ratio1ySdPattern} ratio1ySd - * @property {Ratio1ySdPattern} ratio2ySd - * @property {Ratio1ySdPattern} ratio4ySd - * @property {MetricPattern4} ratioPct1 - * @property {MetricPattern4} ratioPct1Usd - * @property {MetricPattern4} ratioPct2 - * @property {MetricPattern4} ratioPct2Usd - * @property {MetricPattern4} ratioPct5 - * @property {MetricPattern4} ratioPct5Usd - * @property {MetricPattern4} ratioPct95 - * @property {MetricPattern4} ratioPct95Usd - * @property {MetricPattern4} ratioPct98 - * @property {MetricPattern4} ratioPct98Usd - * @property {MetricPattern4} ratioPct99 - * @property {MetricPattern4} ratioPct99Usd - * @property {Ratio1ySdPattern} ratioSd - */ - -/** - * Create a ActivePriceRatioPattern pattern node - * @param {BrkClientBase} client - * @param {string} acc - Accumulated metric name - * @returns {ActivePriceRatioPattern} - */ -function createActivePriceRatioPattern(client, acc) { - return { - ratio: createMetricPattern4(client, acc), - ratio1mSma: createMetricPattern4(client, _m(acc, '1m_sma')), - ratio1wSma: createMetricPattern4(client, _m(acc, '1w_sma')), - ratio1ySd: createRatio1ySdPattern(client, _m(acc, '1y')), - ratio2ySd: createRatio1ySdPattern(client, _m(acc, '2y')), - ratio4ySd: createRatio1ySdPattern(client, _m(acc, '4y')), - ratioPct1: createMetricPattern4(client, _m(acc, 'pct1')), - ratioPct1Usd: createMetricPattern4(client, _m(acc, 'pct1_usd')), - ratioPct2: createMetricPattern4(client, _m(acc, 'pct2')), - ratioPct2Usd: createMetricPattern4(client, _m(acc, 'pct2_usd')), - ratioPct5: createMetricPattern4(client, _m(acc, 'pct5')), - ratioPct5Usd: createMetricPattern4(client, _m(acc, 'pct5_usd')), - ratioPct95: createMetricPattern4(client, _m(acc, 'pct95')), - ratioPct95Usd: createMetricPattern4(client, _m(acc, 'pct95_usd')), - ratioPct98: createMetricPattern4(client, _m(acc, 'pct98')), - ratioPct98Usd: createMetricPattern4(client, _m(acc, 'pct98_usd')), - ratioPct99: createMetricPattern4(client, _m(acc, 'pct99')), - ratioPct99Usd: createMetricPattern4(client, _m(acc, 'pct99_usd')), - ratioSd: createRatio1ySdPattern(client, acc), - }; -} - /** * @typedef {Object} PercentilesPattern * @property {MetricPattern4} pct05 @@ -1750,6 +1697,59 @@ function createPercentilesPattern(client, acc) { }; } +/** + * @typedef {Object} ActivePriceRatioPattern + * @property {MetricPattern4} ratio + * @property {MetricPattern4} ratio1mSma + * @property {MetricPattern4} ratio1wSma + * @property {Ratio1ySdPattern} ratio1ySd + * @property {Ratio1ySdPattern} ratio2ySd + * @property {Ratio1ySdPattern} ratio4ySd + * @property {MetricPattern4} ratioPct1 + * @property {MetricPattern4} ratioPct1Usd + * @property {MetricPattern4} ratioPct2 + * @property {MetricPattern4} ratioPct2Usd + * @property {MetricPattern4} ratioPct5 + * @property {MetricPattern4} ratioPct5Usd + * @property {MetricPattern4} ratioPct95 + * @property {MetricPattern4} ratioPct95Usd + * @property {MetricPattern4} ratioPct98 + * @property {MetricPattern4} ratioPct98Usd + * @property {MetricPattern4} ratioPct99 + * @property {MetricPattern4} ratioPct99Usd + * @property {Ratio1ySdPattern} ratioSd + */ + +/** + * Create a ActivePriceRatioPattern pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {ActivePriceRatioPattern} + */ +function createActivePriceRatioPattern(client, acc) { + return { + ratio: createMetricPattern4(client, acc), + ratio1mSma: createMetricPattern4(client, _m(acc, '1m_sma')), + ratio1wSma: createMetricPattern4(client, _m(acc, '1w_sma')), + ratio1ySd: createRatio1ySdPattern(client, _m(acc, '1y')), + ratio2ySd: createRatio1ySdPattern(client, _m(acc, '2y')), + ratio4ySd: createRatio1ySdPattern(client, _m(acc, '4y')), + ratioPct1: createMetricPattern4(client, _m(acc, 'pct1')), + ratioPct1Usd: createMetricPattern4(client, _m(acc, 'pct1_usd')), + ratioPct2: createMetricPattern4(client, _m(acc, 'pct2')), + ratioPct2Usd: createMetricPattern4(client, _m(acc, 'pct2_usd')), + ratioPct5: createMetricPattern4(client, _m(acc, 'pct5')), + ratioPct5Usd: createMetricPattern4(client, _m(acc, 'pct5_usd')), + ratioPct95: createMetricPattern4(client, _m(acc, 'pct95')), + ratioPct95Usd: createMetricPattern4(client, _m(acc, 'pct95_usd')), + ratioPct98: createMetricPattern4(client, _m(acc, 'pct98')), + ratioPct98Usd: createMetricPattern4(client, _m(acc, 'pct98_usd')), + ratioPct99: createMetricPattern4(client, _m(acc, 'pct99')), + ratioPct99Usd: createMetricPattern4(client, _m(acc, 'pct99_usd')), + ratioSd: createRatio1ySdPattern(client, acc), + }; +} + /** * @typedef {Object} RelativePattern5 * @property {MetricPattern1} negUnrealizedLossRelToMarketCap @@ -2004,45 +2004,6 @@ function createBitcoinPattern(client, acc) { }; } -/** - * @template T - * @typedef {Object} DollarsPattern - * @property {MetricPattern2} average - * @property {MetricPattern11} base - * @property {MetricPattern1} cumulative - * @property {MetricPattern2} max - * @property {MetricPattern6} median - * @property {MetricPattern2} min - * @property {MetricPattern6} pct10 - * @property {MetricPattern6} pct25 - * @property {MetricPattern6} pct75 - * @property {MetricPattern6} pct90 - * @property {MetricPattern2} sum - */ - -/** - * Create a DollarsPattern pattern node - * @template T - * @param {BrkClientBase} client - * @param {string} acc - Accumulated metric name - * @returns {DollarsPattern} - */ -function createDollarsPattern(client, acc) { - return { - average: createMetricPattern2(client, _m(acc, 'average')), - base: createMetricPattern11(client, acc), - cumulative: createMetricPattern1(client, _m(acc, 'cumulative')), - max: createMetricPattern2(client, _m(acc, 'max')), - median: createMetricPattern6(client, _m(acc, 'median')), - min: createMetricPattern2(client, _m(acc, 'min')), - pct10: createMetricPattern6(client, _m(acc, 'pct10')), - pct25: createMetricPattern6(client, _m(acc, 'pct25')), - pct75: createMetricPattern6(client, _m(acc, 'pct75')), - pct90: createMetricPattern6(client, _m(acc, 'pct90')), - sum: createMetricPattern2(client, _m(acc, 'sum')), - }; -} - /** * @template T * @typedef {Object} ClassAveragePricePattern @@ -2083,37 +2044,41 @@ function createClassAveragePricePattern(client, acc) { } /** - * @typedef {Object} RelativePattern - * @property {MetricPattern1} negUnrealizedLossRelToMarketCap - * @property {MetricPattern1} netUnrealizedPnlRelToMarketCap - * @property {MetricPattern1} nupl - * @property {MetricPattern1} supplyInLossRelToCirculatingSupply - * @property {MetricPattern1} supplyInLossRelToOwnSupply - * @property {MetricPattern1} supplyInProfitRelToCirculatingSupply - * @property {MetricPattern1} supplyInProfitRelToOwnSupply - * @property {MetricPattern4} supplyRelToCirculatingSupply - * @property {MetricPattern1} unrealizedLossRelToMarketCap - * @property {MetricPattern1} unrealizedProfitRelToMarketCap + * @template T + * @typedef {Object} DollarsPattern + * @property {MetricPattern2} average + * @property {MetricPattern11} base + * @property {MetricPattern1} cumulative + * @property {MetricPattern2} max + * @property {MetricPattern6} median + * @property {MetricPattern2} min + * @property {MetricPattern6} pct10 + * @property {MetricPattern6} pct25 + * @property {MetricPattern6} pct75 + * @property {MetricPattern6} pct90 + * @property {MetricPattern2} sum */ /** - * Create a RelativePattern pattern node + * Create a DollarsPattern pattern node + * @template T * @param {BrkClientBase} client * @param {string} acc - Accumulated metric name - * @returns {RelativePattern} + * @returns {DollarsPattern} */ -function createRelativePattern(client, acc) { +function createDollarsPattern(client, acc) { return { - negUnrealizedLossRelToMarketCap: createMetricPattern1(client, _m(acc, 'neg_unrealized_loss_rel_to_market_cap')), - netUnrealizedPnlRelToMarketCap: createMetricPattern1(client, _m(acc, 'net_unrealized_pnl_rel_to_market_cap')), - nupl: createMetricPattern1(client, _m(acc, 'nupl')), - supplyInLossRelToCirculatingSupply: createMetricPattern1(client, _m(acc, 'supply_in_loss_rel_to_circulating_supply')), - supplyInLossRelToOwnSupply: createMetricPattern1(client, _m(acc, 'supply_in_loss_rel_to_own_supply')), - supplyInProfitRelToCirculatingSupply: createMetricPattern1(client, _m(acc, 'supply_in_profit_rel_to_circulating_supply')), - supplyInProfitRelToOwnSupply: createMetricPattern1(client, _m(acc, 'supply_in_profit_rel_to_own_supply')), - supplyRelToCirculatingSupply: createMetricPattern4(client, _m(acc, 'supply_rel_to_circulating_supply')), - unrealizedLossRelToMarketCap: createMetricPattern1(client, _m(acc, 'unrealized_loss_rel_to_market_cap')), - unrealizedProfitRelToMarketCap: createMetricPattern1(client, _m(acc, 'unrealized_profit_rel_to_market_cap')), + average: createMetricPattern2(client, _m(acc, 'average')), + base: createMetricPattern11(client, acc), + cumulative: createMetricPattern1(client, _m(acc, 'cumulative')), + max: createMetricPattern2(client, _m(acc, 'max')), + median: createMetricPattern6(client, _m(acc, 'median')), + min: createMetricPattern2(client, _m(acc, 'min')), + pct10: createMetricPattern6(client, _m(acc, 'pct10')), + pct25: createMetricPattern6(client, _m(acc, 'pct25')), + pct75: createMetricPattern6(client, _m(acc, 'pct75')), + pct90: createMetricPattern6(client, _m(acc, 'pct90')), + sum: createMetricPattern2(client, _m(acc, 'sum')), }; } @@ -2152,6 +2117,41 @@ function createRelativePattern2(client, acc) { }; } +/** + * @typedef {Object} RelativePattern + * @property {MetricPattern1} negUnrealizedLossRelToMarketCap + * @property {MetricPattern1} netUnrealizedPnlRelToMarketCap + * @property {MetricPattern1} nupl + * @property {MetricPattern1} supplyInLossRelToCirculatingSupply + * @property {MetricPattern1} supplyInLossRelToOwnSupply + * @property {MetricPattern1} supplyInProfitRelToCirculatingSupply + * @property {MetricPattern1} supplyInProfitRelToOwnSupply + * @property {MetricPattern4} supplyRelToCirculatingSupply + * @property {MetricPattern1} unrealizedLossRelToMarketCap + * @property {MetricPattern1} unrealizedProfitRelToMarketCap + */ + +/** + * Create a RelativePattern pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {RelativePattern} + */ +function createRelativePattern(client, acc) { + return { + negUnrealizedLossRelToMarketCap: createMetricPattern1(client, _m(acc, 'neg_unrealized_loss_rel_to_market_cap')), + netUnrealizedPnlRelToMarketCap: createMetricPattern1(client, _m(acc, 'net_unrealized_pnl_rel_to_market_cap')), + nupl: createMetricPattern1(client, _m(acc, 'nupl')), + supplyInLossRelToCirculatingSupply: createMetricPattern1(client, _m(acc, 'supply_in_loss_rel_to_circulating_supply')), + supplyInLossRelToOwnSupply: createMetricPattern1(client, _m(acc, 'supply_in_loss_rel_to_own_supply')), + supplyInProfitRelToCirculatingSupply: createMetricPattern1(client, _m(acc, 'supply_in_profit_rel_to_circulating_supply')), + supplyInProfitRelToOwnSupply: createMetricPattern1(client, _m(acc, 'supply_in_profit_rel_to_own_supply')), + supplyRelToCirculatingSupply: createMetricPattern4(client, _m(acc, 'supply_rel_to_circulating_supply')), + unrealizedLossRelToMarketCap: createMetricPattern1(client, _m(acc, 'unrealized_loss_rel_to_market_cap')), + unrealizedProfitRelToMarketCap: createMetricPattern1(client, _m(acc, 'unrealized_profit_rel_to_market_cap')), + }; +} + /** * @template T * @typedef {Object} CountPattern2 @@ -2222,41 +2222,6 @@ function createAddrCountPattern(client, acc) { }; } -/** - * @template T - * @typedef {Object} FeeRatePattern - * @property {MetricPattern1} average - * @property {MetricPattern1} max - * @property {MetricPattern11} median - * @property {MetricPattern1} min - * @property {MetricPattern11} pct10 - * @property {MetricPattern11} pct25 - * @property {MetricPattern11} pct75 - * @property {MetricPattern11} pct90 - * @property {MetricPattern27} txindex - */ - -/** - * Create a FeeRatePattern pattern node - * @template T - * @param {BrkClientBase} client - * @param {string} acc - Accumulated metric name - * @returns {FeeRatePattern} - */ -function createFeeRatePattern(client, acc) { - return { - average: createMetricPattern1(client, _m(acc, 'average')), - max: createMetricPattern1(client, _m(acc, 'max')), - median: createMetricPattern11(client, _m(acc, 'median')), - min: createMetricPattern1(client, _m(acc, 'min')), - pct10: createMetricPattern11(client, _m(acc, 'pct10')), - pct25: createMetricPattern11(client, _m(acc, 'pct25')), - pct75: createMetricPattern11(client, _m(acc, 'pct75')), - pct90: createMetricPattern11(client, _m(acc, 'pct90')), - txindex: createMetricPattern27(client, acc), - }; -} - /** * @template T * @typedef {Object} FullnessPattern @@ -2292,6 +2257,41 @@ function createFullnessPattern(client, acc) { }; } +/** + * @template T + * @typedef {Object} FeeRatePattern + * @property {MetricPattern1} average + * @property {MetricPattern1} max + * @property {MetricPattern11} median + * @property {MetricPattern1} min + * @property {MetricPattern11} pct10 + * @property {MetricPattern11} pct25 + * @property {MetricPattern11} pct75 + * @property {MetricPattern11} pct90 + * @property {MetricPattern27} txindex + */ + +/** + * Create a FeeRatePattern pattern node + * @template T + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {FeeRatePattern} + */ +function createFeeRatePattern(client, acc) { + return { + average: createMetricPattern1(client, _m(acc, 'average')), + max: createMetricPattern1(client, _m(acc, 'max')), + median: createMetricPattern11(client, _m(acc, 'median')), + min: createMetricPattern1(client, _m(acc, 'min')), + pct10: createMetricPattern11(client, _m(acc, 'pct10')), + pct25: createMetricPattern11(client, _m(acc, 'pct25')), + pct75: createMetricPattern11(client, _m(acc, 'pct75')), + pct90: createMetricPattern11(client, _m(acc, 'pct90')), + txindex: createMetricPattern27(client, acc), + }; +} + /** * @typedef {Object} _0satsPattern * @property {ActivityPattern2} activity @@ -2356,35 +2356,6 @@ function createPhaseDailyCentsPattern(client, acc) { }; } -/** - * @typedef {Object} _10yPattern - * @property {ActivityPattern2} activity - * @property {CostBasisPattern} costBasis - * @property {OutputsPattern} outputs - * @property {RealizedPattern4} realized - * @property {RelativePattern} relative - * @property {SupplyPattern2} supply - * @property {UnrealizedPattern} unrealized - */ - -/** - * Create a _10yPattern pattern node - * @param {BrkClientBase} client - * @param {string} acc - Accumulated metric name - * @returns {_10yPattern} - */ -function create_10yPattern(client, acc) { - return { - activity: createActivityPattern2(client, acc), - costBasis: createCostBasisPattern(client, acc), - outputs: createOutputsPattern(client, _m(acc, 'utxo_count')), - realized: createRealizedPattern4(client, acc), - relative: createRelativePattern(client, acc), - supply: createSupplyPattern2(client, _m(acc, 'supply')), - unrealized: createUnrealizedPattern(client, acc), - }; -} - /** * @typedef {Object} UnrealizedPattern * @property {MetricPattern1} negUnrealizedLoss @@ -2415,29 +2386,29 @@ function createUnrealizedPattern(client, acc) { } /** - * @typedef {Object} _0satsPattern2 + * @typedef {Object} _10yTo12yPattern * @property {ActivityPattern2} activity - * @property {CostBasisPattern} costBasis + * @property {CostBasisPattern2} costBasis * @property {OutputsPattern} outputs - * @property {RealizedPattern} realized - * @property {RelativePattern4} relative + * @property {RealizedPattern2} realized + * @property {RelativePattern2} relative * @property {SupplyPattern2} supply * @property {UnrealizedPattern} unrealized */ /** - * Create a _0satsPattern2 pattern node + * Create a _10yTo12yPattern pattern node * @param {BrkClientBase} client * @param {string} acc - Accumulated metric name - * @returns {_0satsPattern2} + * @returns {_10yTo12yPattern} */ -function create_0satsPattern2(client, acc) { +function create_10yTo12yPattern(client, acc) { return { activity: createActivityPattern2(client, acc), - costBasis: createCostBasisPattern(client, acc), + costBasis: createCostBasisPattern2(client, acc), outputs: createOutputsPattern(client, _m(acc, 'utxo_count')), - realized: createRealizedPattern(client, acc), - relative: createRelativePattern4(client, _m(acc, 'supply_in')), + realized: createRealizedPattern2(client, acc), + relative: createRelativePattern2(client, acc), supply: createSupplyPattern2(client, _m(acc, 'supply')), unrealized: createUnrealizedPattern(client, acc), }; @@ -2472,6 +2443,35 @@ function createPeriodCagrPattern(client, acc) { }; } +/** + * @typedef {Object} _10yPattern + * @property {ActivityPattern2} activity + * @property {CostBasisPattern} costBasis + * @property {OutputsPattern} outputs + * @property {RealizedPattern4} realized + * @property {RelativePattern} relative + * @property {SupplyPattern2} supply + * @property {UnrealizedPattern} unrealized + */ + +/** + * Create a _10yPattern pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {_10yPattern} + */ +function create_10yPattern(client, acc) { + return { + activity: createActivityPattern2(client, acc), + costBasis: createCostBasisPattern(client, acc), + outputs: createOutputsPattern(client, _m(acc, 'utxo_count')), + realized: createRealizedPattern4(client, acc), + relative: createRelativePattern(client, acc), + supply: createSupplyPattern2(client, _m(acc, 'supply')), + unrealized: createUnrealizedPattern(client, acc), + }; +} + /** * @typedef {Object} _100btcPattern * @property {ActivityPattern2} activity @@ -2502,29 +2502,29 @@ function create_100btcPattern(client, acc) { } /** - * @typedef {Object} _10yTo12yPattern + * @typedef {Object} _0satsPattern2 * @property {ActivityPattern2} activity - * @property {CostBasisPattern2} costBasis + * @property {CostBasisPattern} costBasis * @property {OutputsPattern} outputs - * @property {RealizedPattern2} realized - * @property {RelativePattern2} relative + * @property {RealizedPattern} realized + * @property {RelativePattern4} relative * @property {SupplyPattern2} supply * @property {UnrealizedPattern} unrealized */ /** - * Create a _10yTo12yPattern pattern node + * Create a _0satsPattern2 pattern node * @param {BrkClientBase} client * @param {string} acc - Accumulated metric name - * @returns {_10yTo12yPattern} + * @returns {_0satsPattern2} */ -function create_10yTo12yPattern(client, acc) { +function create_0satsPattern2(client, acc) { return { activity: createActivityPattern2(client, acc), - costBasis: createCostBasisPattern2(client, acc), + costBasis: createCostBasisPattern(client, acc), outputs: createOutputsPattern(client, _m(acc, 'utxo_count')), - realized: createRealizedPattern2(client, acc), - relative: createRelativePattern2(client, acc), + realized: createRealizedPattern(client, acc), + relative: createRelativePattern4(client, _m(acc, 'supply_in')), supply: createSupplyPattern2(client, _m(acc, 'supply')), unrealized: createUnrealizedPattern(client, acc), }; @@ -2580,6 +2580,48 @@ function createSplitPattern2(client, acc) { }; } +/** + * @typedef {Object} _2015Pattern + * @property {MetricPattern4} bitcoin + * @property {MetricPattern4} dollars + * @property {MetricPattern4} sats + */ + +/** + * Create a _2015Pattern pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {_2015Pattern} + */ +function create_2015Pattern(client, acc) { + return { + bitcoin: createMetricPattern4(client, _m(acc, 'btc')), + dollars: createMetricPattern4(client, _m(acc, 'usd')), + sats: createMetricPattern4(client, acc), + }; +} + +/** + * @typedef {Object} ActiveSupplyPattern + * @property {MetricPattern1} bitcoin + * @property {MetricPattern1} dollars + * @property {MetricPattern1} sats + */ + +/** + * Create a ActiveSupplyPattern pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {ActiveSupplyPattern} + */ +function createActiveSupplyPattern(client, acc) { + return { + bitcoin: createMetricPattern1(client, _m(acc, 'btc')), + dollars: createMetricPattern1(client, _m(acc, 'usd')), + sats: createMetricPattern1(client, acc), + }; +} + /** * @typedef {Object} CostBasisPattern2 * @property {MetricPattern1} max @@ -2601,6 +2643,27 @@ function createCostBasisPattern2(client, acc) { }; } +/** + * @typedef {Object} CoinbasePattern2 + * @property {BlockCountPattern} bitcoin + * @property {BlockCountPattern} dollars + * @property {BlockCountPattern} sats + */ + +/** + * Create a CoinbasePattern2 pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {CoinbasePattern2} + */ +function createCoinbasePattern2(client, acc) { + return { + bitcoin: createBlockCountPattern(client, _m(acc, 'btc')), + dollars: createBlockCountPattern(client, _m(acc, 'usd')), + sats: createBlockCountPattern(client, acc), + }; +} + /** * @typedef {Object} SegwitAdoptionPattern * @property {MetricPattern11} base @@ -2643,69 +2706,6 @@ function createCoinbasePattern(client, acc) { }; } -/** - * @typedef {Object} _2015Pattern - * @property {MetricPattern4} bitcoin - * @property {MetricPattern4} dollars - * @property {MetricPattern4} sats - */ - -/** - * Create a _2015Pattern pattern node - * @param {BrkClientBase} client - * @param {string} acc - Accumulated metric name - * @returns {_2015Pattern} - */ -function create_2015Pattern(client, acc) { - return { - bitcoin: createMetricPattern4(client, _m(acc, 'btc')), - dollars: createMetricPattern4(client, _m(acc, 'usd')), - sats: createMetricPattern4(client, acc), - }; -} - -/** - * @typedef {Object} CoinbasePattern2 - * @property {BlockCountPattern} bitcoin - * @property {BlockCountPattern} dollars - * @property {BlockCountPattern} sats - */ - -/** - * Create a CoinbasePattern2 pattern node - * @param {BrkClientBase} client - * @param {string} acc - Accumulated metric name - * @returns {CoinbasePattern2} - */ -function createCoinbasePattern2(client, acc) { - return { - bitcoin: createBlockCountPattern(client, _m(acc, 'btc')), - dollars: createBlockCountPattern(client, _m(acc, 'usd')), - sats: createBlockCountPattern(client, acc), - }; -} - -/** - * @typedef {Object} ActiveSupplyPattern - * @property {MetricPattern1} bitcoin - * @property {MetricPattern1} dollars - * @property {MetricPattern1} sats - */ - -/** - * Create a ActiveSupplyPattern pattern node - * @param {BrkClientBase} client - * @param {string} acc - Accumulated metric name - * @returns {ActiveSupplyPattern} - */ -function createActiveSupplyPattern(client, acc) { - return { - bitcoin: createMetricPattern1(client, _m(acc, 'btc')), - dollars: createMetricPattern1(client, _m(acc, 'usd')), - sats: createMetricPattern1(client, acc), - }; -} - /** * @typedef {Object} UnclaimedRewardsPattern * @property {BitcoinPattern2} bitcoin @@ -2727,25 +2727,6 @@ function createUnclaimedRewardsPattern(client, acc) { }; } -/** - * @typedef {Object} _1dReturns1mSdPattern - * @property {MetricPattern4} sd - * @property {MetricPattern4} sma - */ - -/** - * Create a _1dReturns1mSdPattern pattern node - * @param {BrkClientBase} client - * @param {string} acc - Accumulated metric name - * @returns {_1dReturns1mSdPattern} - */ -function create_1dReturns1mSdPattern(client, acc) { - return { - sd: createMetricPattern4(client, _m(acc, 'sd')), - sma: createMetricPattern4(client, _m(acc, 'sma')), - }; -} - /** * @typedef {Object} SupplyPattern2 * @property {ActiveSupplyPattern} halved @@ -2803,6 +2784,25 @@ function createCostBasisPattern(client, acc) { }; } +/** + * @typedef {Object} _1dReturns1mSdPattern + * @property {MetricPattern4} sd + * @property {MetricPattern4} sma + */ + +/** + * Create a _1dReturns1mSdPattern pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {_1dReturns1mSdPattern} + */ +function create_1dReturns1mSdPattern(client, acc) { + return { + sd: createMetricPattern4(client, _m(acc, 'sd')), + sma: createMetricPattern4(client, _m(acc, 'sma')), + }; +} + /** * @template T * @typedef {Object} BitcoinPattern2 @@ -2861,25 +2861,8 @@ function createBlockCountPattern(client, acc) { */ function createSatsPattern(client, acc) { return { - ohlc: createMetricPattern1(client, _m(acc, 'ohlc_sats')), - split: createSplitPattern2(client, _m(acc, 'sats')), - }; -} - -/** - * @typedef {Object} OutputsPattern - * @property {MetricPattern1} utxoCount - */ - -/** - * Create a OutputsPattern pattern node - * @param {BrkClientBase} client - * @param {string} acc - Accumulated metric name - * @returns {OutputsPattern} - */ -function createOutputsPattern(client, acc) { - return { - utxoCount: createMetricPattern1(client, acc), + ohlc: createMetricPattern1(client, _m(acc, 'ohlc')), + split: createSplitPattern2(client, acc), }; } @@ -2900,6 +2883,23 @@ function createRealizedPriceExtraPattern(client, acc) { }; } +/** + * @typedef {Object} OutputsPattern + * @property {MetricPattern1} utxoCount + */ + +/** + * Create a OutputsPattern pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated metric name + * @returns {OutputsPattern} + */ +function createOutputsPattern(client, acc) { + return { + utxoCount: createMetricPattern1(client, acc), + }; +} + // Catalog tree typedefs /** @@ -4055,8 +4055,8 @@ function createRealizedPriceExtraPattern(client, acc) { * @typedef {Object} MetricsTree_Price * @property {MetricsTree_Price_Cents} cents * @property {MetricsTree_Price_Oracle} oracle - * @property {SatsPattern} sats - * @property {MetricsTree_Price_Usd} usd + * @property {MetricsTree_Price_Sats} sats + * @property {SatsPattern} usd */ /** @@ -4090,9 +4090,9 @@ function createRealizedPriceExtraPattern(client, acc) { */ /** - * @typedef {Object} MetricsTree_Price_Usd - * @property {MetricPattern1} ohlc - * @property {SplitPattern2} split + * @typedef {Object} MetricsTree_Price_Sats + * @property {MetricPattern1} ohlc + * @property {SplitPattern2} split */ /** @@ -5637,7 +5637,7 @@ class BrkClient extends BrkClientBase { identity: createMetricPattern10(this, 'halvingepoch'), }, height: { - dateindex: createMetricPattern11(this, 'height_dateindex'), + dateindex: createMetricPattern11(this, 'dateindex'), difficultyepoch: createMetricPattern11(this, 'difficultyepoch'), halvingepoch: createMetricPattern11(this, 'halvingepoch'), identity: createMetricPattern11(this, 'height'), @@ -6058,11 +6058,11 @@ class BrkClient extends BrkClientBase { priceCents: createMetricPattern11(this, 'oracle_price_cents'), txCount: createMetricPattern6(this, 'oracle_tx_count'), }, - sats: createSatsPattern(this, 'price'), - usd: { - ohlc: createMetricPattern1(this, 'price_ohlc'), - split: createSplitPattern2(this, 'price'), + sats: { + ohlc: createMetricPattern1(this, 'price_ohlc_sats'), + split: createSplitPattern2(this, 'price_sats'), }, + usd: createSatsPattern(this, 'price'), }, scripts: { count: { diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index 6529967e6..d47066097 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -1882,31 +1882,6 @@ class Price111dSmaPattern: self.ratio_pct99_usd: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, 'ratio_pct99_usd')) self.ratio_sd: Ratio1ySdPattern = Ratio1ySdPattern(client, _m(acc, 'ratio')) -class ActivePriceRatioPattern: - """Pattern struct for repeated tree structure.""" - - def __init__(self, client: BrkClientBase, acc: str): - """Create pattern node with accumulated metric name.""" - self.ratio: MetricPattern4[StoredF32] = MetricPattern4(client, acc) - self.ratio_1m_sma: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, '1m_sma')) - self.ratio_1w_sma: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, '1w_sma')) - self.ratio_1y_sd: Ratio1ySdPattern = Ratio1ySdPattern(client, _m(acc, '1y')) - self.ratio_2y_sd: Ratio1ySdPattern = Ratio1ySdPattern(client, _m(acc, '2y')) - self.ratio_4y_sd: Ratio1ySdPattern = Ratio1ySdPattern(client, _m(acc, '4y')) - self.ratio_pct1: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, 'pct1')) - self.ratio_pct1_usd: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, 'pct1_usd')) - self.ratio_pct2: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, 'pct2')) - self.ratio_pct2_usd: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, 'pct2_usd')) - self.ratio_pct5: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, 'pct5')) - self.ratio_pct5_usd: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, 'pct5_usd')) - self.ratio_pct95: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, 'pct95')) - self.ratio_pct95_usd: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, 'pct95_usd')) - self.ratio_pct98: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, 'pct98')) - self.ratio_pct98_usd: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, 'pct98_usd')) - self.ratio_pct99: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, 'pct99')) - self.ratio_pct99_usd: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, 'pct99_usd')) - self.ratio_sd: Ratio1ySdPattern = Ratio1ySdPattern(client, acc) - class PercentilesPattern: """Pattern struct for repeated tree structure.""" @@ -1932,6 +1907,31 @@ class PercentilesPattern: self.pct90: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, 'pct90')) self.pct95: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, 'pct95')) +class ActivePriceRatioPattern: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClientBase, acc: str): + """Create pattern node with accumulated metric name.""" + self.ratio: MetricPattern4[StoredF32] = MetricPattern4(client, acc) + self.ratio_1m_sma: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, '1m_sma')) + self.ratio_1w_sma: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, '1w_sma')) + self.ratio_1y_sd: Ratio1ySdPattern = Ratio1ySdPattern(client, _m(acc, '1y')) + self.ratio_2y_sd: Ratio1ySdPattern = Ratio1ySdPattern(client, _m(acc, '2y')) + self.ratio_4y_sd: Ratio1ySdPattern = Ratio1ySdPattern(client, _m(acc, '4y')) + self.ratio_pct1: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, 'pct1')) + self.ratio_pct1_usd: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, 'pct1_usd')) + self.ratio_pct2: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, 'pct2')) + self.ratio_pct2_usd: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, 'pct2_usd')) + self.ratio_pct5: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, 'pct5')) + self.ratio_pct5_usd: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, 'pct5_usd')) + self.ratio_pct95: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, 'pct95')) + self.ratio_pct95_usd: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, 'pct95_usd')) + self.ratio_pct98: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, 'pct98')) + self.ratio_pct98_usd: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, 'pct98_usd')) + self.ratio_pct99: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, 'pct99')) + self.ratio_pct99_usd: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, 'pct99_usd')) + self.ratio_sd: Ratio1ySdPattern = Ratio1ySdPattern(client, acc) + class RelativePattern5: """Pattern struct for repeated tree structure.""" @@ -2048,23 +2048,6 @@ class BitcoinPattern: self.pct90: MetricPattern6[Bitcoin] = MetricPattern6(client, _m(acc, 'pct90')) self.sum: MetricPattern2[Bitcoin] = MetricPattern2(client, _m(acc, 'sum')) -class DollarsPattern(Generic[T]): - """Pattern struct for repeated tree structure.""" - - def __init__(self, client: BrkClientBase, acc: str): - """Create pattern node with accumulated metric name.""" - self.average: MetricPattern2[T] = MetricPattern2(client, _m(acc, 'average')) - self.base: MetricPattern11[T] = MetricPattern11(client, acc) - self.cumulative: MetricPattern1[T] = MetricPattern1(client, _m(acc, 'cumulative')) - self.max: MetricPattern2[T] = MetricPattern2(client, _m(acc, 'max')) - self.median: MetricPattern6[T] = MetricPattern6(client, _m(acc, 'median')) - self.min: MetricPattern2[T] = MetricPattern2(client, _m(acc, 'min')) - self.pct10: MetricPattern6[T] = MetricPattern6(client, _m(acc, 'pct10')) - self.pct25: MetricPattern6[T] = MetricPattern6(client, _m(acc, 'pct25')) - self.pct75: MetricPattern6[T] = MetricPattern6(client, _m(acc, 'pct75')) - self.pct90: MetricPattern6[T] = MetricPattern6(client, _m(acc, 'pct90')) - self.sum: MetricPattern2[T] = MetricPattern2(client, _m(acc, 'sum')) - class ClassAveragePricePattern(Generic[T]): """Pattern struct for repeated tree structure.""" @@ -2082,21 +2065,22 @@ class ClassAveragePricePattern(Generic[T]): self._2024: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2024_average_price')) self._2025: MetricPattern4[T] = MetricPattern4(client, _m(acc, '2025_average_price')) -class RelativePattern: +class DollarsPattern(Generic[T]): """Pattern struct for repeated tree structure.""" def __init__(self, client: BrkClientBase, acc: str): """Create pattern node with accumulated metric name.""" - self.neg_unrealized_loss_rel_to_market_cap: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'neg_unrealized_loss_rel_to_market_cap')) - self.net_unrealized_pnl_rel_to_market_cap: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'net_unrealized_pnl_rel_to_market_cap')) - self.nupl: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'nupl')) - self.supply_in_loss_rel_to_circulating_supply: MetricPattern1[StoredF64] = MetricPattern1(client, _m(acc, 'supply_in_loss_rel_to_circulating_supply')) - self.supply_in_loss_rel_to_own_supply: MetricPattern1[StoredF64] = MetricPattern1(client, _m(acc, 'supply_in_loss_rel_to_own_supply')) - self.supply_in_profit_rel_to_circulating_supply: MetricPattern1[StoredF64] = MetricPattern1(client, _m(acc, 'supply_in_profit_rel_to_circulating_supply')) - self.supply_in_profit_rel_to_own_supply: MetricPattern1[StoredF64] = MetricPattern1(client, _m(acc, 'supply_in_profit_rel_to_own_supply')) - self.supply_rel_to_circulating_supply: MetricPattern4[StoredF64] = MetricPattern4(client, _m(acc, 'supply_rel_to_circulating_supply')) - self.unrealized_loss_rel_to_market_cap: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'unrealized_loss_rel_to_market_cap')) - self.unrealized_profit_rel_to_market_cap: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'unrealized_profit_rel_to_market_cap')) + self.average: MetricPattern2[T] = MetricPattern2(client, _m(acc, 'average')) + self.base: MetricPattern11[T] = MetricPattern11(client, acc) + self.cumulative: MetricPattern1[T] = MetricPattern1(client, _m(acc, 'cumulative')) + self.max: MetricPattern2[T] = MetricPattern2(client, _m(acc, 'max')) + self.median: MetricPattern6[T] = MetricPattern6(client, _m(acc, 'median')) + self.min: MetricPattern2[T] = MetricPattern2(client, _m(acc, 'min')) + self.pct10: MetricPattern6[T] = MetricPattern6(client, _m(acc, 'pct10')) + self.pct25: MetricPattern6[T] = MetricPattern6(client, _m(acc, 'pct25')) + self.pct75: MetricPattern6[T] = MetricPattern6(client, _m(acc, 'pct75')) + self.pct90: MetricPattern6[T] = MetricPattern6(client, _m(acc, 'pct90')) + self.sum: MetricPattern2[T] = MetricPattern2(client, _m(acc, 'sum')) class RelativePattern2: """Pattern struct for repeated tree structure.""" @@ -2114,6 +2098,22 @@ class RelativePattern2: self.unrealized_profit_rel_to_own_market_cap: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'unrealized_profit_rel_to_own_market_cap')) self.unrealized_profit_rel_to_own_total_unrealized_pnl: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'unrealized_profit_rel_to_own_total_unrealized_pnl')) +class RelativePattern: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClientBase, acc: str): + """Create pattern node with accumulated metric name.""" + self.neg_unrealized_loss_rel_to_market_cap: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'neg_unrealized_loss_rel_to_market_cap')) + self.net_unrealized_pnl_rel_to_market_cap: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'net_unrealized_pnl_rel_to_market_cap')) + self.nupl: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'nupl')) + self.supply_in_loss_rel_to_circulating_supply: MetricPattern1[StoredF64] = MetricPattern1(client, _m(acc, 'supply_in_loss_rel_to_circulating_supply')) + self.supply_in_loss_rel_to_own_supply: MetricPattern1[StoredF64] = MetricPattern1(client, _m(acc, 'supply_in_loss_rel_to_own_supply')) + self.supply_in_profit_rel_to_circulating_supply: MetricPattern1[StoredF64] = MetricPattern1(client, _m(acc, 'supply_in_profit_rel_to_circulating_supply')) + self.supply_in_profit_rel_to_own_supply: MetricPattern1[StoredF64] = MetricPattern1(client, _m(acc, 'supply_in_profit_rel_to_own_supply')) + self.supply_rel_to_circulating_supply: MetricPattern4[StoredF64] = MetricPattern4(client, _m(acc, 'supply_rel_to_circulating_supply')) + self.unrealized_loss_rel_to_market_cap: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'unrealized_loss_rel_to_market_cap')) + self.unrealized_profit_rel_to_market_cap: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'unrealized_profit_rel_to_market_cap')) + class CountPattern2(Generic[T]): """Pattern struct for repeated tree structure.""" @@ -2145,21 +2145,6 @@ class AddrCountPattern: self.p2wpkh: MetricPattern1[StoredU64] = MetricPattern1(client, _p('p2wpkh', acc)) self.p2wsh: MetricPattern1[StoredU64] = MetricPattern1(client, _p('p2wsh', acc)) -class FeeRatePattern(Generic[T]): - """Pattern struct for repeated tree structure.""" - - def __init__(self, client: BrkClientBase, acc: str): - """Create pattern node with accumulated metric name.""" - self.average: MetricPattern1[T] = MetricPattern1(client, _m(acc, 'average')) - self.max: MetricPattern1[T] = MetricPattern1(client, _m(acc, 'max')) - self.median: MetricPattern11[T] = MetricPattern11(client, _m(acc, 'median')) - self.min: MetricPattern1[T] = MetricPattern1(client, _m(acc, 'min')) - self.pct10: MetricPattern11[T] = MetricPattern11(client, _m(acc, 'pct10')) - self.pct25: MetricPattern11[T] = MetricPattern11(client, _m(acc, 'pct25')) - self.pct75: MetricPattern11[T] = MetricPattern11(client, _m(acc, 'pct75')) - self.pct90: MetricPattern11[T] = MetricPattern11(client, _m(acc, 'pct90')) - self.txindex: MetricPattern27[T] = MetricPattern27(client, acc) - class FullnessPattern(Generic[T]): """Pattern struct for repeated tree structure.""" @@ -2175,6 +2160,21 @@ class FullnessPattern(Generic[T]): self.pct75: MetricPattern6[T] = MetricPattern6(client, _m(acc, 'pct75')) self.pct90: MetricPattern6[T] = MetricPattern6(client, _m(acc, 'pct90')) +class FeeRatePattern(Generic[T]): + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClientBase, acc: str): + """Create pattern node with accumulated metric name.""" + self.average: MetricPattern1[T] = MetricPattern1(client, _m(acc, 'average')) + self.max: MetricPattern1[T] = MetricPattern1(client, _m(acc, 'max')) + self.median: MetricPattern11[T] = MetricPattern11(client, _m(acc, 'median')) + self.min: MetricPattern1[T] = MetricPattern1(client, _m(acc, 'min')) + self.pct10: MetricPattern11[T] = MetricPattern11(client, _m(acc, 'pct10')) + self.pct25: MetricPattern11[T] = MetricPattern11(client, _m(acc, 'pct25')) + self.pct75: MetricPattern11[T] = MetricPattern11(client, _m(acc, 'pct75')) + self.pct90: MetricPattern11[T] = MetricPattern11(client, _m(acc, 'pct90')) + self.txindex: MetricPattern27[T] = MetricPattern27(client, acc) + class _0satsPattern: """Pattern struct for repeated tree structure.""" @@ -2203,19 +2203,6 @@ class PhaseDailyCentsPattern(Generic[T]): self.pct75: MetricPattern6[T] = MetricPattern6(client, _m(acc, 'pct75')) self.pct90: MetricPattern6[T] = MetricPattern6(client, _m(acc, 'pct90')) -class _10yPattern: - """Pattern struct for repeated tree structure.""" - - def __init__(self, client: BrkClientBase, acc: str): - """Create pattern node with accumulated metric name.""" - self.activity: ActivityPattern2 = ActivityPattern2(client, acc) - self.cost_basis: CostBasisPattern = CostBasisPattern(client, acc) - self.outputs: OutputsPattern = OutputsPattern(client, _m(acc, 'utxo_count')) - self.realized: RealizedPattern4 = RealizedPattern4(client, acc) - self.relative: RelativePattern = RelativePattern(client, acc) - self.supply: SupplyPattern2 = SupplyPattern2(client, _m(acc, 'supply')) - self.unrealized: UnrealizedPattern = UnrealizedPattern(client, acc) - class UnrealizedPattern: """Pattern struct for repeated tree structure.""" @@ -2229,16 +2216,16 @@ class UnrealizedPattern: self.unrealized_loss: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'unrealized_loss')) self.unrealized_profit: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'unrealized_profit')) -class _0satsPattern2: +class _10yTo12yPattern: """Pattern struct for repeated tree structure.""" def __init__(self, client: BrkClientBase, acc: str): """Create pattern node with accumulated metric name.""" self.activity: ActivityPattern2 = ActivityPattern2(client, acc) - self.cost_basis: CostBasisPattern = CostBasisPattern(client, acc) + self.cost_basis: CostBasisPattern2 = CostBasisPattern2(client, acc) self.outputs: OutputsPattern = OutputsPattern(client, _m(acc, 'utxo_count')) - self.realized: RealizedPattern = RealizedPattern(client, acc) - self.relative: RelativePattern4 = RelativePattern4(client, _m(acc, 'supply_in')) + self.realized: RealizedPattern2 = RealizedPattern2(client, acc) + self.relative: RelativePattern2 = RelativePattern2(client, acc) self.supply: SupplyPattern2 = SupplyPattern2(client, _m(acc, 'supply')) self.unrealized: UnrealizedPattern = UnrealizedPattern(client, acc) @@ -2255,6 +2242,19 @@ class PeriodCagrPattern: self._6y: MetricPattern4[StoredF32] = MetricPattern4(client, _p('6y', acc)) self._8y: MetricPattern4[StoredF32] = MetricPattern4(client, _p('8y', acc)) +class _10yPattern: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClientBase, acc: str): + """Create pattern node with accumulated metric name.""" + self.activity: ActivityPattern2 = ActivityPattern2(client, acc) + self.cost_basis: CostBasisPattern = CostBasisPattern(client, acc) + self.outputs: OutputsPattern = OutputsPattern(client, _m(acc, 'utxo_count')) + self.realized: RealizedPattern4 = RealizedPattern4(client, acc) + self.relative: RelativePattern = RelativePattern(client, acc) + self.supply: SupplyPattern2 = SupplyPattern2(client, _m(acc, 'supply')) + self.unrealized: UnrealizedPattern = UnrealizedPattern(client, acc) + class _100btcPattern: """Pattern struct for repeated tree structure.""" @@ -2268,16 +2268,16 @@ class _100btcPattern: self.supply: SupplyPattern2 = SupplyPattern2(client, _m(acc, 'supply')) self.unrealized: UnrealizedPattern = UnrealizedPattern(client, acc) -class _10yTo12yPattern: +class _0satsPattern2: """Pattern struct for repeated tree structure.""" def __init__(self, client: BrkClientBase, acc: str): """Create pattern node with accumulated metric name.""" self.activity: ActivityPattern2 = ActivityPattern2(client, acc) - self.cost_basis: CostBasisPattern2 = CostBasisPattern2(client, acc) + self.cost_basis: CostBasisPattern = CostBasisPattern(client, acc) self.outputs: OutputsPattern = OutputsPattern(client, _m(acc, 'utxo_count')) - self.realized: RealizedPattern2 = RealizedPattern2(client, acc) - self.relative: RelativePattern2 = RelativePattern2(client, acc) + self.realized: RealizedPattern = RealizedPattern(client, acc) + self.relative: RelativePattern4 = RelativePattern4(client, _m(acc, 'supply_in')) self.supply: SupplyPattern2 = SupplyPattern2(client, _m(acc, 'supply')) self.unrealized: UnrealizedPattern = UnrealizedPattern(client, acc) @@ -2302,6 +2302,24 @@ class SplitPattern2(Generic[T]): self.low: MetricPattern1[T] = MetricPattern1(client, _m(acc, 'low')) self.open: MetricPattern1[T] = MetricPattern1(client, _m(acc, 'open')) +class _2015Pattern: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClientBase, acc: str): + """Create pattern node with accumulated metric name.""" + self.bitcoin: MetricPattern4[Bitcoin] = MetricPattern4(client, _m(acc, 'btc')) + self.dollars: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, 'usd')) + self.sats: MetricPattern4[Sats] = MetricPattern4(client, acc) + +class ActiveSupplyPattern: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClientBase, acc: str): + """Create pattern node with accumulated metric name.""" + self.bitcoin: MetricPattern1[Bitcoin] = MetricPattern1(client, _m(acc, 'btc')) + self.dollars: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'usd')) + self.sats: MetricPattern1[Sats] = MetricPattern1(client, acc) + class CostBasisPattern2: """Pattern struct for repeated tree structure.""" @@ -2311,6 +2329,15 @@ class CostBasisPattern2: self.min: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'min_cost_basis')) self.percentiles: PercentilesPattern = PercentilesPattern(client, _m(acc, 'cost_basis')) +class CoinbasePattern2: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClientBase, acc: str): + """Create pattern node with accumulated metric name.""" + self.bitcoin: BlockCountPattern[Bitcoin] = BlockCountPattern(client, _m(acc, 'btc')) + self.dollars: BlockCountPattern[Dollars] = BlockCountPattern(client, _m(acc, 'usd')) + self.sats: BlockCountPattern[Sats] = BlockCountPattern(client, acc) + class SegwitAdoptionPattern: """Pattern struct for repeated tree structure.""" @@ -2329,33 +2356,6 @@ class CoinbasePattern: self.dollars: DollarsPattern[Dollars] = DollarsPattern(client, _m(acc, 'usd')) self.sats: DollarsPattern[Sats] = DollarsPattern(client, acc) -class _2015Pattern: - """Pattern struct for repeated tree structure.""" - - def __init__(self, client: BrkClientBase, acc: str): - """Create pattern node with accumulated metric name.""" - self.bitcoin: MetricPattern4[Bitcoin] = MetricPattern4(client, _m(acc, 'btc')) - self.dollars: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, 'usd')) - self.sats: MetricPattern4[Sats] = MetricPattern4(client, acc) - -class CoinbasePattern2: - """Pattern struct for repeated tree structure.""" - - def __init__(self, client: BrkClientBase, acc: str): - """Create pattern node with accumulated metric name.""" - self.bitcoin: BlockCountPattern[Bitcoin] = BlockCountPattern(client, _m(acc, 'btc')) - self.dollars: BlockCountPattern[Dollars] = BlockCountPattern(client, _m(acc, 'usd')) - self.sats: BlockCountPattern[Sats] = BlockCountPattern(client, acc) - -class ActiveSupplyPattern: - """Pattern struct for repeated tree structure.""" - - def __init__(self, client: BrkClientBase, acc: str): - """Create pattern node with accumulated metric name.""" - self.bitcoin: MetricPattern1[Bitcoin] = MetricPattern1(client, _m(acc, 'btc')) - self.dollars: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'usd')) - self.sats: MetricPattern1[Sats] = MetricPattern1(client, acc) - class UnclaimedRewardsPattern: """Pattern struct for repeated tree structure.""" @@ -2365,14 +2365,6 @@ class UnclaimedRewardsPattern: self.dollars: BlockCountPattern[Dollars] = BlockCountPattern(client, _m(acc, 'usd')) self.sats: BlockCountPattern[Sats] = BlockCountPattern(client, acc) -class _1dReturns1mSdPattern: - """Pattern struct for repeated tree structure.""" - - def __init__(self, client: BrkClientBase, acc: str): - """Create pattern node with accumulated metric name.""" - self.sd: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, 'sd')) - self.sma: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, 'sma')) - class SupplyPattern2: """Pattern struct for repeated tree structure.""" @@ -2397,6 +2389,14 @@ class CostBasisPattern: self.max: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'max_cost_basis')) self.min: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, 'min_cost_basis')) +class _1dReturns1mSdPattern: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClientBase, acc: str): + """Create pattern node with accumulated metric name.""" + self.sd: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, 'sd')) + self.sma: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, 'sma')) + class BitcoinPattern2(Generic[T]): """Pattern struct for repeated tree structure.""" @@ -2418,15 +2418,8 @@ class SatsPattern(Generic[T]): def __init__(self, client: BrkClientBase, acc: str): """Create pattern node with accumulated metric name.""" - self.ohlc: MetricPattern1[T] = MetricPattern1(client, _m(acc, 'ohlc_sats')) - self.split: SplitPattern2[T] = SplitPattern2(client, _m(acc, 'sats')) - -class OutputsPattern: - """Pattern struct for repeated tree structure.""" - - def __init__(self, client: BrkClientBase, acc: str): - """Create pattern node with accumulated metric name.""" - self.utxo_count: MetricPattern1[StoredU64] = MetricPattern1(client, acc) + self.ohlc: MetricPattern1[T] = MetricPattern1(client, _m(acc, 'ohlc')) + self.split: SplitPattern2[T] = SplitPattern2(client, acc) class RealizedPriceExtraPattern: """Pattern struct for repeated tree structure.""" @@ -2435,6 +2428,13 @@ class RealizedPriceExtraPattern: """Create pattern node with accumulated metric name.""" self.ratio: MetricPattern4[StoredF32] = MetricPattern4(client, acc) +class OutputsPattern: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClientBase, acc: str): + """Create pattern node with accumulated metric name.""" + self.utxo_count: MetricPattern1[StoredU64] = MetricPattern1(client, acc) + # Metrics tree classes class MetricsTree_Addresses: @@ -3144,7 +3144,7 @@ class MetricsTree_Indexes_Height: """Metrics tree node.""" def __init__(self, client: BrkClientBase, base_path: str = ''): - self.dateindex: MetricPattern11[DateIndex] = MetricPattern11(client, 'height_dateindex') + self.dateindex: MetricPattern11[DateIndex] = MetricPattern11(client, 'dateindex') self.difficultyepoch: MetricPattern11[DifficultyEpoch] = MetricPattern11(client, 'difficultyepoch') self.halvingepoch: MetricPattern11[HalvingEpoch] = MetricPattern11(client, 'halvingepoch') self.identity: MetricPattern11[Height] = MetricPattern11(client, 'height') @@ -3687,12 +3687,12 @@ class MetricsTree_Price_Oracle: self.price_cents: MetricPattern11[Cents] = MetricPattern11(client, 'oracle_price_cents') self.tx_count: MetricPattern6[StoredU32] = MetricPattern6(client, 'oracle_tx_count') -class MetricsTree_Price_Usd: +class MetricsTree_Price_Sats: """Metrics tree node.""" def __init__(self, client: BrkClientBase, base_path: str = ''): - self.ohlc: MetricPattern1[OHLCDollars] = MetricPattern1(client, 'price_ohlc') - self.split: SplitPattern2[Dollars] = SplitPattern2(client, 'price') + self.ohlc: MetricPattern1[OHLCSats] = MetricPattern1(client, 'price_ohlc_sats') + self.split: SplitPattern2[Sats] = SplitPattern2(client, 'price_sats') class MetricsTree_Price: """Metrics tree node.""" @@ -3700,8 +3700,8 @@ class MetricsTree_Price: def __init__(self, client: BrkClientBase, base_path: str = ''): self.cents: MetricsTree_Price_Cents = MetricsTree_Price_Cents(client) self.oracle: MetricsTree_Price_Oracle = MetricsTree_Price_Oracle(client) - self.sats: SatsPattern[OHLCSats] = SatsPattern(client, 'price') - self.usd: MetricsTree_Price_Usd = MetricsTree_Price_Usd(client) + self.sats: MetricsTree_Price_Sats = MetricsTree_Price_Sats(client) + self.usd: SatsPattern[OHLCDollars] = SatsPattern(client, 'price') class MetricsTree_Scripts_Count: """Metrics tree node.""" diff --git a/research/analyze_price_signals.py b/research/analyze_price_signals.py new file mode 100644 index 000000000..7134d9643 --- /dev/null +++ b/research/analyze_price_signals.py @@ -0,0 +1,1081 @@ +#!/usr/bin/env python3 +""" +Analyze ALL outputs to find characteristics that correlate with accurate price signals. +Uses txoutindex directly - no pre-filtering. +""" + +import urllib.request +import http.client +import json +import math +import bisect +from collections import defaultdict +from dataclasses import dataclass, field +from typing import Optional +import sys +import time + +BASE_URL = "http://localhost:3110" +API_HOST = "localhost" +API_PORT = 3110 + +# Persistent connection +_conn = None + +def get_conn(): + global _conn + if _conn is None: + _conn = http.client.HTTPConnection(API_HOST, API_PORT, timeout=300) + return _conn + +def reset_conn(): + global _conn + if _conn: + try: + _conn.close() + except: + pass + _conn = None + +# Monthly prices for 2017-2018 (CoinGecko open prices) +MONTHLY_PRICES = { + (2017, 1): 970, (2017, 2): 968, (2017, 3): 1190, (2017, 4): 1080, + (2017, 5): 1362, (2017, 6): 2299, (2017, 7): 2455, (2017, 8): 2865, + (2017, 9): 4738, (2017, 10): 4334, (2017, 11): 6440, (2017, 12): 9968, + (2018, 1): 13888, (2018, 2): 10116, (2018, 3): 10307, (2018, 4): 6922, + (2018, 5): 9243, (2018, 6): 7487, (2018, 7): 6386, (2018, 8): 7726, + (2018, 9): 7016, (2018, 10): 6566, (2018, 11): 6305, (2018, 12): 3972, +} + +def fetch(path: str, retries: int = 5): + """Fetch JSON from API with retry logic and connection reuse.""" + for attempt in range(retries): + try: + conn = get_conn() + conn.request("GET", path) + resp = conn.getresponse() + data = resp.read().decode('utf-8') + return json.loads(data) + except Exception as e: + reset_conn() # Reset connection on error + if attempt < retries - 1: + wait_time = (attempt + 1) * 3 # 3, 6, 9, 12 seconds + print(f" Retry {attempt + 1}/{retries} after {wait_time}s: {type(e).__name__}") + time.sleep(wait_time) + else: + raise + +def fetch_chunked(path_template: str, start: int, end: int, chunk_size: int = 25000) -> list: + """Fetch data in chunks to avoid API limits.""" + result = [] + total_chunks = (end - start + chunk_size - 1) // chunk_size + for i, chunk_start in enumerate(range(start, end, chunk_size)): + chunk_end = min(chunk_start + chunk_size, end) + path = path_template.format(start=chunk_start, end=chunk_end) + if i % 20 == 0 and i > 0: + print(f" chunk {i}/{total_chunks}...") + data = fetch(path)["data"] + result.extend(data) + return result + +def get_phase_bin_and_decade(sats: int) -> tuple: + """Get (phase_bin, decade) for sats value. Returns (None, 0) if out of range.""" + if sats < 1000 or sats > 10_000_000_000_000: + return None, 0 + log_sats = math.log10(sats) + decade = int(math.floor(log_sats)) + phase = log_sats - decade + return min(int(phase * 100), 99), decade + +def get_phase_bin(sats: int) -> Optional[int]: + """Get phase bin for sats value (0-99), or None if out of range.""" + return get_phase_bin_and_decade(sats)[0] + +def get_decade(sats: int) -> int: + """Get the decade (power of 10) for sats value.""" + if sats <= 0: + return 0 + return int(math.floor(math.log10(sats))) + +def sats_to_implied_btc_price(sats: int, btc_price: float) -> float: + """ + What BTC price does this output imply? + If someone paid X sats for $Y worth of goods, and BTC = btc_price, + then the implied price = (sats / 1e8) * btc_price. + But we don't know Y. So instead, assume the output represents ~$btc_price worth, + and see what price that implies: implied_price = btc_price * (1e8 / sats). + """ + if sats <= 0: + return 0 + return btc_price * (100_000_000 / sats) + +def bin_to_price(bin_idx: int, anchor: float) -> float: + """Convert bin to price using anchor for decade selection.""" + EXPONENT = 5.0 + phase = (bin_idx + 0.5) / 100 + raw_price = 10 ** (EXPONENT - phase) + decade_ratio = round(math.log10(anchor / raw_price)) + return raw_price * (10 ** decade_ratio) + +def build_bin_classifier(btc_price: float) -> dict: + """ + Precompute classification for each bin (0-99) at given BTC price. + Returns dict mapping bin -> category. + """ + EXPONENT = 5.0 + result = {} + + for bin_idx in range(100): + phase = (bin_idx + 0.5) / 100 + raw_price = 10 ** (EXPONENT - phase) + + best_error = float('inf') + best_decade = 0 + + for decade in range(-4, 5): + price = raw_price * (10 ** decade) + error = abs(price - btc_price) / btc_price + if error < best_error: + best_error = error + best_decade = decade + + anchor_decade = round(math.log10(btc_price / raw_price)) + + if best_error <= 0.15: + result[bin_idx] = "accurate" + elif best_error <= 0.30: + result[bin_idx] = "close" + elif anchor_decade != best_decade: + result[bin_idx] = "wrong_decade" + else: + result[bin_idx] = "noise" + + return result + +def build_bin_classifier_range(low_price: float, high_price: float) -> dict: + """ + Precompute classification for each bin (0-99) given daily low-high range. + An output is "accurate" if its implied price falls within the daily range. + """ + EXPONENT = 5.0 + result = {} + mid_price = (low_price + high_price) / 2 + + for bin_idx in range(100): + phase = (bin_idx + 0.5) / 100 + raw_price = 10 ** (EXPONENT - phase) + + # Find the best decade match using mid price as anchor + best_error_vs_mid = float('inf') + best_decade = 0 + + for decade in range(-4, 5): + price = raw_price * (10 ** decade) + error = abs(price - mid_price) / mid_price + if error < best_error_vs_mid: + best_error_vs_mid = error + best_decade = decade + + # Get the implied price at best decade + implied_price = raw_price * (10 ** best_decade) + + # Check if implied price falls within the daily range (with tolerance) + # ±5% for accurate (range already captures intraday variation) + # ±15% for close + range_low = low_price * 0.95 + range_high = high_price * 1.05 + + anchor_decade = round(math.log10(mid_price / raw_price)) + + if range_low <= implied_price <= range_high: + result[bin_idx] = "accurate" + elif low_price * 0.85 <= implied_price <= high_price * 1.15: + result[bin_idx] = "close" + elif anchor_decade != best_decade: + result[bin_idx] = "wrong_decade" + else: + result[bin_idx] = "noise" + + return result + +def classify_accuracy_fast(bin_idx: Optional[int], classifier: dict) -> str: + """Fast classification using precomputed bin classifier.""" + if bin_idx is None: + return "noise" + return classifier.get(bin_idx, "noise") + +# Precompute round BTC values as a set with tolerance ranges +_ROUND_VALUES = [1000, 10000, 20000, 30000, 50000, 100000, 200000, 300000, 500000, + 1000000, 2000000, 3000000, 5000000, 10000000, 20000000, 30000000, + 50000000, 100000000, 1000000000] +# Build set of all "close enough" values (within 0.1%) +_ROUND_SET = set() +for r in _ROUND_VALUES: + tol = int(r * 0.001) + for v in range(r - tol, r + tol + 1): + _ROUND_SET.add(v) + +def is_round_btc(sats: int) -> bool: + """Check if sats is a round BTC amount (within 0.1%).""" + return sats in _ROUND_SET + +# Round USD values to check (in cents to avoid float issues) - sorted for binary search +_ROUND_USD_VALUES = [100, 500, 1000, 2000, 2500, 5000, 10000, 20000, 25000, 50000, + 100000, 200000, 250000, 500000, 1000000, 2000000, 2500000, + 5000000, 10000000] # $1 to $100,000 + +def is_round_usd(sats: int, btc_low: float, btc_high: float, tolerance: float = 0.05) -> bool: + """Check if implied USD value is close to a round amount at any price in range.""" + if sats <= 0 or btc_low <= 0 or btc_high <= 0: + return False + # Calculate implied USD range (low price = low USD, high price = high USD) + implied_usd_low = int(sats * btc_low / 1_000_000) # cents + implied_usd_high = int(sats * btc_high / 1_000_000) # cents + + # Check if any round USD value falls within (or near) the implied range + for round_val in _ROUND_USD_VALUES: + # The implied USD at some point during the day could have been round_val + # if round_val is within [implied_low * (1-tol), implied_high * (1+tol)] + range_low = implied_usd_low * (1 - tolerance) + range_high = implied_usd_high * (1 + tolerance) + if range_low <= round_val <= range_high: + return True + return False + +# Micro-round sats: specific round values with 0.01% tolerance (UTXOracle style) +# These are values like 50000, 100000, 200000, etc. that aren't caught by is_round_btc +_MICRO_ROUND_SATS = [] +# 50k-100k range (step 10k) +for v in range(50000, 100000, 10000): + _MICRO_ROUND_SATS.append(v) +# 100k-1M range (step 10k) +for v in range(100000, 1000000, 10000): + _MICRO_ROUND_SATS.append(v) +# 1M-10M range (step 100k) +for v in range(1000000, 10000000, 100000): + _MICRO_ROUND_SATS.append(v) +# 10M-100M range (step 1M) +for v in range(10000000, 100000000, 1000000): + _MICRO_ROUND_SATS.append(v) +_MICRO_ROUND_SATS = sorted(set(_MICRO_ROUND_SATS)) + +def is_micro_round_sats(sats: int, tolerance: float = 0.0001) -> bool: + """Check if sats is a micro-round amount (within 0.01% of specific values).""" + if sats <= 0: + return False + # Binary search for nearest + idx = bisect.bisect_left(_MICRO_ROUND_SATS, sats) + for i in [idx - 1, idx]: + if 0 <= i < len(_MICRO_ROUND_SATS): + round_val = _MICRO_ROUND_SATS[i] + if abs(sats - round_val) <= round_val * tolerance: + return True + return False + +# Phase bins where round USD amounts cluster (0-199 at 200 bins/decade) +# Calculated as: bin = int(frac(log10(usd_cents)) * 200) +# This is price-independent - works regardless of BTC price level! +ROUND_USD_PHASE_BINS_200 = [ + 0, # $1, $10, $100, $1000 (log10 = 0, 1, 2, 3) + 35, # $1.50, $15, $150 (log10 = 0.176) + 60, # $2, $20, $200 (log10 = 0.301) + 80, # $2.50, $25, $250 (log10 = 0.398) + 95, # $3, $30, $300 (log10 = 0.477) + 120, # $4, $40, $400 (log10 = 0.602) + 140, # $5, $50, $500 (log10 = 0.699) + 156, # $6, $60, $600 (log10 = 0.778) + 169, # $7, $70, $700 (log10 = 0.845) + 181, # $8, $80, $800 (log10 = 0.903) + 191, # $9, $90, $900 (log10 = 0.954) +] + +def is_round_usd_phase(sats: int, tolerance_bins: int) -> bool: + """ + Check if sats falls into a round-USD phase bin. NO PRICE NEEDED! + + Uses 200 bins/decade resolution. + Tolerance in bins: 2 bins = 1%, 4 bins = 2%, 10 bins = 5%, 20 bins = 10% + """ + if sats < 1000: # Skip very small values + return False + + phase = math.log10(sats) % 1.0 # fractional part (0.0 to 1.0) + bin_idx = int(phase * 200) # convert to bin (0 to 199) + + for round_bin in ROUND_USD_PHASE_BINS_200: + diff = abs(bin_idx - round_bin) + # Handle wraparound (bin 199 is close to bin 0) + if diff <= tolerance_bins or (200 - diff) <= tolerance_bins: + return True + return False + +def get_tx_pattern(input_count: int, output_count: int) -> str: + """Categorize transaction by input/output pattern.""" + if input_count == 1: + if output_count == 1: + return "1-to-1" + elif output_count == 2: + return "1-to-2" + else: + return "1-to-many" + elif input_count == 2: + if output_count == 1: + return "2-to-1" + elif output_count == 2: + return "2-to-2" + else: + return "2-to-many" + else: # many inputs (3+) + if output_count == 1: + return "many-to-1" + elif output_count == 2: + return "many-to-2" + else: + return "many-to-many" + +@dataclass +class Stats: + """Aggregated statistics.""" + total: int = 0 + by_output_count: dict = field(default_factory=lambda: defaultdict(int)) + by_input_count: dict = field(default_factory=lambda: defaultdict(int)) + by_output_type: dict = field(default_factory=lambda: defaultdict(int)) + by_is_round: dict = field(default_factory=lambda: defaultdict(int)) + by_same_day: dict = field(default_factory=lambda: defaultdict(int)) + by_has_opreturn: dict = field(default_factory=lambda: defaultdict(int)) + by_witness_size: dict = field(default_factory=lambda: defaultdict(int)) + by_value_range: dict = field(default_factory=lambda: defaultdict(int)) + by_both_round: dict = field(default_factory=lambda: defaultdict(int)) + by_bin: dict = field(default_factory=lambda: defaultdict(int)) + by_decade: dict = field(default_factory=lambda: defaultdict(int)) + by_implied_usd_range: dict = field(default_factory=lambda: defaultdict(int)) + # New: output position analysis (for 2-output txs only) + by_output_index: dict = field(default_factory=lambda: defaultdict(int)) + by_is_smaller_output: dict = field(default_factory=lambda: defaultdict(int)) + by_round_pattern: dict = field(default_factory=lambda: defaultdict(int)) # "only_this", "only_other", "both", "neither" + by_value_ratio: dict = field(default_factory=lambda: defaultdict(int)) # ratio of this output to total (for 2-out) + by_error_pct: dict = field(default_factory=lambda: defaultdict(int)) # how close to actual price + by_tx_total_value: dict = field(default_factory=lambda: defaultdict(int)) # total output value of tx + by_round_usd_10pct: dict = field(default_factory=lambda: defaultdict(int)) # 10% tolerance (price-based) + by_round_usd_5pct: dict = field(default_factory=lambda: defaultdict(int)) # 5% tolerance (price-based) + by_round_usd_2pct: dict = field(default_factory=lambda: defaultdict(int)) # 2% tolerance (price-based) + by_round_usd_1pct: dict = field(default_factory=lambda: defaultdict(int)) # 1% tolerance (price-based) + # Phase-based round USD (NO PRICE NEEDED) at different tolerances + by_phase_usd_1pct: dict = field(default_factory=lambda: defaultdict(int)) # 1% = ±2 bins + by_phase_usd_2pct: dict = field(default_factory=lambda: defaultdict(int)) # 2% = ±4 bins + by_phase_usd_5pct: dict = field(default_factory=lambda: defaultdict(int)) # 5% = ±10 bins + by_phase_usd_10pct: dict = field(default_factory=lambda: defaultdict(int)) # 10% = ±20 bins + by_tx_pattern: dict = field(default_factory=lambda: defaultdict(int)) # input->output pattern (1-to-2, many-to-1, etc) + by_value_similarity: dict = field(default_factory=lambda: defaultdict(int)) # how similar are 2-out values (for detecting splits) + by_is_micro_round: dict = field(default_factory=lambda: defaultdict(int)) # very specific round sat amounts (UTXOracle style) + + def record(self, output_count, input_count, output_type, is_round, + same_day, has_opreturn, witness_size, sats, both_round, bin_idx, + btc_price, decade, output_index=None, is_smaller=None, round_pattern=None, value_ratio=None, error_pct=None, tx_total_sats=None, + round_usd_10pct=None, round_usd_5pct=None, round_usd_2pct=None, round_usd_1pct=None, tx_pattern=None, value_similarity=None, is_micro_round=None, + phase_usd_1pct=None, phase_usd_2pct=None, phase_usd_5pct=None, phase_usd_10pct=None): + self.total += 1 + self.by_output_count[min(output_count, 5)] += 1 + self.by_input_count[min(input_count, 5)] += 1 + self.by_output_type[output_type] += 1 + self.by_is_round[is_round] += 1 + self.by_same_day[same_day] += 1 + self.by_has_opreturn[has_opreturn] += 1 + self.by_both_round[both_round] += 1 + if bin_idx is not None: + self.by_bin[bin_idx] += 1 + + # Track decade (power of 10 of sats) + self.by_decade[decade] += 1 + + # Track output position (for 2-output txs) + if output_index is not None: + self.by_output_index[output_index] += 1 + if is_smaller is not None: + self.by_is_smaller_output[is_smaller] += 1 + if round_pattern is not None: + self.by_round_pattern[round_pattern] += 1 + if value_ratio is not None: + self.by_value_ratio[value_ratio] += 1 + if error_pct is not None: + if error_pct < 5: + self.by_error_pct["<5%"] += 1 + elif error_pct < 10: + self.by_error_pct["5-10%"] += 1 + elif error_pct < 15: + self.by_error_pct["10-15%"] += 1 + elif error_pct < 25: + self.by_error_pct["15-25%"] += 1 + elif error_pct < 50: + self.by_error_pct["25-50%"] += 1 + else: + self.by_error_pct["50%+"] += 1 + if tx_total_sats is not None and tx_total_sats > 0: + # Bucket by tx total value (in BTC terms) + if tx_total_sats < 1_000_000: # < 0.01 BTC + self.by_tx_total_value["<0.01 BTC"] += 1 + elif tx_total_sats < 10_000_000: # < 0.1 BTC + self.by_tx_total_value["0.01-0.1 BTC"] += 1 + elif tx_total_sats < 100_000_000: # < 1 BTC + self.by_tx_total_value["0.1-1 BTC"] += 1 + elif tx_total_sats < 1_000_000_000: # < 10 BTC + self.by_tx_total_value["1-10 BTC"] += 1 + else: + self.by_tx_total_value["10+ BTC"] += 1 + + # Track round USD at different tolerances + if round_usd_10pct is not None: + self.by_round_usd_10pct[round_usd_10pct] += 1 + if round_usd_5pct is not None: + self.by_round_usd_5pct[round_usd_5pct] += 1 + if round_usd_2pct is not None: + self.by_round_usd_2pct[round_usd_2pct] += 1 + if round_usd_1pct is not None: + self.by_round_usd_1pct[round_usd_1pct] += 1 + + # Track transaction pattern (input-to-output pattern) + if tx_pattern is not None: + self.by_tx_pattern[tx_pattern] += 1 + + # Track value similarity (for 2-output txs) + if value_similarity is not None: + self.by_value_similarity[value_similarity] += 1 + + # Track micro-round sats + if is_micro_round is not None: + self.by_is_micro_round[is_micro_round] += 1 + + # Track phase-based round USD (no price needed) + if phase_usd_1pct is not None: + self.by_phase_usd_1pct[phase_usd_1pct] += 1 + if phase_usd_2pct is not None: + self.by_phase_usd_2pct[phase_usd_2pct] += 1 + if phase_usd_5pct is not None: + self.by_phase_usd_5pct[phase_usd_5pct] += 1 + if phase_usd_10pct is not None: + self.by_phase_usd_10pct[phase_usd_10pct] += 1 + + # Track implied USD value (sats * btc_price / 1e8) + implied_usd = sats * btc_price / 100_000_000 + if implied_usd < 1: + self.by_implied_usd_range["<$1"] += 1 + elif implied_usd < 10: + self.by_implied_usd_range["$1-$10"] += 1 + elif implied_usd < 100: + self.by_implied_usd_range["$10-$100"] += 1 + elif implied_usd < 1000: + self.by_implied_usd_range["$100-$1k"] += 1 + elif implied_usd < 10000: + self.by_implied_usd_range["$1k-$10k"] += 1 + else: + self.by_implied_usd_range["$10k+"] += 1 + + # Witness size buckets + if witness_size == 0: + self.by_witness_size["0"] += 1 + elif witness_size < 500: + self.by_witness_size["1-499"] += 1 + elif witness_size < 1000: + self.by_witness_size["500-999"] += 1 + elif witness_size < 2500: + self.by_witness_size["1000-2499"] += 1 + else: + self.by_witness_size["2500+"] += 1 + + # Value ranges (sats) + if sats < 10000: + self.by_value_range["<10k"] += 1 + elif sats < 100000: + self.by_value_range["10k-100k"] += 1 + elif sats < 1000000: + self.by_value_range["100k-1M"] += 1 + elif sats < 10000000: + self.by_value_range["1M-10M"] += 1 + elif sats < 100000000: + self.by_value_range["10M-100M"] += 1 + else: + self.by_value_range["100M+"] += 1 + +def print_stats(stats: Stats, label: str, log=print): + """Print statistics with percentages.""" + log(f"\n{'='*50}") + log(f"{label} (n={stats.total:,})") + log('='*50) + + if stats.total == 0: + log("No data") + return + + def pct(d): + return {k: f"{v:,} ({100*v/stats.total:.1f}%)" for k, v in sorted(d.items())} + + log(f"\nOutput count: {pct(stats.by_output_count)}") + log(f"Input count: {pct(stats.by_input_count)}") + log(f"Output type: {pct(stats.by_output_type)}") + log(f"Is round BTC: {pct(stats.by_is_round)}") + log(f"Both outputs round: {pct(stats.by_both_round)}") + log(f"Same-day spend: {pct(stats.by_same_day)}") + log(f"Has OP_RETURN: {pct(stats.by_has_opreturn)}") + log(f"Witness size: {pct(stats.by_witness_size)}") + log(f"Value range (sats): {pct(stats.by_value_range)}") + log(f"Decade (10^N sats): {pct(stats.by_decade)}") + log(f"Implied USD value: {pct(stats.by_implied_usd_range)}") + if stats.by_output_index: + log(f"Output index (2-out only): {pct(stats.by_output_index)}") + if stats.by_is_smaller_output: + log(f"Is smaller output (2-out): {pct(stats.by_is_smaller_output)}") + if stats.by_round_pattern: + log(f"Round pattern (2-out): {pct(stats.by_round_pattern)}") + if stats.by_value_ratio: + log(f"Value ratio (2-out): {pct(stats.by_value_ratio)}") + if stats.by_error_pct: + log(f"Error from actual price: {pct(stats.by_error_pct)}") + if stats.by_tx_total_value: + log(f"Tx total value: {pct(stats.by_tx_total_value)}") + if stats.by_round_usd_10pct: + log(f"Round USD (10% tol): {pct(stats.by_round_usd_10pct)}") + if stats.by_round_usd_5pct: + log(f"Round USD (5% tol): {pct(stats.by_round_usd_5pct)}") + if stats.by_round_usd_2pct: + log(f"Round USD (2% tol): {pct(stats.by_round_usd_2pct)}") + if stats.by_round_usd_1pct: + log(f"Round USD (1% tol): {pct(stats.by_round_usd_1pct)}") + if stats.by_tx_pattern: + log(f"Tx pattern: {pct(stats.by_tx_pattern)}") + if stats.by_value_similarity: + log(f"Value similarity (2-out): {pct(stats.by_value_similarity)}") + if stats.by_is_micro_round: + log(f"Micro-round sats: {pct(stats.by_is_micro_round)}") + if stats.by_phase_usd_1pct: + log(f"Phase USD (1% tol): {pct(stats.by_phase_usd_1pct)}") + if stats.by_phase_usd_2pct: + log(f"Phase USD (2% tol): {pct(stats.by_phase_usd_2pct)}") + if stats.by_phase_usd_5pct: + log(f"Phase USD (5% tol): {pct(stats.by_phase_usd_5pct)}") + if stats.by_phase_usd_10pct: + log(f"Phase USD (10% tol): {pct(stats.by_phase_usd_10pct)}") + + # Top bins + log(f"\nTop 10 bins:") + for bin_idx, count in sorted(stats.by_bin.items(), key=lambda x: -x[1])[:10]: + log(f" Bin {bin_idx}: {count:,} ({100*count/stats.total:.1f}%)") + +def analyze_block_range(start_height: int, end_height: int, start_dateindex: int, end_dateindex: int): + """Analyze all outputs in a block range using daily OHLC prices.""" + print(f"\nFetching data for heights {start_height}-{end_height}...") + + # Fetch daily OHLC prices for this date range (external price data) + print("Fetching daily OHLC prices...") + ohlc_data = fetch(f"/api/metric/price_ohlc/dateindex?start={start_dateindex}&end={end_dateindex}")["data"] + # OHLC format: [open, high, low, close] in dollars + # Store low, high, and mid price for each day (transactions happen throughout the day) + daily_prices = {} # dateindex -> (low, high, mid) + for i, ohlc in enumerate(ohlc_data): + if ohlc and len(ohlc) >= 4: + open_p, high, low, close = ohlc[0], ohlc[1], ohlc[2], ohlc[3] + mid = (open_p + close) / 2 # average of open/close, not low/high (avoid wick skew) + daily_prices[start_dateindex + i] = (low, high, mid) + all_lows = [p[0] for p in daily_prices.values()] + all_highs = [p[1] for p in daily_prices.values()] + print(f" Got prices for {len(daily_prices)} days (${min(all_lows):.0f} - ${max(all_highs):.0f})") + + # Precompute bin classifiers for each unique price (cache for speed) + bin_classifier_cache = {} + def get_bin_classifier(low: float, high: float) -> dict: + """Build classifier that checks if bin falls within daily low-high range.""" + # Cache key is the rounded range + cache_key = (round(low / 10) * 10, round(high / 10) * 10) + if cache_key not in bin_classifier_cache: + bin_classifier_cache[cache_key] = build_bin_classifier_range(low, high) + return bin_classifier_cache[cache_key] + + # Get transaction ranges + first_tx = fetch(f"/api/metric/first_txindex/height?start={start_height}&end={end_height+1}") + first_txs = first_tx["data"] + tx_start = first_txs[0] + tx_end = first_txs[-1] if len(first_txs) > 1 else tx_start + 10000 + + print(f"Transaction range: {tx_start}-{tx_end} ({tx_end-tx_start:,} txs)") + + # Get transaction metadata (chunked for large ranges) + print("Fetching transaction data...") + tx_first_out = fetch_chunked("/api/metric/first_txoutindex/txindex?start={start}&end={end}", tx_start, tx_end) + tx_first_in = fetch_chunked("/api/metric/first_txinindex/txindex?start={start}&end={end}", tx_start, tx_end) + tx_base_size = fetch_chunked("/api/metric/base_size/txindex?start={start}&end={end}", tx_start, tx_end) + tx_total_size = fetch_chunked("/api/metric/total_size/txindex?start={start}&end={end}", tx_start, tx_end) + tx_output_count = fetch_chunked("/api/metric/output_count/txindex?start={start}&end={end}", tx_start, tx_end) + tx_input_count = fetch_chunked("/api/metric/input_count/txindex?start={start}&end={end}", tx_start, tx_end) + tx_height = fetch_chunked("/api/metric/height/txindex?start={start}&end={end}", tx_start, tx_end) + + # Get output data + out_start = tx_first_out[0] if tx_first_out else 0 + # Estimate out_end based on last tx's output count + last_tx_outputs = tx_output_count[-1] if tx_output_count else 10 + out_end = tx_first_out[-1] + last_tx_outputs + 1 if tx_first_out else out_start + 10000 + + print(f"Output range: {out_start}-{out_end} ({out_end-out_start:,} outputs)") + print("Fetching output data...") + out_value = fetch_chunked("/api/metric/value/txoutindex?start={start}&end={end}", out_start, out_end) + out_type = fetch_chunked("/api/metric/outputtype/txoutindex?start={start}&end={end}", out_start, out_end) + + # Get input data for same-day check + in_start = tx_first_in[0] if tx_first_in else 0 + last_tx_inputs = tx_input_count[-1] if tx_input_count else 10 + in_end = tx_first_in[-1] + last_tx_inputs + 1 if tx_first_in else in_start + 10000 + + print(f"Input range: {in_start}-{in_end} ({in_end-in_start:,} inputs)") + print("Fetching input data...") + # Get spent txoutindex for each input + in_spent_txoutindex = fetch_chunked("/api/metric/txoutindex/txinindex?start={start}&end={end}", in_start, in_end) + + # For same-day spend detection, only check outputs created within our block range + # (outputs from before can't be same-day by definition) + # We'll use the output ranges we already have (out_start to out_end) + + # Get height to dateindex for same-day check + height_dateindex = fetch_chunked("/api/metric/dateindex/height?start={start}&end={end}", start_height, end_height+1) + + # Analyze each transaction + # Categories: accurate (<=15%), close (15-30%), wrong_decade, noise + stats_accurate = Stats() + stats_close = Stats() + stats_wrong_decade = Stats() + stats_noise = Stats() + + num_txs = len(tx_first_out) - 1 + print(f"Analyzing {num_txs:,} transactions...") + for i in range(num_txs): + if i % 100000 == 0 and i > 0: + print(f" Progress: {i:,}/{num_txs:,} ({100*i/num_txs:.0f}%)") + txindex = tx_start + i + + out_count = tx_output_count[i] if i < len(tx_output_count) else 0 + in_count = tx_input_count[i] if i < len(tx_input_count) else 0 + base_size = tx_base_size[i] if i < len(tx_base_size) else 0 + total_size = tx_total_size[i] if i < len(tx_total_size) else 0 + witness_size = (total_size or 0) - (base_size or 0) + + first_out = tx_first_out[i] - out_start + next_first_out = tx_first_out[i + 1] - out_start if i + 1 < len(tx_first_out) else first_out + out_count + + first_in = tx_first_in[i] - in_start if i < len(tx_first_in) else 0 + + # Check for OP_RETURN + has_opreturn = False + output_types = [] + output_values = [] + for j in range(first_out, min(next_first_out, len(out_type))): + ot = out_type[j] if j < len(out_type) else "unknown" + output_types.append(ot) + if ot and ot.lower() == "opreturn": + has_opreturn = True + if j < len(out_value): + output_values.append(out_value[j]) + + # Get the daily price range for this transaction + tx_height_val = tx_height[i] if i < len(tx_height) else None + tx_dateindex_val = None + btc_price = None # mid price for round USD calculations + btc_low = None + btc_high = None + bin_classifier = None + if tx_height_val is not None: + tx_di_idx = tx_height_val - start_height + tx_dateindex_val = height_dateindex[tx_di_idx] if 0 <= tx_di_idx < len(height_dateindex) else None + if tx_dateindex_val is not None and tx_dateindex_val in daily_prices: + btc_low, btc_high, btc_price = daily_prices[tx_dateindex_val] + bin_classifier = get_bin_classifier(btc_low, btc_high) + + # Skip if no price data for this day + if btc_price is None or bin_classifier is None: + continue + + # Check same-day spend (was the spent output created within our analysis range?) + # Only outputs created within our range (out_start to out_end) can be same-day + same_day = False + if tx_dateindex_val is not None and in_count and in_count > 0: + for k in range(in_count): + in_idx = first_in + k + if 0 <= in_idx < len(in_spent_txoutindex): + spent_txoutindex = in_spent_txoutindex[in_idx] + if spent_txoutindex and spent_txoutindex < 2**62: + # Check if spent output is within our current range + if out_start <= spent_txoutindex < out_end: + # Binary search for the tx that contains this output + ti = bisect.bisect_right(tx_first_out, spent_txoutindex) - 1 + if 0 <= ti < len(tx_height): + spent_tx_height = tx_height[ti] + if spent_tx_height: + spent_di_idx = spent_tx_height - start_height + if 0 <= spent_di_idx < len(height_dateindex): + if height_dateindex[spent_di_idx] == tx_dateindex_val: + same_day = True + break + + # Compute transaction total output value + tx_total_sats = sum(v for v in output_values if v and v > 0) + + # Check if both outputs are round (for 2-output txs) + both_round = False + if out_count == 2 and len(output_values) >= 2: + both_round = is_round_btc(output_values[0]) and is_round_btc(output_values[1]) + + # Precompute round status for each output (for 2-output analysis) + output_rounds = [is_round_btc(v) if v else False for v in output_values] + + # Precompute value similarity for 2-output txs (same for both outputs) + value_similarity = None + if out_count == 2 and len(output_values) == 2: + v1, v2 = output_values[0] or 0, output_values[1] or 0 + if v1 > 0 and v2 > 0: + similarity = min(v1, v2) / max(v1, v2) + if similarity > 0.95: + value_similarity = "nearly_equal" + elif similarity > 0.8: + value_similarity = "similar" + elif similarity > 0.5: + value_similarity = "moderate" + elif similarity > 0.2: + value_similarity = "different" + else: + value_similarity = "very_different" + + # Analyze each output + for j, sats in enumerate(output_values): + if sats is None or sats < 1000: + continue + + ot = output_types[j] if j < len(output_types) else "unknown" + is_round = output_rounds[j] # Use precomputed value + bin_idx, decade = get_phase_bin_and_decade(sats) + + # Compute error: what price does this output imply vs actual daily range? + # Error is 0 if implied price falls within [low, high], otherwise distance to nearest edge + error_pct = None + if bin_idx is not None: + implied_price = bin_to_price(bin_idx, btc_price) + if implied_price < btc_low: + error_pct = 100 * (btc_low - implied_price) / btc_low + elif implied_price > btc_high: + error_pct = 100 * (implied_price - btc_high) / btc_high + else: + error_pct = 0 # within daily range + + # Use precomputed bin classifier for speed + category = classify_accuracy_fast(bin_idx, bin_classifier) + + if category == "accurate": + stats = stats_accurate + elif category == "close": + stats = stats_close + elif category == "wrong_decade": + stats = stats_wrong_decade + else: + stats = stats_noise + + # Compute 2-output specific metrics + output_index = None + is_smaller = None + round_pattern = None + value_ratio = None + if out_count == 2 and len(output_values) == 2: + output_index = j # 0 or 1 + other_idx = 1 - j + other_sats = output_values[other_idx] or 0 + is_smaller = sats < other_sats + # Round pattern: which outputs are round? + this_round = output_rounds[j] + other_round = output_rounds[other_idx] + if this_round and other_round: + round_pattern = "both_round" + elif this_round and not other_round: + round_pattern = "only_this_round" + elif not this_round and other_round: + round_pattern = "only_other_round" + else: + round_pattern = "neither_round" + # Value ratio: what fraction of total is this output? + total_sats = sats + other_sats + if total_sats > 0: + ratio = sats / total_sats + if ratio < 0.1: + value_ratio = "<10%" + elif ratio < 0.3: + value_ratio = "10-30%" + elif ratio < 0.5: + value_ratio = "30-50%" + elif ratio < 0.7: + value_ratio = "50-70%" + elif ratio < 0.9: + value_ratio = "70-90%" + else: + value_ratio = ">90%" + + # Compute round USD at different tolerances (price-based, using daily range) + round_usd_10pct = is_round_usd(sats, btc_low, btc_high, tolerance=0.10) + round_usd_5pct = is_round_usd(sats, btc_low, btc_high, tolerance=0.05) + round_usd_2pct = is_round_usd(sats, btc_low, btc_high, tolerance=0.02) + round_usd_1pct = is_round_usd(sats, btc_low, btc_high, tolerance=0.01) + + # Compute phase-based round USD (NO PRICE NEEDED!) + # 200 bins/decade: 1%=±2bins, 2%=±4bins, 5%=±10bins, 10%=±20bins + phase_usd_1pct = is_round_usd_phase(sats, tolerance_bins=2) + phase_usd_2pct = is_round_usd_phase(sats, tolerance_bins=4) + phase_usd_5pct = is_round_usd_phase(sats, tolerance_bins=10) + phase_usd_10pct = is_round_usd_phase(sats, tolerance_bins=20) + + tx_pattern = get_tx_pattern(in_count, out_count) + micro_round = is_micro_round_sats(sats) + + stats.record( + output_count=out_count, + input_count=in_count, + output_type=ot, + is_round=is_round, + same_day=same_day, + has_opreturn=has_opreturn, + witness_size=witness_size, + sats=sats, + both_round=both_round, + bin_idx=bin_idx, + btc_price=btc_price, + decade=decade, + output_index=output_index, + is_smaller=is_smaller, + round_pattern=round_pattern, + value_ratio=value_ratio, + error_pct=error_pct, + tx_total_sats=tx_total_sats, + round_usd_10pct=round_usd_10pct, + round_usd_5pct=round_usd_5pct, + round_usd_2pct=round_usd_2pct, + round_usd_1pct=round_usd_1pct, + tx_pattern=tx_pattern, + value_similarity=value_similarity, + is_micro_round=micro_round, + phase_usd_1pct=phase_usd_1pct, + phase_usd_2pct=phase_usd_2pct, + phase_usd_5pct=phase_usd_5pct, + phase_usd_10pct=phase_usd_10pct + ) + + return stats_accurate, stats_close, stats_wrong_decade, stats_noise + +def main(): + # Open report file + report_file = open("research/price_signal_analysis_report.txt", "w") + + def log(msg=""): + print(msg) + report_file.write(msg + "\n") + report_file.flush() + + log("=" * 60) + log("PHASE ORACLE SIGNAL ANALYSIS") + log("=" * 60) + + # Cache dates lookup (same for all months) + log("Fetching date index...") + dates = fetch("/api/metric/date/dateindex?start=0&end=4000")["data"] + + # Analyze all months + for year in [2017, 2018]: + for month in range(1, 13): # All months + key = (year, month) + if key not in MONTHLY_PRICES: + continue + + log(f"\n\n{'#'*60}") + log(f"# {year}-{month:02d}") + log('#'*60) + + # Get heights and dateindexes for this month + try: + start_di = None + end_di = None + for i, d in enumerate(dates): + if d and d.startswith(f"{year}-{month:02d}"): + if start_di is None: + start_di = i + end_di = i + 1 # Keep updating to find last day of month + + if start_di is None: + log(f"Could not find date index for {year}-{month:02d}") + continue + + # Get all heights for the month + heights = fetch(f"/api/metric/first_height/dateindex?start={start_di}&end={end_di+1}")["data"] + start_height = heights[0] + end_height = heights[-1] if len(heights) > 1 else start_height + 1000 + + log(f"Date range: {dates[start_di]} to {dates[end_di-1]} (dateindex {start_di}-{end_di})") + + # Analyze entire month with daily prices + accurate, close, wrong_decade, noise = analyze_block_range(start_height, end_height, start_di, end_di) + + total_outputs = accurate.total + close.total + wrong_decade.total + noise.total + log(f"\n--- SUMMARY ---") + log(f"Total outputs analyzed: {total_outputs:,}") + log(f" Accurate (≤15% error): {accurate.total:,} ({100*accurate.total/total_outputs:.1f}%)") + log(f" Close (15-30% error): {close.total:,} ({100*close.total/total_outputs:.1f}%)") + log(f" Wrong decade: {wrong_decade.total:,} ({100*wrong_decade.total/total_outputs:.1f}%)") + log(f" Noise: {noise.total:,} ({100*noise.total/total_outputs:.1f}%)") + + print_stats(accurate, "ACCURATE (within 15% of actual price)", log) + print_stats(noise, "NOISE (no decade matches)", log) + + # Print ratio comparison + if accurate.total > 0 and noise.total > 0: + # Sanity check: show True/False split for key boolean fields + log(f"\n--- ROUND USD BREAKDOWN ---") + for name, acc_d, noise_d in [ + ("Round USD 10%", accurate.by_round_usd_10pct, noise.by_round_usd_10pct), + ("Round USD 5%", accurate.by_round_usd_5pct, noise.by_round_usd_5pct), + ("Round USD 2%", accurate.by_round_usd_2pct, noise.by_round_usd_2pct), + ("Round USD 1%", accurate.by_round_usd_1pct, noise.by_round_usd_1pct), + ]: + acc_true_pct = 100 * acc_d.get(True, 0) / accurate.total + acc_false_pct = 100 * acc_d.get(False, 0) / accurate.total + noise_true_pct = 100 * noise_d.get(True, 0) / noise.total + noise_false_pct = 100 * noise_d.get(False, 0) / noise.total + log(f"{name}: accurate True={acc_true_pct:.1f}% False={acc_false_pct:.1f}% | noise True={noise_true_pct:.1f}% False={noise_false_pct:.1f}%") + + log(f"\n{'='*50}") + log("KEY DIFFERENCES (accurate vs noise):") + log('='*50) + + def compare(name, acc_dict, noise_dict): + log(f"\n{name}:") + all_keys = set(acc_dict.keys()) | set(noise_dict.keys()) + for k in sorted(all_keys): + acc_pct = 100 * acc_dict.get(k, 0) / accurate.total + noise_pct = 100 * noise_dict.get(k, 0) / noise.total + diff = acc_pct - noise_pct + if abs(diff) > 2: # Only show significant differences + log(f" {k}: {acc_pct:.1f}% vs {noise_pct:.1f}% (diff: {diff:+.1f}%)") + + compare("Output count", accurate.by_output_count, noise.by_output_count) + compare("Input count", accurate.by_input_count, noise.by_input_count) + compare("Output type", accurate.by_output_type, noise.by_output_type) + compare("Is round BTC", accurate.by_is_round, noise.by_is_round) + compare("Both round", accurate.by_both_round, noise.by_both_round) + compare("Same-day spend", accurate.by_same_day, noise.by_same_day) + compare("OP_RETURN", accurate.by_has_opreturn, noise.by_has_opreturn) + compare("Witness size", accurate.by_witness_size, noise.by_witness_size) + compare("Value range (sats)", accurate.by_value_range, noise.by_value_range) + compare("Decade (10^N sats)", accurate.by_decade, noise.by_decade) + compare("Implied USD", accurate.by_implied_usd_range, noise.by_implied_usd_range) + compare("Output index (2-out)", accurate.by_output_index, noise.by_output_index) + compare("Is smaller (2-out)", accurate.by_is_smaller_output, noise.by_is_smaller_output) + compare("Round pattern (2-out)", accurate.by_round_pattern, noise.by_round_pattern) + compare("Value ratio (2-out)", accurate.by_value_ratio, noise.by_value_ratio) + compare("Error from price", accurate.by_error_pct, noise.by_error_pct) + compare("Tx total value", accurate.by_tx_total_value, noise.by_tx_total_value) + compare("Round USD (10%)", accurate.by_round_usd_10pct, noise.by_round_usd_10pct) + compare("Round USD (5%)", accurate.by_round_usd_5pct, noise.by_round_usd_5pct) + compare("Round USD (2%)", accurate.by_round_usd_2pct, noise.by_round_usd_2pct) + compare("Round USD (1%)", accurate.by_round_usd_1pct, noise.by_round_usd_1pct) + compare("Phase USD (1%)", accurate.by_phase_usd_1pct, noise.by_phase_usd_1pct) + compare("Phase USD (2%)", accurate.by_phase_usd_2pct, noise.by_phase_usd_2pct) + compare("Phase USD (5%)", accurate.by_phase_usd_5pct, noise.by_phase_usd_5pct) + compare("Phase USD (10%)", accurate.by_phase_usd_10pct, noise.by_phase_usd_10pct) + compare("Tx pattern", accurate.by_tx_pattern, noise.by_tx_pattern) + compare("Value similarity (2-out)", accurate.by_value_similarity, noise.by_value_similarity) + compare("Micro-round sats", accurate.by_is_micro_round, noise.by_is_micro_round) + + # EXCLUSION RECOMMENDATIONS + log(f"\n{'='*50}") + log("EXCLUSION CANDIDATES (overrepresented in noise):") + log('='*50) + log("Characteristics where noise% > accurate% suggest exclusion filters:\n") + + def find_exclusions(name, acc_dict, noise_dict, threshold=3.0): + """Find characteristics overrepresented in noise (candidates for exclusion).""" + exclusions = [] + for k in set(acc_dict.keys()) | set(noise_dict.keys()): + acc_pct = 100 * acc_dict.get(k, 0) / accurate.total + noise_pct = 100 * noise_dict.get(k, 0) / noise.total + diff = noise_pct - acc_pct # positive = more in noise + if diff > threshold and noise_pct > 1: # at least 1% of noise + exclusions.append((k, acc_pct, noise_pct, diff)) + return sorted(exclusions, key=lambda x: -x[3]) # sort by diff descending + + all_exclusions = [] + for name, acc_d, noise_d in [ + ("Value range", accurate.by_value_range, noise.by_value_range), + ("Implied USD", accurate.by_implied_usd_range, noise.by_implied_usd_range), + ("Decade", accurate.by_decade, noise.by_decade), + ("Output count", accurate.by_output_count, noise.by_output_count), + ("Is round BTC", accurate.by_is_round, noise.by_is_round), + ("Both round", accurate.by_both_round, noise.by_both_round), + ("Tx pattern", accurate.by_tx_pattern, noise.by_tx_pattern), + ("Value similarity", accurate.by_value_similarity, noise.by_value_similarity), + ("Value ratio", accurate.by_value_ratio, noise.by_value_ratio), + ("Round USD 10%", accurate.by_round_usd_10pct, noise.by_round_usd_10pct), + ("Round USD 5%", accurate.by_round_usd_5pct, noise.by_round_usd_5pct), + ("Round USD 2%", accurate.by_round_usd_2pct, noise.by_round_usd_2pct), + ("Round USD 1%", accurate.by_round_usd_1pct, noise.by_round_usd_1pct), + ("Phase USD 10%", accurate.by_phase_usd_10pct, noise.by_phase_usd_10pct), + ("Phase USD 5%", accurate.by_phase_usd_5pct, noise.by_phase_usd_5pct), + ("Phase USD 2%", accurate.by_phase_usd_2pct, noise.by_phase_usd_2pct), + ("Phase USD 1%", accurate.by_phase_usd_1pct, noise.by_phase_usd_1pct), + ("Tx total value", accurate.by_tx_total_value, noise.by_tx_total_value), + ("Micro-round sats", accurate.by_is_micro_round, noise.by_is_micro_round), + ]: + excl = find_exclusions(name, acc_d, noise_d) + for k, acc_pct, noise_pct, diff in excl: + all_exclusions.append((name, k, acc_pct, noise_pct, diff)) + + # Sort by impact (diff) and print + all_exclusions.sort(key=lambda x: -x[4]) + for name, k, acc_pct, noise_pct, diff in all_exclusions[:15]: + log(f" EXCLUDE {name}={k}: noise {noise_pct:.1f}% vs accurate {acc_pct:.1f}% (+{diff:.1f}%)") + + # Also show INCLUSION candidates (overrepresented in accurate) + log(f"\n{'='*50}") + log("INCLUSION SIGNALS (overrepresented in accurate):") + log('='*50) + log("Characteristics where accurate% > noise% are good signals:\n") + + all_inclusions = [] + for name, acc_d, noise_d in [ + ("Value range", accurate.by_value_range, noise.by_value_range), + ("Implied USD", accurate.by_implied_usd_range, noise.by_implied_usd_range), + ("Decade", accurate.by_decade, noise.by_decade), + ("Output count", accurate.by_output_count, noise.by_output_count), + ("Is round BTC", accurate.by_is_round, noise.by_is_round), + ("Is smaller (2-out)", accurate.by_is_smaller_output, noise.by_is_smaller_output), + ("Tx pattern", accurate.by_tx_pattern, noise.by_tx_pattern), + ("Value similarity", accurate.by_value_similarity, noise.by_value_similarity), + ("Value ratio", accurate.by_value_ratio, noise.by_value_ratio), + ("Round USD 10%", accurate.by_round_usd_10pct, noise.by_round_usd_10pct), + ("Round USD 5%", accurate.by_round_usd_5pct, noise.by_round_usd_5pct), + ("Round USD 2%", accurate.by_round_usd_2pct, noise.by_round_usd_2pct), + ("Round USD 1%", accurate.by_round_usd_1pct, noise.by_round_usd_1pct), + ("Phase USD 10%", accurate.by_phase_usd_10pct, noise.by_phase_usd_10pct), + ("Phase USD 5%", accurate.by_phase_usd_5pct, noise.by_phase_usd_5pct), + ("Phase USD 2%", accurate.by_phase_usd_2pct, noise.by_phase_usd_2pct), + ("Phase USD 1%", accurate.by_phase_usd_1pct, noise.by_phase_usd_1pct), + ]: + for k in set(acc_d.keys()) | set(noise_d.keys()): + acc_pct = 100 * acc_d.get(k, 0) / accurate.total + noise_pct = 100 * noise_d.get(k, 0) / noise.total + diff = acc_pct - noise_pct # positive = more in accurate + if diff > 3.0 and acc_pct > 1: + all_inclusions.append((name, k, acc_pct, noise_pct, diff)) + + all_inclusions.sort(key=lambda x: -x[4]) + for name, k, acc_pct, noise_pct, diff in all_inclusions[:15]: + log(f" KEEP {name}={k}: accurate {acc_pct:.1f}% vs noise {noise_pct:.1f}% (+{diff:.1f}%)") + + except Exception as e: + log(f"Error: {e}") + import traceback + traceback.print_exc() + traceback.print_exc(file=report_file) + + report_file.close() + print(f"\nReport saved to: research/price_signal_analysis_report.txt") + +if __name__ == "__main__": + main() diff --git a/research/oracle_filter_analysis.md b/research/oracle_filter_analysis.md new file mode 100644 index 000000000..f7ccecd26 --- /dev/null +++ b/research/oracle_filter_analysis.md @@ -0,0 +1,174 @@ +# Oracle Filter Analysis + +## Summary + +Analysis of ~20M outputs across 2017-2018 to find filters that distinguish accurate price signals from noise. + +## Key Finding: Round USD is the Only Reliable Filter + +| Filter | Accuracy Advantage | Consistency | +|--------|-------------------|-------------| +| **Round USD = True** | **+20% to +29%** | **12/12 months** | +| Round BTC | +12% to -8% | Flips with price | +| Value range/Decade | varies | Shifts with price | +| Same-day spend | ~3% | Weak | +| Micro-round sats | 0-5% | Inconsistent | +| Tx pattern | <5% | Weak | +| Is smaller output | ~3-4% | Weak | + +## Why Other Filters Fail + +### Round BTC (Unreliable) +- Jan-Mar 2017 ($1k): Round BTC = True is GOOD (+10-12%) +- Jun-Jul 2017 ($2.5k): Round BTC = True is BAD (-7%) +- Reason: Round BTC only correlates with accuracy when it happens to align with round USD at current price + +### Value Range / Decade (Price-Dependent) +- At $1,000/BTC: Decade 5 (100k-1M sats) is good +- At $10,000/BTC: Decade 6 (1M-10M sats) is good +- At $100,000/BTC: Decade 7 (10M-100M sats) would be good +- These shift with price, making them useless as static filters + +## The Round USD Insight + +Round USD amounts ($1, $5, $10, $20, $50, $100, etc.) always map to the **same phase bins** regardless of price level: + +``` +$100 at $10,000/BTC = 1,000,000 sats → log10 = 6.0 → phase = 0.0 → bin 0 +$100 at $100,000/BTC = 100,000 sats → log10 = 5.0 → phase = 0.0 → bin 0 +$100 at $1,000/BTC = 10,000,000 sats → log10 = 7.0 → phase = 0.0 → bin 0 +``` + +The phase = `frac(log10(sats))` is **invariant** to price decade! + +## Round USD Phase Bins + +| USD Amount | log10(USD) | Phase = frac(log10) | Bin (×100) | +|------------|------------|---------------------|------------| +| $1, $10, $100, $1000 | 0, 1, 2, 3 | 0.00 | 0 | +| $1.50, $15, $150 | 0.18, 1.18, 2.18 | 0.18 | 18 | +| $2, $20, $200 | 0.30, 1.30, 2.30 | 0.30 | 30 | +| $2.50, $25, $250 | 0.40, 1.40, 2.40 | 0.40 | 40 | +| $3, $30, $300 | 0.48, 1.48, 2.48 | 0.48 | 48 | +| $4, $40, $400 | 0.60, 1.60, 2.60 | 0.60 | 60 | +| $5, $50, $500 | 0.70, 1.70, 2.70 | 0.70 | 70 | +| $6, $60, $600 | 0.78, 1.78, 2.78 | 0.78 | 78 | +| $7, $70, $700 | 0.85, 1.85, 2.85 | 0.85 | 85 | +| $8, $80, $800 | 0.90, 1.90, 2.90 | 0.90 | 90 | +| $9, $90, $900 | 0.95, 1.95, 2.95 | 0.95 | 95 | + +## Implementation Plan + +### Approach: Phase-Based Round USD Filtering + +Filter outputs to only those whose phase bin corresponds to a round USD amount. No price knowledge needed. + +```rust +/// Phase bins where round USD amounts cluster +/// Computed as: bin = round(frac(log10(usd_cents)) * 100) +const ROUND_USD_BINS: &[u8] = &[ + 0, // $1, $10, $100, $1000 (and $0.10, $0.01) + 18, // $1.50, $15, $150 + 30, // $2, $20, $200 + 40, // $2.50, $25, $250 + 48, // $3, $30, $300 + 60, // $4, $40, $400 + 70, // $5, $50, $500 + 78, // $6, $60, $600 + 85, // $7, $70, $700 + 90, // $8, $80, $800 + 95, // $9, $90, $900 +]; + +/// Check if a histogram bin corresponds to a round USD amount +fn is_round_usd_bin(bin: usize, tolerance: u8) -> bool { + let phase_bin = (bin % 100) as u8; + ROUND_USD_BINS.iter().any(|&round_bin| { + let diff = if phase_bin >= round_bin { + phase_bin - round_bin + } else { + round_bin - phase_bin + }; + // Handle wraparound (bin 99 is close to bin 0) + diff <= tolerance || (100 - diff) <= tolerance + }) +} +``` + +### Where to Apply Filter + +In `compute.rs`, when adding outputs to histogram: + +```rust +for sats in values { + if let Some(bin) = Histogram::sats_to_bin(sats) { + // Only include outputs in round-USD phase bins + if is_round_usd_bin(bin, 2) { // ±2 bin tolerance + block_sparse.push((bin as u16, 1.0)); + // ... rest of processing + } + } +} +``` + +### Expected Impact + +- Reduces histogram noise by ~60-70% (only ~35% of accurate outputs are round USD) +- Remaining outputs are 2-3x more likely to be accurate signals +- Stencil matching should be more reliable with cleaner signal +- Decade selection via anchors remains unchanged + +### Alternative: Weighted Approach + +Instead of hard filtering, weight round-USD bins higher: + +```rust +let weight = if is_round_usd_bin(bin, 2) { 3.0 } else { 1.0 }; +block_sparse.push((bin as u16, weight)); +``` + +This preserves some signal from non-round outputs while emphasizing round USD. + +## Bin Resolution: 100 vs 200 + +UTXOracle uses **200 bins per decade**. Current phase oracle uses 100. + +| Resolution | Precision | Round USD cluster | +|------------|-----------|-------------------| +| 100 bins | 1% per bin | Wider, more overlap | +| 200 bins | 0.5% per bin | Tighter, cleaner separation | + +**Round USD bins at 200 resolution:** +| USD Amount | Phase = frac(log10) | Bin (×200) | +|------------|---------------------|------------| +| $1, $10, $100 | 0.000 | 0 | +| $1.50, $15, $150 | 0.176 | 35 | +| $2, $20, $200 | 0.301 | 60 | +| $2.50, $25, $250 | 0.398 | 80 | +| $3, $30, $300 | 0.477 | 95 | +| $4, $40, $400 | 0.602 | 120 | +| $5, $50, $500 | 0.699 | 140 | +| $6, $60, $600 | 0.778 | 156 | +| $7, $70, $700 | 0.845 | 169 | +| $8, $80, $800 | 0.903 | 181 | +| $9, $90, $900 | 0.954 | 191 | + +**Recommendation**: Use 200 bins for: +1. Compatibility with UTXOracle stencil +2. Tighter round-USD detection +3. Better separation of signal from noise + +## Questions to Resolve + +1. **Tolerance**: ±2 bins (at 200) = ±1% vs ±4 bins = ±2% +2. **Hard filter vs weight**: Filter completely or just weight higher? +3. **Minimum count threshold**: What if too few outputs pass filter? +4. **Interaction with existing smooth_round_btc()**: Still needed? +5. **Migration**: Update PHASE_BINS constant from 100 to 200 + +## Validation Plan + +1. Implement phase-based filtering +2. Run on 2017-2018 data +3. Compare accuracy vs current approach +4. Tune tolerance parameter diff --git a/research/price_signal_analysis_report.txt b/research/price_signal_analysis_report.txt new file mode 100644 index 000000000..ee7c79107 --- /dev/null +++ b/research/price_signal_analysis_report.txt @@ -0,0 +1,2549 @@ +============================================================ +PHASE ORACLE SIGNAL ANALYSIS +============================================================ +Fetching date index... + + +############################################################ +# 2017-01 +############################################################ +Date range: 2017-01-01 to 2017-01-31 (dateindex 2915-2946) + +--- SUMMARY --- +Total outputs analyzed: 20,784,941 + Accurate (≤15% error): 2,194,762 (10.6%) + Close (15-30% error): 2,145,648 (10.3%) + Wrong decade: 4,805,602 (23.1%) + Noise: 11,638,929 (56.0%) + +================================================== +ACCURATE (within 15% of actual price) (n=2,194,762) +================================================== + +Output count: {1: '125,081 (5.7%)', 2: '1,502,699 (68.5%)', 3: '55,218 (2.5%)', 4: '26,954 (1.2%)', 5: '484,810 (22.1%)'} +Input count: {1: '1,550,243 (70.6%)', 2: '275,718 (12.6%)', 3: '124,520 (5.7%)', 4: '57,236 (2.6%)', 5: '187,045 (8.5%)'} +Output type: {'opreturn': '35 (0.0%)', 'p2ms': '6 (0.0%)', 'p2pk33': '5,975 (0.3%)', 'p2pk65': '1 (0.0%)', 'p2pkh': '1,738,507 (79.2%)', 'p2sh': '450,238 (20.5%)'} +Is round BTC: {False: '1,980,037 (90.2%)', True: '214,725 (9.8%)'} +Both outputs round: {False: '2,187,724 (99.7%)', True: '7,038 (0.3%)'} +Same-day spend: {False: '576,764 (26.3%)', True: '1,617,998 (73.7%)'} +Has OP_RETURN: {False: '2,188,679 (99.7%)', True: '6,083 (0.3%)'} +Witness size: {'0': '2,194,762 (100.0%)'} +Value range (sats): {'100M+': '292,390 (13.3%)', '100k-1M': '417,886 (19.0%)', '10M-100M': '489,096 (22.3%)', '10k-100k': '196,170 (8.9%)', '1M-10M': '774,907 (35.3%)', '<10k': '24,313 (1.1%)'} +Decade (10^N sats): {3: '24,313 (1.1%)', 4: '196,170 (8.9%)', 5: '417,886 (19.0%)', 6: '774,907 (35.3%)', 7: '489,096 (22.3%)', 8: '213,152 (9.7%)', 9: '64,142 (2.9%)', 10: '13,779 (0.6%)', 11: '1,304 (0.1%)', 12: '13 (0.0%)'} +Implied USD value: {'$1-$10': '595,665 (27.1%)', '$10-$100': '658,891 (30.0%)', '$100-$1k': '381,553 (17.4%)', '$10k+': '59,555 (2.7%)', '$1k-$10k': '163,942 (7.5%)', '<$1': '335,156 (15.3%)'} +Output index (2-out only): {0: '787,232 (35.9%)', 1: '715,467 (32.6%)'} +Is smaller output (2-out): {False: '690,717 (31.5%)', True: '811,982 (37.0%)'} +Round pattern (2-out): {'both_round': '7,038 (0.3%)', 'neither_round': '1,257,509 (57.3%)', 'only_other_round': '90,334 (4.1%)', 'only_this_round': '147,818 (6.7%)'} +Value ratio (2-out): {'10-30%': '207,905 (9.5%)', '30-50%': '151,604 (6.9%)', '50-70%': '151,005 (6.9%)', '70-90%': '164,972 (7.5%)', '<10%': '452,473 (20.6%)', '>90%': '374,740 (17.1%)'} +Error from actual price: {'5-10%': '32,313 (1.5%)', '<5%': '2,162,449 (98.5%)'} +Tx total value: {'0.01-0.1 BTC': '626,193 (28.5%)', '0.1-1 BTC': '663,749 (30.2%)', '1-10 BTC': '507,981 (23.1%)', '10+ BTC': '267,266 (12.2%)', '<0.01 BTC': '129,573 (5.9%)'} +Round USD (10% tol): {False: '185,925 (8.5%)', True: '2,008,837 (91.5%)'} +Round USD (5% tol): {False: '308,709 (14.1%)', True: '1,886,053 (85.9%)'} +Round USD (2% tol): {False: '777,165 (35.4%)', True: '1,417,597 (64.6%)'} +Round USD (1% tol): {False: '969,924 (44.2%)', True: '1,224,838 (55.8%)'} +Tx pattern: {'1-to-1': '100,229 (4.6%)', '1-to-2': '1,108,936 (50.5%)', '1-to-many': '341,078 (15.5%)', '2-to-1': '7,827 (0.4%)', '2-to-2': '201,178 (9.2%)', '2-to-many': '66,713 (3.0%)', 'many-to-1': '17,025 (0.8%)', 'many-to-2': '192,585 (8.8%)', 'many-to-many': '159,191 (7.3%)'} +Value similarity (2-out): {'different': '263,256 (12.0%)', 'moderate': '139,057 (6.3%)', 'nearly_equal': '42,055 (1.9%)', 'similar': '70,332 (3.2%)', 'very_different': '986,438 (44.9%)'} +Micro-round sats: {False: '1,929,122 (87.9%)', True: '265,640 (12.1%)'} +Phase USD (1% tol): {False: '1,649,482 (75.2%)', True: '545,280 (24.8%)'} +Phase USD (2% tol): {False: '1,367,711 (62.3%)', True: '827,051 (37.7%)'} +Phase USD (5% tol): {False: '587,433 (26.8%)', True: '1,607,329 (73.2%)'} +Phase USD (10% tol): {True: '2,194,762 (100.0%)'} + +Top 10 bins: + Bin 0: 318,635 (14.5%) + Bin 4: 309,737 (14.1%) + Bin 3: 251,392 (11.5%) + Bin 2: 186,051 (8.5%) + Bin 5: 180,060 (8.2%) + Bin 7: 151,894 (6.9%) + Bin 1: 141,676 (6.5%) + Bin 6: 128,561 (5.9%) + Bin 99: 88,269 (4.0%) + Bin 8: 85,764 (3.9%) + +================================================== +NOISE (no decade matches) (n=11,638,929) +================================================== + +Output count: {1: '671,672 (5.8%)', 2: '7,553,923 (64.9%)', 3: '297,359 (2.6%)', 4: '157,871 (1.4%)', 5: '2,958,104 (25.4%)'} +Input count: {1: '8,543,222 (73.4%)', 2: '1,281,977 (11.0%)', 3: '554,978 (4.8%)', 4: '274,686 (2.4%)', 5: '984,066 (8.5%)'} +Output type: {'opreturn': '61 (0.0%)', 'p2ms': '57 (0.0%)', 'p2pk33': '7,402 (0.1%)', 'p2pk65': '2 (0.0%)', 'p2pkh': '9,362,938 (80.4%)', 'p2sh': '2,268,469 (19.5%)'} +Is round BTC: {False: '11,030,276 (94.8%)', True: '608,653 (5.2%)'} +Both outputs round: {False: '11,628,202 (99.9%)', True: '10,727 (0.1%)'} +Same-day spend: {False: '2,858,539 (24.6%)', True: '8,780,390 (75.4%)'} +Has OP_RETURN: {False: '11,532,973 (99.1%)', True: '105,956 (0.9%)'} +Witness size: {'0': '11,638,929 (100.0%)'} +Value range (sats): {'100M+': '1,510,761 (13.0%)', '100k-1M': '2,144,630 (18.4%)', '10M-100M': '2,284,789 (19.6%)', '10k-100k': '1,758,095 (15.1%)', '1M-10M': '3,646,842 (31.3%)', '<10k': '293,812 (2.5%)'} +Decade (10^N sats): {3: '293,812 (2.5%)', 4: '1,758,095 (15.1%)', 5: '2,144,630 (18.4%)', 6: '3,646,842 (31.3%)', 7: '2,284,789 (19.6%)', 8: '1,122,284 (9.6%)', 9: '327,415 (2.8%)', 10: '57,145 (0.5%)', 11: '3,908 (0.0%)', 12: '9 (0.0%)'} +Implied USD value: {'$1-$10': '2,195,278 (18.9%)', '$10-$100': '3,598,463 (30.9%)', '$100-$1k': '2,274,870 (19.5%)', '$10k+': '382,570 (3.3%)', '$1k-$10k': '1,109,324 (9.5%)', '<$1': '2,078,424 (17.9%)'} +Output index (2-out only): {0: '3,716,775 (31.9%)', 1: '3,837,148 (33.0%)'} +Is smaller output (2-out): {False: '3,894,127 (33.5%)', True: '3,659,796 (31.4%)'} +Round pattern (2-out): {'both_round': '10,727 (0.1%)', 'neither_round': '6,502,192 (55.9%)', 'only_other_round': '614,026 (5.3%)', 'only_this_round': '426,978 (3.7%)'} +Value ratio (2-out): {'10-30%': '940,072 (8.1%)', '30-50%': '650,436 (5.6%)', '50-70%': '688,489 (5.9%)', '70-90%': '988,407 (8.5%)', '<10%': '2,069,288 (17.8%)', '>90%': '2,217,231 (19.1%)'} +Error from actual price: {'15-25%': '1,706,916 (14.7%)', '25-50%': '5,038,733 (43.3%)', '50%+': '4,893,280 (42.0%)'} +Tx total value: {'0.01-0.1 BTC': '2,974,275 (25.6%)', '0.1-1 BTC': '3,497,316 (30.0%)', '1-10 BTC': '2,748,446 (23.6%)', '10+ BTC': '1,581,641 (13.6%)', '<0.01 BTC': '837,251 (7.2%)'} +Round USD (10% tol): {False: '8,492,977 (73.0%)', True: '3,145,952 (27.0%)'} +Round USD (5% tol): {False: '9,649,843 (82.9%)', True: '1,989,086 (17.1%)'} +Round USD (2% tol): {False: '10,278,728 (88.3%)', True: '1,360,201 (11.7%)'} +Round USD (1% tol): {False: '10,505,507 (90.3%)', True: '1,133,422 (9.7%)'} +Tx pattern: {'1-to-1': '521,175 (4.5%)', '1-to-2': '5,930,965 (51.0%)', '1-to-many': '2,091,082 (18.0%)', '2-to-1': '46,412 (0.4%)', '2-to-2': '891,466 (7.7%)', '2-to-many': '344,099 (3.0%)', 'many-to-1': '104,085 (0.9%)', 'many-to-2': '731,492 (6.3%)', 'many-to-many': '978,153 (8.4%)'} +Value similarity (2-out): {'different': '1,354,125 (11.6%)', 'moderate': '746,414 (6.4%)', 'nearly_equal': '101,940 (0.9%)', 'similar': '258,452 (2.2%)', 'very_different': '5,076,530 (43.6%)'} +Micro-round sats: {False: '10,469,415 (90.0%)', True: '1,169,514 (10.0%)'} +Phase USD (1% tol): {False: '7,508,336 (64.5%)', True: '4,130,593 (35.5%)'} +Phase USD (2% tol): {False: '5,166,026 (44.4%)', True: '6,472,903 (55.6%)'} +Phase USD (5% tol): {False: '749,429 (6.4%)', True: '10,889,500 (93.6%)'} +Phase USD (10% tol): {True: '11,638,929 (100.0%)'} + +Top 10 bins: + Bin 30: 509,751 (4.4%) + Bin 47: 430,806 (3.7%) + Bin 17: 291,350 (2.5%) + Bin 39: 264,110 (2.3%) + Bin 43: 262,725 (2.3%) + Bin 34: 259,233 (2.2%) + Bin 35: 217,883 (1.9%) + Bin 33: 216,068 (1.9%) + Bin 90: 214,472 (1.8%) + Bin 38: 204,484 (1.8%) + +================================================== +KEY DIFFERENCES (accurate vs noise): +================================================== + +Output count: + 2: 68.5% vs 64.9% (diff: +3.6%) + 5: 22.1% vs 25.4% (diff: -3.3%) + +Input count: + 1: 70.6% vs 73.4% (diff: -2.8%) + +Output type: + +Is round BTC: + False: 90.2% vs 94.8% (diff: -4.6%) + True: 9.8% vs 5.2% (diff: +4.6%) + +Both round: + +Same-day spend: + +OP_RETURN: + +Witness size: + +Value range (sats): + 10M-100M: 22.3% vs 19.6% (diff: +2.7%) + 10k-100k: 8.9% vs 15.1% (diff: -6.2%) + 1M-10M: 35.3% vs 31.3% (diff: +4.0%) + +Decade (10^N sats): + 4: 8.9% vs 15.1% (diff: -6.2%) + 6: 35.3% vs 31.3% (diff: +4.0%) + 7: 22.3% vs 19.6% (diff: +2.7%) + +Implied USD: + $1-$10: 27.1% vs 18.9% (diff: +8.3%) + $100-$1k: 17.4% vs 19.5% (diff: -2.2%) + $1k-$10k: 7.5% vs 9.5% (diff: -2.1%) + <$1: 15.3% vs 17.9% (diff: -2.6%) + +Output index (2-out): + 0: 35.9% vs 31.9% (diff: +3.9%) + +Is smaller (2-out): + True: 37.0% vs 31.4% (diff: +5.6%) + +Round pattern (2-out): + only_this_round: 6.7% vs 3.7% (diff: +3.1%) + +Value ratio (2-out): + <10%: 20.6% vs 17.8% (diff: +2.8%) + +Error from price: + 15-25%: 0.0% vs 14.7% (diff: -14.7%) + 25-50%: 0.0% vs 43.3% (diff: -43.3%) + 50%+: 0.0% vs 42.0% (diff: -42.0%) + <5%: 98.5% vs 0.0% (diff: +98.5%) + +Tx total value: + 0.01-0.1 BTC: 28.5% vs 25.6% (diff: +3.0%) + +Round USD (10%): + False: 8.5% vs 73.0% (diff: -64.5%) + True: 91.5% vs 27.0% (diff: +64.5%) + +Round USD (5%): + False: 14.1% vs 82.9% (diff: -68.8%) + True: 85.9% vs 17.1% (diff: +68.8%) + +Round USD (2%): + False: 35.4% vs 88.3% (diff: -52.9%) + True: 64.6% vs 11.7% (diff: +52.9%) + +Round USD (1%): + False: 44.2% vs 90.3% (diff: -46.1%) + True: 55.8% vs 9.7% (diff: +46.1%) + +Phase USD (1%): + False: 75.2% vs 64.5% (diff: +10.6%) + True: 24.8% vs 35.5% (diff: -10.6%) + +Phase USD (2%): + False: 62.3% vs 44.4% (diff: +17.9%) + True: 37.7% vs 55.6% (diff: -17.9%) + +Phase USD (5%): + False: 26.8% vs 6.4% (diff: +20.3%) + True: 73.2% vs 93.6% (diff: -20.3%) + +Phase USD (10%): + +Tx pattern: + 1-to-many: 15.5% vs 18.0% (diff: -2.4%) + many-to-2: 8.8% vs 6.3% (diff: +2.5%) + +Value similarity (2-out): + +Micro-round sats: + False: 87.9% vs 90.0% (diff: -2.1%) + True: 12.1% vs 10.0% (diff: +2.1%) + +================================================== +EXCLUSION CANDIDATES (overrepresented in noise): +================================================== +Characteristics where noise% > accurate% suggest exclusion filters: + + EXCLUDE Round USD 5%=False: noise 82.9% vs accurate 14.1% (+68.8%) + EXCLUDE Round USD 10%=False: noise 73.0% vs accurate 8.5% (+64.5%) + EXCLUDE Round USD 2%=False: noise 88.3% vs accurate 35.4% (+52.9%) + EXCLUDE Round USD 1%=False: noise 90.3% vs accurate 44.2% (+46.1%) + EXCLUDE Phase USD 5%=True: noise 93.6% vs accurate 73.2% (+20.3%) + EXCLUDE Phase USD 2%=True: noise 55.6% vs accurate 37.7% (+17.9%) + EXCLUDE Phase USD 1%=True: noise 35.5% vs accurate 24.8% (+10.6%) + EXCLUDE Value range=10k-100k: noise 15.1% vs accurate 8.9% (+6.2%) + EXCLUDE Decade=4: noise 15.1% vs accurate 8.9% (+6.2%) + EXCLUDE Is round BTC=False: noise 94.8% vs accurate 90.2% (+4.6%) + EXCLUDE Output count=5: noise 25.4% vs accurate 22.1% (+3.3%) + +================================================== +INCLUSION SIGNALS (overrepresented in accurate): +================================================== +Characteristics where accurate% > noise% are good signals: + + KEEP Round USD 5%=True: accurate 85.9% vs noise 17.1% (+68.8%) + KEEP Round USD 10%=True: accurate 91.5% vs noise 27.0% (+64.5%) + KEEP Round USD 2%=True: accurate 64.6% vs noise 11.7% (+52.9%) + KEEP Round USD 1%=True: accurate 55.8% vs noise 9.7% (+46.1%) + KEEP Phase USD 5%=False: accurate 26.8% vs noise 6.4% (+20.3%) + KEEP Phase USD 2%=False: accurate 62.3% vs noise 44.4% (+17.9%) + KEEP Phase USD 1%=False: accurate 75.2% vs noise 64.5% (+10.6%) + KEEP Implied USD=$1-$10: accurate 27.1% vs noise 18.9% (+8.3%) + KEEP Is smaller (2-out)=True: accurate 37.0% vs noise 31.4% (+5.6%) + KEEP Is round BTC=True: accurate 9.8% vs noise 5.2% (+4.6%) + KEEP Value range=1M-10M: accurate 35.3% vs noise 31.3% (+4.0%) + KEEP Decade=6: accurate 35.3% vs noise 31.3% (+4.0%) + KEEP Output count=2: accurate 68.5% vs noise 64.9% (+3.6%) + + +############################################################ +# 2017-02 +############################################################ +Date range: 2017-02-01 to 2017-02-28 (dateindex 2946-2974) + +--- SUMMARY --- +Total outputs analyzed: 19,635,062 + Accurate (≤15% error): 2,175,861 (11.1%) + Close (15-30% error): 1,942,576 (9.9%) + Wrong decade: 4,495,499 (22.9%) + Noise: 11,021,126 (56.1%) + +================================================== +ACCURATE (within 15% of actual price) (n=2,175,861) +================================================== + +Output count: {1: '126,575 (5.8%)', 2: '1,505,826 (69.2%)', 3: '45,629 (2.1%)', 4: '21,941 (1.0%)', 5: '475,890 (21.9%)'} +Input count: {1: '1,491,077 (68.5%)', 2: '268,345 (12.3%)', 3: '142,641 (6.6%)', 4: '64,978 (3.0%)', 5: '208,820 (9.6%)'} +Output type: {'opreturn': '7 (0.0%)', 'p2ms': '41 (0.0%)', 'p2pk33': '8,956 (0.4%)', 'p2pk65': '1 (0.0%)', 'p2pkh': '1,676,529 (77.1%)', 'p2sh': '490,327 (22.5%)'} +Is round BTC: {False: '1,712,005 (78.7%)', True: '463,856 (21.3%)'} +Both outputs round: {False: '2,162,975 (99.4%)', True: '12,886 (0.6%)'} +Same-day spend: {False: '575,313 (26.4%)', True: '1,600,548 (73.6%)'} +Has OP_RETURN: {False: '2,166,223 (99.6%)', True: '9,638 (0.4%)'} +Witness size: {'0': '2,175,861 (100.0%)'} +Value range (sats): {'100M+': '193,849 (8.9%)', '100k-1M': '585,726 (26.9%)', '10M-100M': '314,223 (14.4%)', '10k-100k': '292,614 (13.4%)', '1M-10M': '755,329 (34.7%)', '<10k': '34,120 (1.6%)'} +Decade (10^N sats): {3: '34,120 (1.6%)', 4: '292,614 (13.4%)', 5: '585,726 (26.9%)', 6: '755,329 (34.7%)', 7: '314,223 (14.4%)', 8: '149,491 (6.9%)', 9: '38,710 (1.8%)', 10: '5,195 (0.2%)', 11: '452 (0.0%)', 12: '1 (0.0%)'} +Implied USD value: {'$1-$10': '525,873 (24.2%)', '$10-$100': '738,003 (33.9%)', '$100-$1k': '400,600 (18.4%)', '$10k+': '69,414 (3.2%)', '$1k-$10k': '191,188 (8.8%)', '<$1': '250,783 (11.5%)'} +Output index (2-out only): {0: '785,826 (36.1%)', 1: '720,000 (33.1%)'} +Is smaller output (2-out): {False: '669,873 (30.8%)', True: '835,953 (38.4%)'} +Round pattern (2-out): {'both_round': '12,886 (0.6%)', 'neither_round': '1,116,866 (51.3%)', 'only_other_round': '88,959 (4.1%)', 'only_this_round': '287,115 (13.2%)'} +Value ratio (2-out): {'10-30%': '201,626 (9.3%)', '30-50%': '155,619 (7.2%)', '50-70%': '131,264 (6.0%)', '70-90%': '159,984 (7.4%)', '<10%': '478,708 (22.0%)', '>90%': '378,625 (17.4%)'} +Error from actual price: {'<5%': '2,175,861 (100.0%)'} +Tx total value: {'0.01-0.1 BTC': '593,693 (27.3%)', '0.1-1 BTC': '619,200 (28.5%)', '1-10 BTC': '506,021 (23.3%)', '10+ BTC': '253,494 (11.7%)', '<0.01 BTC': '203,453 (9.4%)'} +Round USD (10% tol): {False: '128,190 (5.9%)', True: '2,047,671 (94.1%)'} +Round USD (5% tol): {False: '180,369 (8.3%)', True: '1,995,492 (91.7%)'} +Round USD (2% tol): {False: '705,478 (32.4%)', True: '1,470,383 (67.6%)'} +Round USD (1% tol): {False: '946,641 (43.5%)', True: '1,229,220 (56.5%)'} +Tx pattern: {'1-to-1': '106,864 (4.9%)', '1-to-2': '1,084,284 (49.8%)', '1-to-many': '299,929 (13.8%)', '2-to-1': '7,401 (0.3%)', '2-to-2': '186,472 (8.6%)', '2-to-many': '74,472 (3.4%)', 'many-to-1': '12,310 (0.6%)', 'many-to-2': '235,070 (10.8%)', 'many-to-many': '169,059 (7.8%)'} +Value similarity (2-out): {'different': '253,868 (11.7%)', 'moderate': '128,172 (5.9%)', 'nearly_equal': '49,568 (2.3%)', 'similar': '60,519 (2.8%)', 'very_different': '1,009,778 (46.4%)'} +Micro-round sats: {False: '1,762,486 (81.0%)', True: '413,375 (19.0%)'} +Phase USD (1% tol): {False: '775,049 (35.6%)', True: '1,400,812 (64.4%)'} +Phase USD (2% tol): {False: '99,209 (4.6%)', True: '2,076,652 (95.4%)'} +Phase USD (5% tol): {True: '2,175,861 (100.0%)'} +Phase USD (10% tol): {True: '2,175,861 (100.0%)'} + +Top 10 bins: + Bin 0: 680,110 (31.3%) + Bin 99: 284,185 (13.1%) + Bin 97: 209,029 (9.6%) + Bin 98: 196,566 (9.0%) + Bin 1: 144,256 (6.6%) + Bin 95: 126,337 (5.8%) + Bin 96: 106,545 (4.9%) + Bin 92: 91,281 (4.2%) + Bin 94: 75,946 (3.5%) + Bin 93: 69,348 (3.2%) + +================================================== +NOISE (no decade matches) (n=11,021,126) +================================================== + +Output count: {1: '654,656 (5.9%)', 2: '7,419,331 (67.3%)', 3: '256,579 (2.3%)', 4: '134,434 (1.2%)', 5: '2,556,126 (23.2%)'} +Input count: {1: '8,254,029 (74.9%)', 2: '1,167,632 (10.6%)', 3: '494,548 (4.5%)', 4: '250,461 (2.3%)', 5: '854,456 (7.8%)'} +Output type: {'p2ms': '245 (0.0%)', 'p2pk33': '7,308 (0.1%)', 'p2pk65': '1 (0.0%)', 'p2pkh': '8,665,297 (78.6%)', 'p2sh': '2,348,275 (21.3%)'} +Is round BTC: {False: '10,534,548 (95.6%)', True: '486,578 (4.4%)'} +Both outputs round: {False: '11,013,351 (99.9%)', True: '7,775 (0.1%)'} +Same-day spend: {False: '2,491,199 (22.6%)', True: '8,529,927 (77.4%)'} +Has OP_RETURN: {False: '10,911,176 (99.0%)', True: '109,950 (1.0%)'} +Witness size: {'0': '11,021,126 (100.0%)'} +Value range (sats): {'100M+': '1,588,086 (14.4%)', '100k-1M': '2,126,038 (19.3%)', '10M-100M': '2,253,703 (20.4%)', '10k-100k': '1,207,366 (11.0%)', '1M-10M': '3,632,117 (33.0%)', '<10k': '213,816 (1.9%)'} +Decade (10^N sats): {3: '213,816 (1.9%)', 4: '1,207,366 (11.0%)', 5: '2,126,038 (19.3%)', 6: '3,632,117 (33.0%)', 7: '2,253,703 (20.4%)', 8: '1,107,903 (10.1%)', 9: '428,057 (3.9%)', 10: '46,771 (0.4%)', 11: '5,329 (0.0%)', 12: '26 (0.0%)'} +Implied USD value: {'$1-$10': '2,126,038 (19.3%)', '$10-$100': '3,632,117 (33.0%)', '$100-$1k': '2,253,703 (20.4%)', '$10k+': '480,183 (4.4%)', '$1k-$10k': '1,107,903 (10.1%)', '<$1': '1,421,182 (12.9%)'} +Output index (2-out only): {0: '3,683,597 (33.4%)', 1: '3,735,734 (33.9%)'} +Is smaller output (2-out): {False: '3,829,635 (34.7%)', True: '3,589,696 (32.6%)'} +Round pattern (2-out): {'both_round': '7,775 (0.1%)', 'neither_round': '6,529,430 (59.2%)', 'only_other_round': '548,130 (5.0%)', 'only_this_round': '333,996 (3.0%)'} +Value ratio (2-out): {'10-30%': '883,687 (8.0%)', '30-50%': '621,239 (5.6%)', '50-70%': '673,428 (6.1%)', '70-90%': '906,099 (8.2%)', '<10%': '2,084,770 (18.9%)', '>90%': '2,250,108 (20.4%)'} +Error from actual price: {'15-25%': '1,601,868 (14.5%)', '25-50%': '4,504,079 (40.9%)', '50%+': '4,915,179 (44.6%)'} +Tx total value: {'0.01-0.1 BTC': '2,806,604 (25.5%)', '0.1-1 BTC': '3,096,291 (28.1%)', '1-10 BTC': '2,600,433 (23.6%)', '10+ BTC': '1,697,409 (15.4%)', '<0.01 BTC': '820,389 (7.4%)'} +Round USD (10% tol): {False: '8,097,019 (73.5%)', True: '2,924,107 (26.5%)'} +Round USD (5% tol): {False: '9,191,585 (83.4%)', True: '1,829,541 (16.6%)'} +Round USD (2% tol): {False: '9,872,030 (89.6%)', True: '1,149,096 (10.4%)'} +Round USD (1% tol): {False: '10,094,517 (91.6%)', True: '926,609 (8.4%)'} +Tx pattern: {'1-to-1': '516,432 (4.7%)', '1-to-2': '5,915,098 (53.7%)', '1-to-many': '1,822,499 (16.5%)', '2-to-1': '48,794 (0.4%)', '2-to-2': '787,763 (7.1%)', '2-to-many': '331,075 (3.0%)', 'many-to-1': '89,430 (0.8%)', 'many-to-2': '716,470 (6.5%)', 'many-to-many': '793,565 (7.2%)'} +Value similarity (2-out): {'different': '1,235,044 (11.2%)', 'moderate': '716,570 (6.5%)', 'nearly_equal': '102,776 (0.9%)', 'similar': '260,722 (2.4%)', 'very_different': '5,066,445 (46.0%)'} +Micro-round sats: {False: '9,923,298 (90.0%)', True: '1,097,828 (10.0%)'} +Phase USD (1% tol): {False: '7,350,213 (66.7%)', True: '3,670,913 (33.3%)'} +Phase USD (2% tol): {False: '5,305,870 (48.1%)', True: '5,715,256 (51.9%)'} +Phase USD (5% tol): {False: '1,311,431 (11.9%)', True: '9,709,695 (88.1%)'} +Phase USD (10% tol): {True: '11,021,126 (100.0%)'} + +Top 10 bins: + Bin 30: 474,026 (4.3%) + Bin 17: 256,385 (2.3%) + Bin 47: 249,925 (2.3%) + Bin 29: 228,357 (2.1%) + Bin 77: 227,309 (2.1%) + Bin 39: 224,393 (2.0%) + Bin 27: 206,813 (1.9%) + Bin 90: 200,876 (1.8%) + Bin 84: 198,875 (1.8%) + Bin 43: 196,837 (1.8%) + +================================================== +KEY DIFFERENCES (accurate vs noise): +================================================== + +Output count: + +Input count: + 1: 68.5% vs 74.9% (diff: -6.4%) + 3: 6.6% vs 4.5% (diff: +2.1%) + +Output type: + +Is round BTC: + False: 78.7% vs 95.6% (diff: -16.9%) + True: 21.3% vs 4.4% (diff: +16.9%) + +Both round: + +Same-day spend: + False: 26.4% vs 22.6% (diff: +3.8%) + True: 73.6% vs 77.4% (diff: -3.8%) + +OP_RETURN: + +Witness size: + +Value range (sats): + 100M+: 8.9% vs 14.4% (diff: -5.5%) + 100k-1M: 26.9% vs 19.3% (diff: +7.6%) + 10M-100M: 14.4% vs 20.4% (diff: -6.0%) + 10k-100k: 13.4% vs 11.0% (diff: +2.5%) + +Decade (10^N sats): + 4: 13.4% vs 11.0% (diff: +2.5%) + 5: 26.9% vs 19.3% (diff: +7.6%) + 7: 14.4% vs 20.4% (diff: -6.0%) + 8: 6.9% vs 10.1% (diff: -3.2%) + 9: 1.8% vs 3.9% (diff: -2.1%) + +Implied USD: + $1-$10: 24.2% vs 19.3% (diff: +4.9%) + $100-$1k: 18.4% vs 20.4% (diff: -2.0%) + +Output index (2-out): + 0: 36.1% vs 33.4% (diff: +2.7%) + +Is smaller (2-out): + False: 30.8% vs 34.7% (diff: -4.0%) + True: 38.4% vs 32.6% (diff: +5.8%) + +Round pattern (2-out): + neither_round: 51.3% vs 59.2% (diff: -7.9%) + only_this_round: 13.2% vs 3.0% (diff: +10.2%) + +Value ratio (2-out): + <10%: 22.0% vs 18.9% (diff: +3.1%) + >90%: 17.4% vs 20.4% (diff: -3.0%) + +Error from price: + 15-25%: 0.0% vs 14.5% (diff: -14.5%) + 25-50%: 0.0% vs 40.9% (diff: -40.9%) + 50%+: 0.0% vs 44.6% (diff: -44.6%) + <5%: 100.0% vs 0.0% (diff: +100.0%) + +Tx total value: + 10+ BTC: 11.7% vs 15.4% (diff: -3.8%) + +Round USD (10%): + False: 5.9% vs 73.5% (diff: -67.6%) + True: 94.1% vs 26.5% (diff: +67.6%) + +Round USD (5%): + False: 8.3% vs 83.4% (diff: -75.1%) + True: 91.7% vs 16.6% (diff: +75.1%) + +Round USD (2%): + False: 32.4% vs 89.6% (diff: -57.2%) + True: 67.6% vs 10.4% (diff: +57.2%) + +Round USD (1%): + False: 43.5% vs 91.6% (diff: -48.1%) + True: 56.5% vs 8.4% (diff: +48.1%) + +Phase USD (1%): + False: 35.6% vs 66.7% (diff: -31.1%) + True: 64.4% vs 33.3% (diff: +31.1%) + +Phase USD (2%): + False: 4.6% vs 48.1% (diff: -43.6%) + True: 95.4% vs 51.9% (diff: +43.6%) + +Phase USD (5%): + False: 0.0% vs 11.9% (diff: -11.9%) + True: 100.0% vs 88.1% (diff: +11.9%) + +Phase USD (10%): + +Tx pattern: + 1-to-2: 49.8% vs 53.7% (diff: -3.8%) + 1-to-many: 13.8% vs 16.5% (diff: -2.8%) + many-to-2: 10.8% vs 6.5% (diff: +4.3%) + +Value similarity (2-out): + +Micro-round sats: + False: 81.0% vs 90.0% (diff: -9.0%) + True: 19.0% vs 10.0% (diff: +9.0%) + +================================================== +EXCLUSION CANDIDATES (overrepresented in noise): +================================================== +Characteristics where noise% > accurate% suggest exclusion filters: + + EXCLUDE Round USD 5%=False: noise 83.4% vs accurate 8.3% (+75.1%) + EXCLUDE Round USD 10%=False: noise 73.5% vs accurate 5.9% (+67.6%) + EXCLUDE Round USD 2%=False: noise 89.6% vs accurate 32.4% (+57.2%) + EXCLUDE Round USD 1%=False: noise 91.6% vs accurate 43.5% (+48.1%) + EXCLUDE Phase USD 2%=False: noise 48.1% vs accurate 4.6% (+43.6%) + EXCLUDE Phase USD 1%=False: noise 66.7% vs accurate 35.6% (+31.1%) + EXCLUDE Is round BTC=False: noise 95.6% vs accurate 78.7% (+16.9%) + EXCLUDE Phase USD 5%=False: noise 11.9% vs accurate 0.0% (+11.9%) + EXCLUDE Micro-round sats=False: noise 90.0% vs accurate 81.0% (+9.0%) + EXCLUDE Value range=10M-100M: noise 20.4% vs accurate 14.4% (+6.0%) + EXCLUDE Decade=7: noise 20.4% vs accurate 14.4% (+6.0%) + EXCLUDE Value range=100M+: noise 14.4% vs accurate 8.9% (+5.5%) + EXCLUDE Tx pattern=1-to-2: noise 53.7% vs accurate 49.8% (+3.8%) + EXCLUDE Tx total value=10+ BTC: noise 15.4% vs accurate 11.7% (+3.8%) + EXCLUDE Decade=8: noise 10.1% vs accurate 6.9% (+3.2%) + +================================================== +INCLUSION SIGNALS (overrepresented in accurate): +================================================== +Characteristics where accurate% > noise% are good signals: + + KEEP Round USD 5%=True: accurate 91.7% vs noise 16.6% (+75.1%) + KEEP Round USD 10%=True: accurate 94.1% vs noise 26.5% (+67.6%) + KEEP Round USD 2%=True: accurate 67.6% vs noise 10.4% (+57.2%) + KEEP Round USD 1%=True: accurate 56.5% vs noise 8.4% (+48.1%) + KEEP Phase USD 2%=True: accurate 95.4% vs noise 51.9% (+43.6%) + KEEP Phase USD 1%=True: accurate 64.4% vs noise 33.3% (+31.1%) + KEEP Is round BTC=True: accurate 21.3% vs noise 4.4% (+16.9%) + KEEP Phase USD 5%=True: accurate 100.0% vs noise 88.1% (+11.9%) + KEEP Value range=100k-1M: accurate 26.9% vs noise 19.3% (+7.6%) + KEEP Decade=5: accurate 26.9% vs noise 19.3% (+7.6%) + KEEP Is smaller (2-out)=True: accurate 38.4% vs noise 32.6% (+5.8%) + KEEP Implied USD=$1-$10: accurate 24.2% vs noise 19.3% (+4.9%) + KEEP Tx pattern=many-to-2: accurate 10.8% vs noise 6.5% (+4.3%) + KEEP Value ratio=<10%: accurate 22.0% vs noise 18.9% (+3.1%) + + +############################################################ +# 2017-03 +############################################################ +Date range: 2017-03-01 to 2017-03-31 (dateindex 2974-3005) + +--- SUMMARY --- +Total outputs analyzed: 21,768,446 + Accurate (≤15% error): 2,503,254 (11.5%) + Close (15-30% error): 1,998,582 (9.2%) + Wrong decade: 5,110,180 (23.5%) + Noise: 12,156,430 (55.8%) + +================================================== +ACCURATE (within 15% of actual price) (n=2,503,254) +================================================== + +Output count: {1: '140,102 (5.6%)', 2: '1,673,936 (66.9%)', 3: '66,369 (2.7%)', 4: '34,618 (1.4%)', 5: '588,229 (23.5%)'} +Input count: {1: '1,768,677 (70.7%)', 2: '278,397 (11.1%)', 3: '131,303 (5.2%)', 4: '71,543 (2.9%)', 5: '253,334 (10.1%)'} +Output type: {'p2ms': '26 (0.0%)', 'p2pk33': '3,734 (0.1%)', 'p2pk65': '2 (0.0%)', 'p2pkh': '1,973,494 (78.8%)', 'p2sh': '525,998 (21.0%)'} +Is round BTC: {False: '2,074,608 (82.9%)', True: '428,646 (17.1%)'} +Both outputs round: {False: '2,490,488 (99.5%)', True: '12,766 (0.5%)'} +Same-day spend: {False: '624,862 (25.0%)', True: '1,878,392 (75.0%)'} +Has OP_RETURN: {False: '2,489,880 (99.5%)', True: '13,374 (0.5%)'} +Witness size: {'0': '2,503,254 (100.0%)'} +Value range (sats): {'100M+': '244,563 (9.8%)', '100k-1M': '648,327 (25.9%)', '10M-100M': '405,927 (16.2%)', '10k-100k': '326,738 (13.1%)', '1M-10M': '833,275 (33.3%)', '<10k': '44,424 (1.8%)'} +Decade (10^N sats): {3: '44,424 (1.8%)', 4: '326,738 (13.1%)', 5: '648,327 (25.9%)', 6: '833,275 (33.3%)', 7: '405,927 (16.2%)', 8: '178,586 (7.1%)', 9: '57,090 (2.3%)', 10: '8,131 (0.3%)', 11: '754 (0.0%)', 12: '2 (0.0%)'} +Implied USD value: {'$1-$10': '587,965 (23.5%)', '$10-$100': '789,994 (31.6%)', '$100-$1k': '507,297 (20.3%)', '$10k+': '101,838 (4.1%)', '$1k-$10k': '243,025 (9.7%)', '<$1': '273,135 (10.9%)'} +Output index (2-out only): {0: '866,770 (34.6%)', 1: '807,166 (32.2%)'} +Is smaller output (2-out): {False: '772,950 (30.9%)', True: '900,986 (36.0%)'} +Round pattern (2-out): {'both_round': '12,766 (0.5%)', 'neither_round': '1,289,013 (51.5%)', 'only_other_round': '104,962 (4.2%)', 'only_this_round': '267,195 (10.7%)'} +Value ratio (2-out): {'10-30%': '214,019 (8.5%)', '30-50%': '162,926 (6.5%)', '50-70%': '141,374 (5.6%)', '70-90%': '192,620 (7.7%)', '<10%': '524,041 (20.9%)', '>90%': '438,956 (17.5%)'} +Error from actual price: {'<5%': '2,503,254 (100.0%)'} +Tx total value: {'0.01-0.1 BTC': '628,383 (25.1%)', '0.1-1 BTC': '705,451 (28.2%)', '1-10 BTC': '574,071 (22.9%)', '10+ BTC': '374,638 (15.0%)', '<0.01 BTC': '220,711 (8.8%)'} +Round USD (10% tol): {False: '112,872 (4.5%)', True: '2,390,382 (95.5%)'} +Round USD (5% tol): {False: '165,215 (6.6%)', True: '2,338,039 (93.4%)'} +Round USD (2% tol): {False: '749,395 (29.9%)', True: '1,753,859 (70.1%)'} +Round USD (1% tol): {False: '991,835 (39.6%)', True: '1,511,419 (60.4%)'} +Tx pattern: {'1-to-1': '112,660 (4.5%)', '1-to-2': '1,258,174 (50.3%)', '1-to-many': '397,843 (15.9%)', '2-to-1': '10,703 (0.4%)', '2-to-2': '199,744 (8.0%)', '2-to-many': '67,950 (2.7%)', 'many-to-1': '16,739 (0.7%)', 'many-to-2': '216,018 (8.6%)', 'many-to-many': '223,423 (8.9%)'} +Value similarity (2-out): {'different': '280,020 (11.2%)', 'moderate': '142,082 (5.7%)', 'nearly_equal': '42,735 (1.7%)', 'similar': '67,146 (2.7%)', 'very_different': '1,135,827 (45.4%)'} +Micro-round sats: {False: '2,048,313 (81.8%)', True: '454,941 (18.2%)'} +Phase USD (1% tol): {False: '938,782 (37.5%)', True: '1,564,472 (62.5%)'} +Phase USD (2% tol): {False: '257,119 (10.3%)', True: '2,246,135 (89.7%)'} +Phase USD (5% tol): {False: '11,038 (0.4%)', True: '2,492,216 (99.6%)'} +Phase USD (10% tol): {True: '2,503,254 (100.0%)'} + +Top 10 bins: + Bin 0: 587,189 (23.5%) + Bin 90: 202,787 (8.1%) + Bin 99: 184,544 (7.4%) + Bin 95: 150,780 (6.0%) + Bin 98: 148,339 (5.9%) + Bin 89: 138,187 (5.5%) + Bin 91: 135,925 (5.4%) + Bin 92: 124,969 (5.0%) + Bin 97: 123,105 (4.9%) + Bin 96: 119,823 (4.8%) + +================================================== +NOISE (no decade matches) (n=12,156,430) +================================================== + +Output count: {1: '677,933 (5.6%)', 2: '7,852,056 (64.6%)', 3: '344,621 (2.8%)', 4: '177,789 (1.5%)', 5: '3,104,031 (25.5%)'} +Input count: {1: '8,910,621 (73.3%)', 2: '1,243,821 (10.2%)', 3: '552,345 (4.5%)', 4: '311,077 (2.6%)', 5: '1,138,566 (9.4%)'} +Output type: {'p2ms': '59 (0.0%)', 'p2pk33': '7,078 (0.1%)', 'p2pk65': '1 (0.0%)', 'p2pkh': '9,650,141 (79.4%)', 'p2sh': '2,499,151 (20.6%)'} +Is round BTC: {False: '11,427,352 (94.0%)', True: '729,078 (6.0%)'} +Both outputs round: {False: '12,141,273 (99.9%)', True: '15,157 (0.1%)'} +Same-day spend: {False: '2,707,074 (22.3%)', True: '9,449,356 (77.7%)'} +Has OP_RETURN: {False: '11,991,742 (98.6%)', True: '164,688 (1.4%)'} +Witness size: {'0': '12,156,430 (100.0%)'} +Value range (sats): {'100M+': '1,810,755 (14.9%)', '100k-1M': '2,325,925 (19.1%)', '10M-100M': '2,533,129 (20.8%)', '10k-100k': '1,367,800 (11.3%)', '1M-10M': '3,909,099 (32.2%)', '<10k': '209,722 (1.7%)'} +Decade (10^N sats): {3: '209,722 (1.7%)', 4: '1,367,800 (11.3%)', 5: '2,325,925 (19.1%)', 6: '3,909,099 (32.2%)', 7: '2,533,129 (20.8%)', 8: '1,219,158 (10.0%)', 9: '506,659 (4.2%)', 10: '74,936 (0.6%)', 11: '9,937 (0.1%)', 12: '65 (0.0%)'} +Implied USD value: {'$1-$10': '2,303,129 (18.9%)', '$10-$100': '3,908,145 (32.1%)', '$100-$1k': '2,548,135 (21.0%)', '$10k+': '601,191 (4.9%)', '$1k-$10k': '1,230,865 (10.1%)', '<$1': '1,564,965 (12.9%)'} +Output index (2-out only): {0: '3,935,015 (32.4%)', 1: '3,917,041 (32.2%)'} +Is smaller output (2-out): {False: '4,041,439 (33.2%)', True: '3,810,617 (31.3%)'} +Round pattern (2-out): {'both_round': '15,157 (0.1%)', 'neither_round': '6,762,951 (55.6%)', 'only_other_round': '607,034 (5.0%)', 'only_this_round': '466,914 (3.8%)'} +Value ratio (2-out): {'10-30%': '934,487 (7.7%)', '30-50%': '643,595 (5.3%)', '50-70%': '694,074 (5.7%)', '70-90%': '935,018 (7.7%)', '<10%': '2,232,535 (18.4%)', '>90%': '2,412,347 (19.8%)'} +Error from actual price: {'15-25%': '1,945,440 (16.0%)', '25-50%': '5,096,496 (41.9%)', '50%+': '5,114,494 (42.1%)'} +Tx total value: {'0.01-0.1 BTC': '2,967,326 (24.4%)', '0.1-1 BTC': '3,411,755 (28.1%)', '1-10 BTC': '2,816,901 (23.2%)', '10+ BTC': '2,080,170 (17.1%)', '<0.01 BTC': '880,278 (7.2%)'} +Round USD (10% tol): {False: '8,581,809 (70.6%)', True: '3,574,621 (29.4%)'} +Round USD (5% tol): {False: '9,721,045 (80.0%)', True: '2,435,385 (20.0%)'} +Round USD (2% tol): {False: '10,455,495 (86.0%)', True: '1,700,935 (14.0%)'} +Round USD (1% tol): {False: '10,708,537 (88.1%)', True: '1,447,893 (11.9%)'} +Tx pattern: {'1-to-1': '521,821 (4.3%)', '1-to-2': '6,271,701 (51.6%)', '1-to-many': '2,117,099 (17.4%)', '2-to-1': '57,165 (0.5%)', '2-to-2': '810,571 (6.7%)', '2-to-many': '376,085 (3.1%)', 'many-to-1': '98,947 (0.8%)', 'many-to-2': '769,784 (6.3%)', 'many-to-many': '1,133,257 (9.3%)'} +Value similarity (2-out): {'different': '1,289,142 (10.6%)', 'moderate': '738,027 (6.1%)', 'nearly_equal': '110,081 (0.9%)', 'similar': '257,746 (2.1%)', 'very_different': '5,371,761 (44.2%)'} +Micro-round sats: {False: '10,799,074 (88.8%)', True: '1,357,356 (11.2%)'} +Phase USD (1% tol): {False: '8,242,492 (67.8%)', True: '3,913,938 (32.2%)'} +Phase USD (2% tol): {False: '6,013,566 (49.5%)', True: '6,142,864 (50.5%)'} +Phase USD (5% tol): {False: '1,461,553 (12.0%)', True: '10,694,877 (88.0%)'} +Phase USD (10% tol): {True: '12,156,430 (100.0%)'} + +Top 10 bins: + Bin 30: 529,684 (4.4%) + Bin 17: 312,314 (2.6%) + Bin 0: 293,129 (2.4%) + Bin 69: 280,118 (2.3%) + Bin 20: 247,406 (2.0%) + Bin 77: 244,918 (2.0%) + Bin 39: 241,386 (2.0%) + Bin 29: 225,247 (1.9%) + Bin 23: 220,565 (1.8%) + Bin 27: 210,625 (1.7%) + +================================================== +KEY DIFFERENCES (accurate vs noise): +================================================== + +Output count: + 2: 66.9% vs 64.6% (diff: +2.3%) + 5: 23.5% vs 25.5% (diff: -2.0%) + +Input count: + 1: 70.7% vs 73.3% (diff: -2.6%) + +Output type: + +Is round BTC: + False: 82.9% vs 94.0% (diff: -11.1%) + True: 17.1% vs 6.0% (diff: +11.1%) + +Both round: + +Same-day spend: + False: 25.0% vs 22.3% (diff: +2.7%) + True: 75.0% vs 77.7% (diff: -2.7%) + +OP_RETURN: + +Witness size: + +Value range (sats): + 100M+: 9.8% vs 14.9% (diff: -5.1%) + 100k-1M: 25.9% vs 19.1% (diff: +6.8%) + 10M-100M: 16.2% vs 20.8% (diff: -4.6%) + +Decade (10^N sats): + 5: 25.9% vs 19.1% (diff: +6.8%) + 7: 16.2% vs 20.8% (diff: -4.6%) + 8: 7.1% vs 10.0% (diff: -2.9%) + +Implied USD: + $1-$10: 23.5% vs 18.9% (diff: +4.5%) + +Output index (2-out): + 0: 34.6% vs 32.4% (diff: +2.3%) + +Is smaller (2-out): + False: 30.9% vs 33.2% (diff: -2.4%) + True: 36.0% vs 31.3% (diff: +4.6%) + +Round pattern (2-out): + neither_round: 51.5% vs 55.6% (diff: -4.1%) + only_this_round: 10.7% vs 3.8% (diff: +6.8%) + +Value ratio (2-out): + <10%: 20.9% vs 18.4% (diff: +2.6%) + >90%: 17.5% vs 19.8% (diff: -2.3%) + +Error from price: + 15-25%: 0.0% vs 16.0% (diff: -16.0%) + 25-50%: 0.0% vs 41.9% (diff: -41.9%) + 50%+: 0.0% vs 42.1% (diff: -42.1%) + <5%: 100.0% vs 0.0% (diff: +100.0%) + +Tx total value: + 10+ BTC: 15.0% vs 17.1% (diff: -2.1%) + +Round USD (10%): + False: 4.5% vs 70.6% (diff: -66.1%) + True: 95.5% vs 29.4% (diff: +66.1%) + +Round USD (5%): + False: 6.6% vs 80.0% (diff: -73.4%) + True: 93.4% vs 20.0% (diff: +73.4%) + +Round USD (2%): + False: 29.9% vs 86.0% (diff: -56.1%) + True: 70.1% vs 14.0% (diff: +56.1%) + +Round USD (1%): + False: 39.6% vs 88.1% (diff: -48.5%) + True: 60.4% vs 11.9% (diff: +48.5%) + +Phase USD (1%): + False: 37.5% vs 67.8% (diff: -30.3%) + True: 62.5% vs 32.2% (diff: +30.3%) + +Phase USD (2%): + False: 10.3% vs 49.5% (diff: -39.2%) + True: 89.7% vs 50.5% (diff: +39.2%) + +Phase USD (5%): + False: 0.4% vs 12.0% (diff: -11.6%) + True: 99.6% vs 88.0% (diff: +11.6%) + +Phase USD (10%): + +Tx pattern: + many-to-2: 8.6% vs 6.3% (diff: +2.3%) + +Value similarity (2-out): + +Micro-round sats: + False: 81.8% vs 88.8% (diff: -7.0%) + True: 18.2% vs 11.2% (diff: +7.0%) + +================================================== +EXCLUSION CANDIDATES (overrepresented in noise): +================================================== +Characteristics where noise% > accurate% suggest exclusion filters: + + EXCLUDE Round USD 5%=False: noise 80.0% vs accurate 6.6% (+73.4%) + EXCLUDE Round USD 10%=False: noise 70.6% vs accurate 4.5% (+66.1%) + EXCLUDE Round USD 2%=False: noise 86.0% vs accurate 29.9% (+56.1%) + EXCLUDE Round USD 1%=False: noise 88.1% vs accurate 39.6% (+48.5%) + EXCLUDE Phase USD 2%=False: noise 49.5% vs accurate 10.3% (+39.2%) + EXCLUDE Phase USD 1%=False: noise 67.8% vs accurate 37.5% (+30.3%) + EXCLUDE Phase USD 5%=False: noise 12.0% vs accurate 0.4% (+11.6%) + EXCLUDE Is round BTC=False: noise 94.0% vs accurate 82.9% (+11.1%) + EXCLUDE Micro-round sats=False: noise 88.8% vs accurate 81.8% (+7.0%) + EXCLUDE Value range=100M+: noise 14.9% vs accurate 9.8% (+5.1%) + EXCLUDE Value range=10M-100M: noise 20.8% vs accurate 16.2% (+4.6%) + EXCLUDE Decade=7: noise 20.8% vs accurate 16.2% (+4.6%) + +================================================== +INCLUSION SIGNALS (overrepresented in accurate): +================================================== +Characteristics where accurate% > noise% are good signals: + + KEEP Round USD 5%=True: accurate 93.4% vs noise 20.0% (+73.4%) + KEEP Round USD 10%=True: accurate 95.5% vs noise 29.4% (+66.1%) + KEEP Round USD 2%=True: accurate 70.1% vs noise 14.0% (+56.1%) + KEEP Round USD 1%=True: accurate 60.4% vs noise 11.9% (+48.5%) + KEEP Phase USD 2%=True: accurate 89.7% vs noise 50.5% (+39.2%) + KEEP Phase USD 1%=True: accurate 62.5% vs noise 32.2% (+30.3%) + KEEP Phase USD 5%=True: accurate 99.6% vs noise 88.0% (+11.6%) + KEEP Is round BTC=True: accurate 17.1% vs noise 6.0% (+11.1%) + KEEP Value range=100k-1M: accurate 25.9% vs noise 19.1% (+6.8%) + KEEP Decade=5: accurate 25.9% vs noise 19.1% (+6.8%) + KEEP Is smaller (2-out)=True: accurate 36.0% vs noise 31.3% (+4.6%) + KEEP Implied USD=$1-$10: accurate 23.5% vs noise 18.9% (+4.5%) + + +############################################################ +# 2017-04 +############################################################ +Date range: 2017-04-01 to 2017-04-30 (dateindex 3005-3035) + +--- SUMMARY --- +Total outputs analyzed: 21,321,307 + Accurate (≤15% error): 1,580,938 (7.4%) + Close (15-30% error): 2,158,401 (10.1%) + Wrong decade: 5,183,458 (24.3%) + Noise: 12,398,510 (58.2%) + +================================================== +ACCURATE (within 15% of actual price) (n=1,580,938) +================================================== + +Output count: {1: '66,023 (4.2%)', 2: '995,964 (63.0%)', 3: '38,055 (2.4%)', 4: '20,649 (1.3%)', 5: '460,247 (29.1%)'} +Input count: {1: '1,200,470 (75.9%)', 2: '144,844 (9.2%)', 3: '66,970 (4.2%)', 4: '40,418 (2.6%)', 5: '128,236 (8.1%)'} +Output type: {'p2ms': '4 (0.0%)', 'p2pk33': '90 (0.0%)', 'p2pkh': '1,237,305 (78.3%)', 'p2sh': '343,539 (21.7%)'} +Is round BTC: {False: '1,580,938 (100.0%)'} +Both outputs round: {False: '1,580,938 (100.0%)'} +Same-day spend: {False: '348,093 (22.0%)', True: '1,232,845 (78.0%)'} +Has OP_RETURN: {False: '1,572,537 (99.5%)', True: '8,401 (0.5%)'} +Witness size: {'0': '1,580,938 (100.0%)'} +Value range (sats): {'100M+': '92,093 (5.8%)', '100k-1M': '479,607 (30.3%)', '10M-100M': '177,673 (11.2%)', '10k-100k': '256,828 (16.2%)', '1M-10M': '481,595 (30.5%)', '<10k': '93,142 (5.9%)'} +Decade (10^N sats): {3: '93,142 (5.9%)', 4: '256,828 (16.2%)', 5: '479,607 (30.3%)', 6: '481,595 (30.5%)', 7: '177,673 (11.2%)', 8: '68,001 (4.3%)', 9: '22,775 (1.4%)', 10: '1,310 (0.1%)', 11: '7 (0.0%)'} +Implied USD value: {'$1-$10': '383,108 (24.2%)', '$10-$100': '462,034 (29.2%)', '$100-$1k': '354,735 (22.4%)', '$10k+': '58,533 (3.7%)', '$1k-$10k': '124,893 (7.9%)', '<$1': '197,635 (12.5%)'} +Output index (2-out only): {0: '512,681 (32.4%)', 1: '483,283 (30.6%)'} +Is smaller output (2-out): {False: '466,444 (29.5%)', True: '529,520 (33.5%)'} +Round pattern (2-out): {'neither_round': '939,616 (59.4%)', 'only_other_round': '56,348 (3.6%)'} +Value ratio (2-out): {'10-30%': '139,512 (8.8%)', '30-50%': '97,676 (6.2%)', '50-70%': '68,777 (4.4%)', '70-90%': '126,176 (8.0%)', '<10%': '292,332 (18.5%)', '>90%': '271,491 (17.2%)'} +Error from actual price: {'<5%': '1,580,938 (100.0%)'} +Tx total value: {'0.01-0.1 BTC': '410,150 (25.9%)', '0.1-1 BTC': '453,443 (28.7%)', '1-10 BTC': '319,287 (20.2%)', '10+ BTC': '251,399 (15.9%)', '<0.01 BTC': '146,659 (9.3%)'} +Round USD (10% tol): {False: '94,459 (6.0%)', True: '1,486,479 (94.0%)'} +Round USD (5% tol): {False: '152,454 (9.6%)', True: '1,428,484 (90.4%)'} +Round USD (2% tol): {False: '658,768 (41.7%)', True: '922,170 (58.3%)'} +Round USD (1% tol): {False: '832,164 (52.6%)', True: '748,774 (47.4%)'} +Tx pattern: {'1-to-1': '51,792 (3.3%)', '1-to-2': '807,880 (51.1%)', '1-to-many': '340,798 (21.6%)', '2-to-1': '5,690 (0.4%)', '2-to-2': '92,047 (5.8%)', '2-to-many': '47,107 (3.0%)', 'many-to-1': '8,541 (0.5%)', 'many-to-2': '96,037 (6.1%)', 'many-to-many': '131,046 (8.3%)'} +Value similarity (2-out): {'different': '166,664 (10.5%)', 'moderate': '85,285 (5.4%)', 'nearly_equal': '16,527 (1.0%)', 'similar': '37,257 (2.4%)', 'very_different': '686,782 (43.4%)'} +Micro-round sats: {False: '1,395,040 (88.2%)', True: '185,898 (11.8%)'} +Phase USD (1% tol): {False: '685,462 (43.4%)', True: '895,476 (56.6%)'} +Phase USD (2% tol): {False: '178,637 (11.3%)', True: '1,402,301 (88.7%)'} +Phase USD (5% tol): {True: '1,580,938 (100.0%)'} +Phase USD (10% tol): {True: '1,580,938 (100.0%)'} + +Top 10 bins: + Bin 90: 238,730 (15.1%) + Bin 92: 201,764 (12.8%) + Bin 91: 199,048 (12.6%) + Bin 95: 185,940 (11.8%) + Bin 94: 174,875 (11.1%) + Bin 93: 144,802 (9.2%) + Bin 89: 112,113 (7.1%) + Bin 88: 81,147 (5.1%) + Bin 87: 61,197 (3.9%) + Bin 96: 54,629 (3.5%) + +================================================== +NOISE (no decade matches) (n=12,398,510) +================================================== + +Output count: {1: '642,588 (5.2%)', 2: '7,949,539 (64.1%)', 3: '376,464 (3.0%)', 4: '182,208 (1.5%)', 5: '3,247,711 (26.2%)'} +Input count: {1: '9,001,903 (72.6%)', 2: '1,335,975 (10.8%)', 3: '591,270 (4.8%)', 4: '323,897 (2.6%)', 5: '1,145,465 (9.2%)'} +Output type: {'opreturn': '15 (0.0%)', 'p2ms': '52 (0.0%)', 'p2pk33': '14,359 (0.1%)', 'p2pk65': '3 (0.0%)', 'p2pkh': '10,045,328 (81.0%)', 'p2sh': '2,338,753 (18.9%)'} +Is round BTC: {False: '11,336,928 (91.4%)', True: '1,061,582 (8.6%)'} +Both outputs round: {False: '12,373,309 (99.8%)', True: '25,201 (0.2%)'} +Same-day spend: {False: '2,857,111 (23.0%)', True: '9,541,399 (77.0%)'} +Has OP_RETURN: {False: '12,241,361 (98.7%)', True: '157,149 (1.3%)'} +Witness size: {'0': '12,398,510 (100.0%)'} +Value range (sats): {'100M+': '1,735,641 (14.0%)', '100k-1M': '2,354,720 (19.0%)', '10M-100M': '2,637,281 (21.3%)', '10k-100k': '1,198,658 (9.7%)', '1M-10M': '4,299,418 (34.7%)', '<10k': '172,792 (1.4%)'} +Decade (10^N sats): {3: '172,792 (1.4%)', 4: '1,198,658 (9.7%)', 5: '2,354,720 (19.0%)', 6: '4,299,418 (34.7%)', 7: '2,637,281 (21.3%)', 8: '1,192,692 (9.6%)', 9: '458,551 (3.7%)', 10: '77,887 (0.6%)', 11: '6,505 (0.1%)', 12: '6 (0.0%)'} +Implied USD value: {'$1-$10': '2,272,450 (18.3%)', '$10-$100': '4,280,970 (34.5%)', '$100-$1k': '2,709,927 (21.9%)', '$10k+': '559,784 (4.5%)', '$1k-$10k': '1,230,061 (9.9%)', '<$1': '1,345,318 (10.9%)'} +Output index (2-out only): {0: '3,993,240 (32.2%)', 1: '3,956,299 (31.9%)'} +Is smaller output (2-out): {False: '4,067,922 (32.8%)', True: '3,881,617 (31.3%)'} +Round pattern (2-out): {'both_round': '25,201 (0.2%)', 'neither_round': '6,652,198 (53.7%)', 'only_other_round': '584,132 (4.7%)', 'only_this_round': '688,008 (5.5%)'} +Value ratio (2-out): {'10-30%': '985,456 (7.9%)', '30-50%': '700,117 (5.6%)', '50-70%': '767,719 (6.2%)', '70-90%': '980,903 (7.9%)', '<10%': '2,196,044 (17.7%)', '>90%': '2,319,300 (18.7%)'} +Error from actual price: {'15-25%': '2,270,572 (18.3%)', '25-50%': '4,805,613 (38.8%)', '50%+': '5,322,325 (42.9%)'} +Tx total value: {'0.01-0.1 BTC': '3,152,823 (25.4%)', '0.1-1 BTC': '3,611,687 (29.1%)', '1-10 BTC': '2,707,664 (21.8%)', '10+ BTC': '2,183,853 (17.6%)', '<0.01 BTC': '742,483 (6.0%)'} +Round USD (10% tol): {False: '9,357,641 (75.5%)', True: '3,040,869 (24.5%)'} +Round USD (5% tol): {False: '10,606,873 (85.5%)', True: '1,791,637 (14.5%)'} +Round USD (2% tol): {False: '11,357,536 (91.6%)', True: '1,040,974 (8.4%)'} +Round USD (1% tol): {False: '11,611,504 (93.7%)', True: '787,006 (6.3%)'} +Tx pattern: {'1-to-1': '495,729 (4.0%)', '1-to-2': '6,170,408 (49.8%)', '1-to-many': '2,335,766 (18.8%)', '2-to-1': '53,738 (0.4%)', '2-to-2': '912,414 (7.4%)', '2-to-many': '369,823 (3.0%)', 'many-to-1': '93,121 (0.8%)', 'many-to-2': '866,717 (7.0%)', 'many-to-many': '1,100,794 (8.9%)'} +Value similarity (2-out): {'different': '1,366,124 (11.0%)', 'moderate': '782,353 (6.3%)', 'nearly_equal': '135,465 (1.1%)', 'similar': '291,998 (2.4%)', 'very_different': '5,308,984 (42.8%)'} +Micro-round sats: {False: '10,718,510 (86.4%)', True: '1,680,000 (13.6%)'} +Phase USD (1% tol): {False: '8,199,646 (66.1%)', True: '4,198,864 (33.9%)'} +Phase USD (2% tol): {False: '6,190,719 (49.9%)', True: '6,207,791 (50.1%)'} +Phase USD (5% tol): {False: '1,656,194 (13.4%)', True: '10,742,316 (86.6%)'} +Phase USD (10% tol): {True: '12,398,510 (100.0%)'} + +Top 10 bins: + Bin 0: 718,309 (5.8%) + Bin 69: 516,777 (4.2%) + Bin 30: 468,228 (3.8%) + Bin 17: 275,163 (2.2%) + Bin 77: 239,647 (1.9%) + Bin 23: 236,601 (1.9%) + Bin 25: 226,578 (1.8%) + Bin 7: 223,215 (1.8%) + Bin 20: 222,686 (1.8%) + Bin 39: 210,496 (1.7%) + +================================================== +KEY DIFFERENCES (accurate vs noise): +================================================== + +Output count: + 5: 29.1% vs 26.2% (diff: +2.9%) + +Input count: + 1: 75.9% vs 72.6% (diff: +3.3%) + +Output type: + p2pkh: 78.3% vs 81.0% (diff: -2.8%) + p2sh: 21.7% vs 18.9% (diff: +2.9%) + +Is round BTC: + False: 100.0% vs 91.4% (diff: +8.6%) + True: 0.0% vs 8.6% (diff: -8.6%) + +Both round: + +Same-day spend: + +OP_RETURN: + +Witness size: + +Value range (sats): + 100M+: 5.8% vs 14.0% (diff: -8.2%) + 100k-1M: 30.3% vs 19.0% (diff: +11.3%) + 10M-100M: 11.2% vs 21.3% (diff: -10.0%) + 10k-100k: 16.2% vs 9.7% (diff: +6.6%) + 1M-10M: 30.5% vs 34.7% (diff: -4.2%) + <10k: 5.9% vs 1.4% (diff: +4.5%) + +Decade (10^N sats): + 3: 5.9% vs 1.4% (diff: +4.5%) + 4: 16.2% vs 9.7% (diff: +6.6%) + 5: 30.3% vs 19.0% (diff: +11.3%) + 6: 30.5% vs 34.7% (diff: -4.2%) + 7: 11.2% vs 21.3% (diff: -10.0%) + 8: 4.3% vs 9.6% (diff: -5.3%) + 9: 1.4% vs 3.7% (diff: -2.3%) + +Implied USD: + $1-$10: 24.2% vs 18.3% (diff: +5.9%) + $10-$100: 29.2% vs 34.5% (diff: -5.3%) + $1k-$10k: 7.9% vs 9.9% (diff: -2.0%) + +Output index (2-out): + +Is smaller (2-out): + False: 29.5% vs 32.8% (diff: -3.3%) + True: 33.5% vs 31.3% (diff: +2.2%) + +Round pattern (2-out): + neither_round: 59.4% vs 53.7% (diff: +5.8%) + only_this_round: 0.0% vs 5.5% (diff: -5.5%) + +Value ratio (2-out): + +Error from price: + 15-25%: 0.0% vs 18.3% (diff: -18.3%) + 25-50%: 0.0% vs 38.8% (diff: -38.8%) + 50%+: 0.0% vs 42.9% (diff: -42.9%) + <5%: 100.0% vs 0.0% (diff: +100.0%) + +Tx total value: + <0.01 BTC: 9.3% vs 6.0% (diff: +3.3%) + +Round USD (10%): + False: 6.0% vs 75.5% (diff: -69.5%) + True: 94.0% vs 24.5% (diff: +69.5%) + +Round USD (5%): + False: 9.6% vs 85.5% (diff: -75.9%) + True: 90.4% vs 14.5% (diff: +75.9%) + +Round USD (2%): + False: 41.7% vs 91.6% (diff: -49.9%) + True: 58.3% vs 8.4% (diff: +49.9%) + +Round USD (1%): + False: 52.6% vs 93.7% (diff: -41.0%) + True: 47.4% vs 6.3% (diff: +41.0%) + +Phase USD (1%): + False: 43.4% vs 66.1% (diff: -22.8%) + True: 56.6% vs 33.9% (diff: +22.8%) + +Phase USD (2%): + False: 11.3% vs 49.9% (diff: -38.6%) + True: 88.7% vs 50.1% (diff: +38.6%) + +Phase USD (5%): + False: 0.0% vs 13.4% (diff: -13.4%) + True: 100.0% vs 86.6% (diff: +13.4%) + +Phase USD (10%): + +Tx pattern: + 1-to-many: 21.6% vs 18.8% (diff: +2.7%) + +Value similarity (2-out): + +Micro-round sats: + +================================================== +EXCLUSION CANDIDATES (overrepresented in noise): +================================================== +Characteristics where noise% > accurate% suggest exclusion filters: + + EXCLUDE Round USD 5%=False: noise 85.5% vs accurate 9.6% (+75.9%) + EXCLUDE Round USD 10%=False: noise 75.5% vs accurate 6.0% (+69.5%) + EXCLUDE Round USD 2%=False: noise 91.6% vs accurate 41.7% (+49.9%) + EXCLUDE Round USD 1%=False: noise 93.7% vs accurate 52.6% (+41.0%) + EXCLUDE Phase USD 2%=False: noise 49.9% vs accurate 11.3% (+38.6%) + EXCLUDE Phase USD 1%=False: noise 66.1% vs accurate 43.4% (+22.8%) + EXCLUDE Phase USD 5%=False: noise 13.4% vs accurate 0.0% (+13.4%) + EXCLUDE Value range=10M-100M: noise 21.3% vs accurate 11.2% (+10.0%) + EXCLUDE Decade=7: noise 21.3% vs accurate 11.2% (+10.0%) + EXCLUDE Is round BTC=True: noise 8.6% vs accurate 0.0% (+8.6%) + EXCLUDE Value range=100M+: noise 14.0% vs accurate 5.8% (+8.2%) + EXCLUDE Decade=8: noise 9.6% vs accurate 4.3% (+5.3%) + EXCLUDE Implied USD=$10-$100: noise 34.5% vs accurate 29.2% (+5.3%) + EXCLUDE Value range=1M-10M: noise 34.7% vs accurate 30.5% (+4.2%) + EXCLUDE Decade=6: noise 34.7% vs accurate 30.5% (+4.2%) + +================================================== +INCLUSION SIGNALS (overrepresented in accurate): +================================================== +Characteristics where accurate% > noise% are good signals: + + KEEP Round USD 5%=True: accurate 90.4% vs noise 14.5% (+75.9%) + KEEP Round USD 10%=True: accurate 94.0% vs noise 24.5% (+69.5%) + KEEP Round USD 2%=True: accurate 58.3% vs noise 8.4% (+49.9%) + KEEP Round USD 1%=True: accurate 47.4% vs noise 6.3% (+41.0%) + KEEP Phase USD 2%=True: accurate 88.7% vs noise 50.1% (+38.6%) + KEEP Phase USD 1%=True: accurate 56.6% vs noise 33.9% (+22.8%) + KEEP Phase USD 5%=True: accurate 100.0% vs noise 86.6% (+13.4%) + KEEP Value range=100k-1M: accurate 30.3% vs noise 19.0% (+11.3%) + KEEP Decade=5: accurate 30.3% vs noise 19.0% (+11.3%) + KEEP Is round BTC=False: accurate 100.0% vs noise 91.4% (+8.6%) + KEEP Value range=10k-100k: accurate 16.2% vs noise 9.7% (+6.6%) + KEEP Decade=4: accurate 16.2% vs noise 9.7% (+6.6%) + KEEP Implied USD=$1-$10: accurate 24.2% vs noise 18.3% (+5.9%) + KEEP Value range=<10k: accurate 5.9% vs noise 1.4% (+4.5%) + KEEP Decade=3: accurate 5.9% vs noise 1.4% (+4.5%) + + +############################################################ +# 2017-05 +############################################################ +Date range: 2017-05-01 to 2017-05-31 (dateindex 3035-3066) + +--- SUMMARY --- +Total outputs analyzed: 24,477,363 + Accurate (≤15% error): 2,233,661 (9.1%) + Close (15-30% error): 2,029,485 (8.3%) + Wrong decade: 5,611,231 (22.9%) + Noise: 14,602,986 (59.7%) + +================================================== +ACCURATE (within 15% of actual price) (n=2,233,661) +================================================== + +Output count: {1: '119,601 (5.4%)', 2: '1,510,834 (67.6%)', 3: '67,197 (3.0%)', 4: '35,097 (1.6%)', 5: '500,932 (22.4%)'} +Input count: {1: '1,689,363 (75.6%)', 2: '215,803 (9.7%)', 3: '98,531 (4.4%)', 4: '50,954 (2.3%)', 5: '179,010 (8.0%)'} +Output type: {'p2ms': '12 (0.0%)', 'p2pk33': '127 (0.0%)', 'p2pkh': '1,835,534 (82.2%)', 'p2sh': '397,988 (17.8%)'} +Is round BTC: {False: '2,112,393 (94.6%)', True: '121,268 (5.4%)'} +Both outputs round: {False: '2,231,991 (99.9%)', True: '1,670 (0.1%)'} +Same-day spend: {False: '582,649 (26.1%)', True: '1,651,012 (73.9%)'} +Has OP_RETURN: {False: '2,213,347 (99.1%)', True: '20,314 (0.9%)'} +Witness size: {'0': '2,233,661 (100.0%)'} +Value range (sats): {'100M+': '183,753 (8.2%)', '100k-1M': '512,511 (22.9%)', '10M-100M': '411,045 (18.4%)', '10k-100k': '279,817 (12.5%)', '1M-10M': '800,150 (35.8%)', '<10k': '46,385 (2.1%)'} +Decade (10^N sats): {3: '46,385 (2.1%)', 4: '279,817 (12.5%)', 5: '512,511 (22.9%)', 6: '800,150 (35.8%)', 7: '411,045 (18.4%)', 8: '129,007 (5.8%)', 9: '47,027 (2.1%)', 10: '7,655 (0.3%)', 11: '64 (0.0%)'} +Implied USD value: {'$1-$10': '394,605 (17.7%)', '$10-$100': '653,555 (29.3%)', '$100-$1k': '623,088 (27.9%)', '$10k+': '118,257 (5.3%)', '$1k-$10k': '277,612 (12.4%)', '<$1': '166,544 (7.5%)'} +Output index (2-out only): {0: '781,145 (35.0%)', 1: '729,689 (32.7%)'} +Is smaller output (2-out): {False: '748,248 (33.5%)', True: '762,586 (34.1%)'} +Round pattern (2-out): {'both_round': '1,670 (0.1%)', 'neither_round': '1,334,385 (59.7%)', 'only_other_round': '96,556 (4.3%)', 'only_this_round': '78,223 (3.5%)'} +Value ratio (2-out): {'10-30%': '178,534 (8.0%)', '30-50%': '143,484 (6.4%)', '50-70%': '130,663 (5.8%)', '70-90%': '212,128 (9.5%)', '<10%': '440,568 (19.7%)', '>90%': '405,457 (18.2%)'} +Error from actual price: {'<5%': '2,233,661 (100.0%)'} +Tx total value: {'0.01-0.1 BTC': '555,486 (24.9%)', '0.1-1 BTC': '660,253 (29.6%)', '1-10 BTC': '454,299 (20.3%)', '10+ BTC': '365,105 (16.3%)', '<0.01 BTC': '198,518 (8.9%)'} +Round USD (10% tol): {False: '54,104 (2.4%)', True: '2,179,557 (97.6%)'} +Round USD (5% tol): {False: '132,640 (5.9%)', True: '2,101,021 (94.1%)'} +Round USD (2% tol): {False: '682,209 (30.5%)', True: '1,551,452 (69.5%)'} +Round USD (1% tol): {False: '889,644 (39.8%)', True: '1,344,017 (60.2%)'} +Tx pattern: {'1-to-1': '95,260 (4.3%)', '1-to-2': '1,236,998 (55.4%)', '1-to-many': '357,105 (16.0%)', '2-to-1': '10,189 (0.5%)', '2-to-2': '139,286 (6.2%)', '2-to-many': '66,328 (3.0%)', 'many-to-1': '14,152 (0.6%)', 'many-to-2': '134,550 (6.0%)', 'many-to-many': '179,793 (8.0%)'} +Value similarity (2-out): {'different': '269,619 (12.1%)', 'moderate': '136,567 (6.1%)', 'nearly_equal': '28,433 (1.3%)', 'similar': '58,918 (2.6%)', 'very_different': '1,013,697 (45.4%)'} +Micro-round sats: {False: '1,917,375 (85.8%)', True: '316,286 (14.2%)'} +Phase USD (1% tol): {False: '1,460,771 (65.4%)', True: '772,890 (34.6%)'} +Phase USD (2% tol): {False: '963,495 (43.1%)', True: '1,270,166 (56.9%)'} +Phase USD (5% tol): {False: '6,796 (0.3%)', True: '2,226,865 (99.7%)'} +Phase USD (10% tol): {True: '2,233,661 (100.0%)'} + +Top 10 bins: + Bin 69: 229,748 (10.3%) + Bin 77: 125,928 (5.6%) + Bin 75: 108,797 (4.9%) + Bin 74: 96,624 (4.3%) + Bin 76: 87,655 (3.9%) + Bin 73: 86,488 (3.9%) + Bin 71: 86,378 (3.9%) + Bin 65: 86,032 (3.9%) + Bin 68: 85,390 (3.8%) + Bin 64: 83,874 (3.8%) + +================================================== +NOISE (no decade matches) (n=14,602,986) +================================================== + +Output count: {1: '780,812 (5.3%)', 2: '9,710,354 (66.5%)', 3: '386,335 (2.6%)', 4: '200,008 (1.4%)', 5: '3,525,477 (24.1%)'} +Input count: {1: '10,735,130 (73.5%)', 2: '1,524,102 (10.4%)', 3: '761,061 (5.2%)', 4: '389,846 (2.7%)', 5: '1,192,847 (8.2%)'} +Output type: {'opreturn': '25 (0.0%)', 'p2ms': '77 (0.0%)', 'p2pk33': '12,079 (0.1%)', 'p2pk65': '5 (0.0%)', 'p2pkh': '11,979,657 (82.0%)', 'p2sh': '2,611,143 (17.9%)'} +Is round BTC: {False: '13,491,464 (92.4%)', True: '1,111,522 (7.6%)'} +Both outputs round: {False: '14,576,491 (99.8%)', True: '26,495 (0.2%)'} +Same-day spend: {False: '3,582,444 (24.5%)', True: '11,020,542 (75.5%)'} +Has OP_RETURN: {False: '14,445,159 (98.9%)', True: '157,827 (1.1%)'} +Witness size: {'0': '14,602,986 (100.0%)'} +Value range (sats): {'100M+': '2,015,370 (13.8%)', '100k-1M': '2,755,962 (18.9%)', '10M-100M': '3,242,853 (22.2%)', '10k-100k': '1,061,221 (7.3%)', '1M-10M': '5,276,710 (36.1%)', '<10k': '250,870 (1.7%)'} +Decade (10^N sats): {3: '250,870 (1.7%)', 4: '1,061,221 (7.3%)', 5: '2,755,962 (18.9%)', 6: '5,276,710 (36.1%)', 7: '3,242,853 (22.2%)', 8: '1,415,344 (9.7%)', 9: '476,717 (3.3%)', 10: '117,069 (0.8%)', 11: '6,221 (0.0%)', 12: '19 (0.0%)'} +Implied USD value: {'$1-$10': '1,986,452 (13.6%)', '$10-$100': '4,843,354 (33.2%)', '$100-$1k': '4,109,402 (28.1%)', '$10k+': '828,777 (5.7%)', '$1k-$10k': '1,953,330 (13.4%)', '<$1': '881,671 (6.0%)'} +Output index (2-out only): {0: '4,840,553 (33.1%)', 1: '4,869,801 (33.3%)'} +Is smaller output (2-out): {False: '4,878,367 (33.4%)', True: '4,831,987 (33.1%)'} +Round pattern (2-out): {'both_round': '26,495 (0.2%)', 'neither_round': '8,247,910 (56.5%)', 'only_other_round': '677,119 (4.6%)', 'only_this_round': '758,830 (5.2%)'} +Value ratio (2-out): {'10-30%': '1,256,435 (8.6%)', '30-50%': '904,580 (6.2%)', '50-70%': '871,326 (6.0%)', '70-90%': '1,206,143 (8.3%)', '<10%': '2,670,972 (18.3%)', '>90%': '2,800,898 (19.2%)'} +Error from actual price: {'15-25%': '1,936,391 (13.3%)', '25-50%': '6,907,793 (47.3%)', '50%+': '5,758,802 (39.4%)'} +Tx total value: {'0.01-0.1 BTC': '3,343,598 (22.9%)', '0.1-1 BTC': '4,208,647 (28.8%)', '1-10 BTC': '3,080,227 (21.1%)', '10+ BTC': '3,093,673 (21.2%)', '<0.01 BTC': '876,841 (6.0%)'} +Round USD (10% tol): {False: '9,392,802 (64.3%)', True: '5,210,184 (35.7%)'} +Round USD (5% tol): {False: '11,045,100 (75.6%)', True: '3,557,886 (24.4%)'} +Round USD (2% tol): {False: '12,017,362 (82.3%)', True: '2,585,624 (17.7%)'} +Round USD (1% tol): {False: '12,378,570 (84.8%)', True: '2,224,416 (15.2%)'} +Tx pattern: {'1-to-1': '613,798 (4.2%)', '1-to-2': '7,521,610 (51.5%)', '1-to-many': '2,599,722 (17.8%)', '2-to-1': '67,447 (0.5%)', '2-to-2': '1,049,340 (7.2%)', '2-to-many': '407,315 (2.8%)', 'many-to-1': '99,567 (0.7%)', 'many-to-2': '1,139,404 (7.8%)', 'many-to-many': '1,104,783 (7.6%)'} +Value similarity (2-out): {'different': '1,690,400 (11.6%)', 'moderate': '925,966 (6.3%)', 'nearly_equal': '179,601 (1.2%)', 'similar': '383,270 (2.6%)', 'very_different': '6,468,404 (44.3%)'} +Micro-round sats: {False: '12,644,893 (86.6%)', True: '1,958,093 (13.4%)'} +Phase USD (1% tol): {False: '9,413,573 (64.5%)', True: '5,189,413 (35.5%)'} +Phase USD (2% tol): {False: '6,586,748 (45.1%)', True: '8,016,238 (54.9%)'} +Phase USD (5% tol): {False: '2,091,062 (14.3%)', True: '12,511,924 (85.7%)'} +Phase USD (10% tol): {True: '14,602,986 (100.0%)'} + +Top 10 bins: + Bin 0: 1,111,273 (7.6%) + Bin 97: 476,621 (3.3%) + Bin 99: 439,674 (3.0%) + Bin 94: 356,007 (2.4%) + Bin 64: 338,788 (2.3%) + Bin 7: 331,025 (2.3%) + Bin 95: 311,255 (2.1%) + Bin 4: 292,666 (2.0%) + Bin 2: 273,414 (1.9%) + Bin 1: 249,777 (1.7%) + +================================================== +KEY DIFFERENCES (accurate vs noise): +================================================== + +Output count: + +Input count: + 1: 75.6% vs 73.5% (diff: +2.1%) + +Output type: + +Is round BTC: + False: 94.6% vs 92.4% (diff: +2.2%) + True: 5.4% vs 7.6% (diff: -2.2%) + +Both round: + +Same-day spend: + +OP_RETURN: + +Witness size: + +Value range (sats): + 100M+: 8.2% vs 13.8% (diff: -5.6%) + 100k-1M: 22.9% vs 18.9% (diff: +4.1%) + 10M-100M: 18.4% vs 22.2% (diff: -3.8%) + 10k-100k: 12.5% vs 7.3% (diff: +5.3%) + +Decade (10^N sats): + 4: 12.5% vs 7.3% (diff: +5.3%) + 5: 22.9% vs 18.9% (diff: +4.1%) + 7: 18.4% vs 22.2% (diff: -3.8%) + 8: 5.8% vs 9.7% (diff: -3.9%) + +Implied USD: + $1-$10: 17.7% vs 13.6% (diff: +4.1%) + $10-$100: 29.3% vs 33.2% (diff: -3.9%) + +Output index (2-out): + +Is smaller (2-out): + +Round pattern (2-out): + neither_round: 59.7% vs 56.5% (diff: +3.3%) + +Value ratio (2-out): + +Error from price: + 15-25%: 0.0% vs 13.3% (diff: -13.3%) + 25-50%: 0.0% vs 47.3% (diff: -47.3%) + 50%+: 0.0% vs 39.4% (diff: -39.4%) + <5%: 100.0% vs 0.0% (diff: +100.0%) + +Tx total value: + 10+ BTC: 16.3% vs 21.2% (diff: -4.8%) + <0.01 BTC: 8.9% vs 6.0% (diff: +2.9%) + +Round USD (10%): + False: 2.4% vs 64.3% (diff: -61.9%) + True: 97.6% vs 35.7% (diff: +61.9%) + +Round USD (5%): + False: 5.9% vs 75.6% (diff: -69.7%) + True: 94.1% vs 24.4% (diff: +69.7%) + +Round USD (2%): + False: 30.5% vs 82.3% (diff: -51.8%) + True: 69.5% vs 17.7% (diff: +51.8%) + +Round USD (1%): + False: 39.8% vs 84.8% (diff: -44.9%) + True: 60.2% vs 15.2% (diff: +44.9%) + +Phase USD (1%): + +Phase USD (2%): + +Phase USD (5%): + False: 0.3% vs 14.3% (diff: -14.0%) + True: 99.7% vs 85.7% (diff: +14.0%) + +Phase USD (10%): + +Tx pattern: + 1-to-2: 55.4% vs 51.5% (diff: +3.9%) + +Value similarity (2-out): + +Micro-round sats: + +================================================== +EXCLUSION CANDIDATES (overrepresented in noise): +================================================== +Characteristics where noise% > accurate% suggest exclusion filters: + + EXCLUDE Round USD 5%=False: noise 75.6% vs accurate 5.9% (+69.7%) + EXCLUDE Round USD 10%=False: noise 64.3% vs accurate 2.4% (+61.9%) + EXCLUDE Round USD 2%=False: noise 82.3% vs accurate 30.5% (+51.8%) + EXCLUDE Round USD 1%=False: noise 84.8% vs accurate 39.8% (+44.9%) + EXCLUDE Phase USD 5%=False: noise 14.3% vs accurate 0.3% (+14.0%) + EXCLUDE Value range=100M+: noise 13.8% vs accurate 8.2% (+5.6%) + EXCLUDE Tx total value=10+ BTC: noise 21.2% vs accurate 16.3% (+4.8%) + EXCLUDE Decade=8: noise 9.7% vs accurate 5.8% (+3.9%) + EXCLUDE Implied USD=$10-$100: noise 33.2% vs accurate 29.3% (+3.9%) + EXCLUDE Value range=10M-100M: noise 22.2% vs accurate 18.4% (+3.8%) + EXCLUDE Decade=7: noise 22.2% vs accurate 18.4% (+3.8%) + +================================================== +INCLUSION SIGNALS (overrepresented in accurate): +================================================== +Characteristics where accurate% > noise% are good signals: + + KEEP Round USD 5%=True: accurate 94.1% vs noise 24.4% (+69.7%) + KEEP Round USD 10%=True: accurate 97.6% vs noise 35.7% (+61.9%) + KEEP Round USD 2%=True: accurate 69.5% vs noise 17.7% (+51.8%) + KEEP Round USD 1%=True: accurate 60.2% vs noise 15.2% (+44.9%) + KEEP Phase USD 5%=True: accurate 99.7% vs noise 85.7% (+14.0%) + KEEP Value range=10k-100k: accurate 12.5% vs noise 7.3% (+5.3%) + KEEP Decade=4: accurate 12.5% vs noise 7.3% (+5.3%) + KEEP Value range=100k-1M: accurate 22.9% vs noise 18.9% (+4.1%) + KEEP Decade=5: accurate 22.9% vs noise 18.9% (+4.1%) + KEEP Implied USD=$1-$10: accurate 17.7% vs noise 13.6% (+4.1%) + KEEP Tx pattern=1-to-2: accurate 55.4% vs noise 51.5% (+3.9%) + + +############################################################ +# 2017-06 +############################################################ +Date range: 2017-06-01 to 2017-06-30 (dateindex 3066-3096) + +--- SUMMARY --- +Total outputs analyzed: 20,549,135 + Accurate (≤15% error): 1,719,265 (8.4%) + Close (15-30% error): 1,646,294 (8.0%) + Wrong decade: 5,159,346 (25.1%) + Noise: 12,024,230 (58.5%) + +================================================== +ACCURATE (within 15% of actual price) (n=1,719,265) +================================================== + +Output count: {1: '94,725 (5.5%)', 2: '1,105,323 (64.3%)', 3: '55,643 (3.2%)', 4: '28,196 (1.6%)', 5: '435,378 (25.3%)'} +Input count: {1: '1,250,137 (72.7%)', 2: '179,544 (10.4%)', 3: '88,492 (5.1%)', 4: '43,692 (2.5%)', 5: '157,400 (9.2%)'} +Output type: {'p2ms': '221 (0.0%)', 'p2pk33': '1,831 (0.1%)', 'p2pkh': '1,437,615 (83.6%)', 'p2sh': '279,598 (16.3%)'} +Is round BTC: {False: '1,707,000 (99.3%)', True: '12,265 (0.7%)'} +Both outputs round: {False: '1,719,065 (100.0%)', True: '200 (0.0%)'} +Same-day spend: {False: '459,758 (26.7%)', True: '1,259,507 (73.3%)'} +Has OP_RETURN: {False: '1,704,170 (99.1%)', True: '15,095 (0.9%)'} +Witness size: {'0': '1,719,265 (100.0%)'} +Value range (sats): {'100M+': '177,351 (10.3%)', '100k-1M': '427,354 (24.9%)', '10M-100M': '316,946 (18.4%)', '10k-100k': '144,510 (8.4%)', '1M-10M': '628,272 (36.5%)', '<10k': '24,832 (1.4%)'} +Decade (10^N sats): {3: '24,832 (1.4%)', 4: '144,510 (8.4%)', 5: '427,354 (24.9%)', 6: '628,272 (36.5%)', 7: '316,946 (18.4%)', 8: '113,691 (6.6%)', 9: '45,030 (2.6%)', 10: '18,573 (1.1%)', 11: '57 (0.0%)'} +Implied USD value: {'$1-$10': '270,320 (15.7%)', '$10-$100': '524,482 (30.5%)', '$100-$1k': '483,638 (28.1%)', '$10k+': '127,060 (7.4%)', '$1k-$10k': '222,773 (13.0%)', '<$1': '90,992 (5.3%)'} +Output index (2-out only): {0: '567,747 (33.0%)', 1: '537,576 (31.3%)'} +Is smaller output (2-out): {False: '540,364 (31.4%)', True: '564,959 (32.9%)'} +Round pattern (2-out): {'both_round': '200 (0.0%)', 'neither_round': '1,033,014 (60.1%)', 'only_other_round': '65,587 (3.8%)', 'only_this_round': '6,522 (0.4%)'} +Value ratio (2-out): {'10-30%': '147,813 (8.6%)', '30-50%': '95,542 (5.6%)', '50-70%': '90,645 (5.3%)', '70-90%': '139,404 (8.1%)', '<10%': '321,604 (18.7%)', '>90%': '310,315 (18.0%)'} +Error from actual price: {'<5%': '1,719,265 (100.0%)'} +Tx total value: {'0.01-0.1 BTC': '411,409 (23.9%)', '0.1-1 BTC': '449,483 (26.1%)', '1-10 BTC': '345,092 (20.1%)', '10+ BTC': '338,451 (19.7%)', '<0.01 BTC': '174,830 (10.2%)'} +Round USD (10% tol): {False: '43,462 (2.5%)', True: '1,675,803 (97.5%)'} +Round USD (5% tol): {False: '87,027 (5.1%)', True: '1,632,238 (94.9%)'} +Round USD (2% tol): {False: '502,483 (29.2%)', True: '1,216,782 (70.8%)'} +Round USD (1% tol): {False: '657,176 (38.2%)', True: '1,062,089 (61.8%)'} +Tx pattern: {'1-to-1': '71,964 (4.2%)', '1-to-2': '894,862 (52.0%)', '1-to-many': '283,311 (16.5%)', '2-to-1': '8,866 (0.5%)', '2-to-2': '113,220 (6.6%)', '2-to-many': '57,458 (3.3%)', 'many-to-1': '13,895 (0.8%)', 'many-to-2': '97,241 (5.7%)', 'many-to-many': '178,448 (10.4%)'} +Value similarity (2-out): {'different': '214,160 (12.5%)', 'moderate': '98,062 (5.7%)', 'nearly_equal': '16,311 (0.9%)', 'similar': '36,683 (2.1%)', 'very_different': '738,344 (42.9%)'} +Micro-round sats: {False: '1,540,729 (89.6%)', True: '178,536 (10.4%)'} +Phase USD (1% tol): {False: '1,166,952 (67.9%)', True: '552,313 (32.1%)'} +Phase USD (2% tol): {False: '871,314 (50.7%)', True: '847,951 (49.3%)'} +Phase USD (5% tol): {False: '169,279 (9.8%)', True: '1,549,986 (90.2%)'} +Phase USD (10% tol): {True: '1,719,265 (100.0%)'} + +Top 10 bins: + Bin 60: 247,134 (14.4%) + Bin 59: 208,135 (12.1%) + Bin 57: 173,362 (10.1%) + Bin 58: 172,622 (10.0%) + Bin 56: 148,792 (8.7%) + Bin 61: 128,582 (7.5%) + Bin 55: 123,445 (7.2%) + Bin 54: 109,337 (6.4%) + Bin 62: 99,801 (5.8%) + Bin 53: 59,942 (3.5%) + +================================================== +NOISE (no decade matches) (n=12,024,230) +================================================== + +Output count: {1: '675,639 (5.6%)', 2: '7,499,880 (62.4%)', 3: '380,363 (3.2%)', 4: '224,753 (1.9%)', 5: '3,243,595 (27.0%)'} +Input count: {1: '8,291,917 (69.0%)', 2: '1,476,963 (12.3%)', 3: '635,740 (5.3%)', 4: '358,707 (3.0%)', 5: '1,260,903 (10.5%)'} +Output type: {'opreturn': '70 (0.0%)', 'p2ms': '15 (0.0%)', 'p2pk33': '16,973 (0.1%)', 'p2pkh': '10,037,857 (83.5%)', 'p2sh': '1,969,315 (16.4%)'} +Is round BTC: {False: '10,940,949 (91.0%)', True: '1,083,281 (9.0%)'} +Both outputs round: {False: '12,000,150 (99.8%)', True: '24,080 (0.2%)'} +Same-day spend: {False: '3,199,254 (26.6%)', True: '8,824,976 (73.4%)'} +Has OP_RETURN: {False: '11,860,360 (98.6%)', True: '163,870 (1.4%)'} +Witness size: {'0': '12,024,230 (100.0%)'} +Value range (sats): {'100M+': '1,439,621 (12.0%)', '100k-1M': '3,050,529 (25.4%)', '10M-100M': '2,398,472 (19.9%)', '10k-100k': '968,098 (8.1%)', '1M-10M': '3,804,693 (31.6%)', '<10k': '362,817 (3.0%)'} +Decade (10^N sats): {3: '362,817 (3.0%)', 4: '968,098 (8.1%)', 5: '3,050,529 (25.4%)', 6: '3,804,693 (31.6%)', 7: '2,398,472 (19.9%)', 8: '986,121 (8.2%)', 9: '364,791 (3.0%)', 10: '87,191 (0.7%)', 11: '1,483 (0.0%)', 12: '35 (0.0%)'} +Implied USD value: {'$1-$10': '1,679,776 (14.0%)', '$10-$100': '3,998,617 (33.3%)', '$100-$1k': '3,140,113 (26.1%)', '$10k+': '858,358 (7.1%)', '$1k-$10k': '1,633,666 (13.6%)', '<$1': '713,700 (5.9%)'} +Output index (2-out only): {0: '3,725,822 (31.0%)', 1: '3,774,058 (31.4%)'} +Is smaller output (2-out): {False: '3,734,729 (31.1%)', True: '3,765,151 (31.3%)'} +Round pattern (2-out): {'both_round': '24,080 (0.2%)', 'neither_round': '6,285,415 (52.3%)', 'only_other_round': '521,691 (4.3%)', 'only_this_round': '668,694 (5.6%)'} +Value ratio (2-out): {'10-30%': '918,890 (7.6%)', '30-50%': '688,627 (5.7%)', '50-70%': '655,561 (5.5%)', '70-90%': '938,857 (7.8%)', '<10%': '2,157,634 (17.9%)', '>90%': '2,140,311 (17.8%)'} +Error from actual price: {'15-25%': '1,779,998 (14.8%)', '25-50%': '4,663,861 (38.8%)', '50%+': '5,580,371 (46.4%)'} +Tx total value: {'0.01-0.1 BTC': '2,827,508 (23.5%)', '0.1-1 BTC': '3,111,049 (25.9%)', '1-10 BTC': '2,504,780 (20.8%)', '10+ BTC': '2,491,248 (20.7%)', '<0.01 BTC': '1,089,645 (9.1%)'} +Round USD (10% tol): {False: '7,507,838 (62.4%)', True: '4,516,392 (37.6%)'} +Round USD (5% tol): {False: '8,921,978 (74.2%)', True: '3,102,252 (25.8%)'} +Round USD (2% tol): {False: '9,869,624 (82.1%)', True: '2,154,606 (17.9%)'} +Round USD (1% tol): {False: '10,172,286 (84.6%)', True: '1,851,944 (15.4%)'} +Tx pattern: {'1-to-1': '501,451 (4.2%)', '1-to-2': '5,635,396 (46.9%)', '1-to-many': '2,155,070 (17.9%)', '2-to-1': '66,210 (0.6%)', '2-to-2': '939,143 (7.8%)', '2-to-many': '471,610 (3.9%)', 'many-to-1': '107,978 (0.9%)', 'many-to-2': '925,341 (7.7%)', 'many-to-many': '1,222,031 (10.2%)'} +Value similarity (2-out): {'different': '1,326,352 (11.0%)', 'moderate': '699,493 (5.8%)', 'nearly_equal': '123,012 (1.0%)', 'similar': '275,556 (2.3%)', 'very_different': '5,028,535 (41.8%)'} +Micro-round sats: {False: '10,305,567 (85.7%)', True: '1,718,663 (14.3%)'} +Phase USD (1% tol): {False: '6,939,624 (57.7%)', True: '5,084,606 (42.3%)'} +Phase USD (2% tol): {False: '4,195,983 (34.9%)', True: '7,828,247 (65.1%)'} +Phase USD (5% tol): {False: '650,682 (5.4%)', True: '11,373,548 (94.6%)'} +Phase USD (10% tol): {True: '12,024,230 (100.0%)'} + +Top 10 bins: + Bin 0: 862,751 (7.2%) + Bin 69: 416,271 (3.5%) + Bin 47: 332,570 (2.8%) + Bin 99: 312,585 (2.6%) + Bin 39: 262,859 (2.2%) + Bin 95: 251,719 (2.1%) + Bin 77: 244,725 (2.0%) + Bin 90: 243,914 (2.0%) + Bin 4: 240,655 (2.0%) + Bin 84: 230,488 (1.9%) + +================================================== +KEY DIFFERENCES (accurate vs noise): +================================================== + +Output count: + +Input count: + 1: 72.7% vs 69.0% (diff: +3.8%) + +Output type: + +Is round BTC: + False: 99.3% vs 91.0% (diff: +8.3%) + True: 0.7% vs 9.0% (diff: -8.3%) + +Both round: + +Same-day spend: + +OP_RETURN: + +Witness size: + +Value range (sats): + 1M-10M: 36.5% vs 31.6% (diff: +4.9%) + +Decade (10^N sats): + 6: 36.5% vs 31.6% (diff: +4.9%) + +Implied USD: + $10-$100: 30.5% vs 33.3% (diff: -2.7%) + $100-$1k: 28.1% vs 26.1% (diff: +2.0%) + +Output index (2-out): + 0: 33.0% vs 31.0% (diff: +2.0%) + +Is smaller (2-out): + +Round pattern (2-out): + neither_round: 60.1% vs 52.3% (diff: +7.8%) + only_this_round: 0.4% vs 5.6% (diff: -5.2%) + +Value ratio (2-out): + +Error from price: + 15-25%: 0.0% vs 14.8% (diff: -14.8%) + 25-50%: 0.0% vs 38.8% (diff: -38.8%) + 50%+: 0.0% vs 46.4% (diff: -46.4%) + <5%: 100.0% vs 0.0% (diff: +100.0%) + +Tx total value: + +Round USD (10%): + False: 2.5% vs 62.4% (diff: -59.9%) + True: 97.5% vs 37.6% (diff: +59.9%) + +Round USD (5%): + False: 5.1% vs 74.2% (diff: -69.1%) + True: 94.9% vs 25.8% (diff: +69.1%) + +Round USD (2%): + False: 29.2% vs 82.1% (diff: -52.9%) + True: 70.8% vs 17.9% (diff: +52.9%) + +Round USD (1%): + False: 38.2% vs 84.6% (diff: -46.4%) + True: 61.8% vs 15.4% (diff: +46.4%) + +Phase USD (1%): + False: 67.9% vs 57.7% (diff: +10.2%) + True: 32.1% vs 42.3% (diff: -10.2%) + +Phase USD (2%): + False: 50.7% vs 34.9% (diff: +15.8%) + True: 49.3% vs 65.1% (diff: -15.8%) + +Phase USD (5%): + False: 9.8% vs 5.4% (diff: +4.4%) + True: 90.2% vs 94.6% (diff: -4.4%) + +Phase USD (10%): + +Tx pattern: + 1-to-2: 52.0% vs 46.9% (diff: +5.2%) + many-to-2: 5.7% vs 7.7% (diff: -2.0%) + +Value similarity (2-out): + +Micro-round sats: + False: 89.6% vs 85.7% (diff: +3.9%) + True: 10.4% vs 14.3% (diff: -3.9%) + +================================================== +EXCLUSION CANDIDATES (overrepresented in noise): +================================================== +Characteristics where noise% > accurate% suggest exclusion filters: + + EXCLUDE Round USD 5%=False: noise 74.2% vs accurate 5.1% (+69.1%) + EXCLUDE Round USD 10%=False: noise 62.4% vs accurate 2.5% (+59.9%) + EXCLUDE Round USD 2%=False: noise 82.1% vs accurate 29.2% (+52.9%) + EXCLUDE Round USD 1%=False: noise 84.6% vs accurate 38.2% (+46.4%) + EXCLUDE Phase USD 2%=True: noise 65.1% vs accurate 49.3% (+15.8%) + EXCLUDE Phase USD 1%=True: noise 42.3% vs accurate 32.1% (+10.2%) + EXCLUDE Is round BTC=True: noise 9.0% vs accurate 0.7% (+8.3%) + EXCLUDE Phase USD 5%=True: noise 94.6% vs accurate 90.2% (+4.4%) + EXCLUDE Micro-round sats=True: noise 14.3% vs accurate 10.4% (+3.9%) + +================================================== +INCLUSION SIGNALS (overrepresented in accurate): +================================================== +Characteristics where accurate% > noise% are good signals: + + KEEP Round USD 5%=True: accurate 94.9% vs noise 25.8% (+69.1%) + KEEP Round USD 10%=True: accurate 97.5% vs noise 37.6% (+59.9%) + KEEP Round USD 2%=True: accurate 70.8% vs noise 17.9% (+52.9%) + KEEP Round USD 1%=True: accurate 61.8% vs noise 15.4% (+46.4%) + KEEP Phase USD 2%=False: accurate 50.7% vs noise 34.9% (+15.8%) + KEEP Phase USD 1%=False: accurate 67.9% vs noise 57.7% (+10.2%) + KEEP Is round BTC=False: accurate 99.3% vs noise 91.0% (+8.3%) + KEEP Tx pattern=1-to-2: accurate 52.0% vs noise 46.9% (+5.2%) + KEEP Value range=1M-10M: accurate 36.5% vs noise 31.6% (+4.9%) + KEEP Decade=6: accurate 36.5% vs noise 31.6% (+4.9%) + KEEP Phase USD 5%=False: accurate 9.8% vs noise 5.4% (+4.4%) + + +############################################################ +# 2017-07 +############################################################ +Date range: 2017-07-01 to 2017-07-31 (dateindex 3096-3127) + +--- SUMMARY --- +Total outputs analyzed: 18,594,197 + Accurate (≤15% error): 1,607,604 (8.6%) + Close (15-30% error): 1,570,549 (8.4%) + Wrong decade: 4,604,315 (24.8%) + Noise: 10,811,729 (58.1%) + +================================================== +ACCURATE (within 15% of actual price) (n=1,607,604) +================================================== + +Output count: {1: '80,767 (5.0%)', 2: '1,001,500 (62.3%)', 3: '53,916 (3.4%)', 4: '24,423 (1.5%)', 5: '446,998 (27.8%)'} +Input count: {1: '1,168,811 (72.7%)', 2: '172,023 (10.7%)', 3: '68,047 (4.2%)', 4: '38,940 (2.4%)', 5: '159,783 (9.9%)'} +Output type: {'p2ms': '20 (0.0%)', 'p2pk33': '4,920 (0.3%)', 'p2pkh': '1,323,229 (82.3%)', 'p2sh': '279,435 (17.4%)'} +Is round BTC: {False: '1,578,399 (98.2%)', True: '29,205 (1.8%)'} +Both outputs round: {False: '1,607,090 (100.0%)', True: '514 (0.0%)'} +Same-day spend: {False: '433,282 (27.0%)', True: '1,174,322 (73.0%)'} +Has OP_RETURN: {False: '1,592,483 (99.1%)', True: '15,121 (0.9%)'} +Witness size: {'0': '1,607,604 (100.0%)'} +Value range (sats): {'100M+': '160,010 (10.0%)', '100k-1M': '470,142 (29.2%)', '10M-100M': '273,179 (17.0%)', '10k-100k': '165,384 (10.3%)', '1M-10M': '513,233 (31.9%)', '<10k': '25,656 (1.6%)'} +Decade (10^N sats): {3: '25,656 (1.6%)', 4: '165,384 (10.3%)', 5: '470,142 (29.2%)', 6: '513,233 (31.9%)', 7: '273,179 (17.0%)', 8: '107,675 (6.7%)', 9: '39,352 (2.4%)', 10: '12,803 (0.8%)', 11: '174 (0.0%)', 12: '6 (0.0%)'} +Implied USD value: {'$1-$10': '268,731 (16.7%)', '$10-$100': '520,608 (32.4%)', '$100-$1k': '404,303 (25.1%)', '$10k+': '110,834 (6.9%)', '$1k-$10k': '193,197 (12.0%)', '<$1': '109,931 (6.8%)'} +Output index (2-out only): {0: '516,231 (32.1%)', 1: '485,269 (30.2%)'} +Is smaller output (2-out): {False: '485,650 (30.2%)', True: '515,850 (32.1%)'} +Round pattern (2-out): {'both_round': '514 (0.0%)', 'neither_round': '937,854 (58.3%)', 'only_other_round': '48,737 (3.0%)', 'only_this_round': '14,395 (0.9%)'} +Value ratio (2-out): {'10-30%': '132,780 (8.3%)', '30-50%': '90,436 (5.6%)', '50-70%': '81,718 (5.1%)', '70-90%': '130,059 (8.1%)', '<10%': '292,634 (18.2%)', '>90%': '273,873 (17.0%)'} +Error from actual price: {'<5%': '1,607,604 (100.0%)'} +Tx total value: {'0.01-0.1 BTC': '368,758 (22.9%)', '0.1-1 BTC': '395,064 (24.6%)', '1-10 BTC': '351,080 (21.8%)', '10+ BTC': '313,513 (19.5%)', '<0.01 BTC': '179,189 (11.1%)'} +Round USD (10% tol): {False: '38,639 (2.4%)', True: '1,568,965 (97.6%)'} +Round USD (5% tol): {False: '68,163 (4.2%)', True: '1,539,441 (95.8%)'} +Round USD (2% tol): {False: '449,214 (27.9%)', True: '1,158,390 (72.1%)'} +Round USD (1% tol): {False: '590,748 (36.7%)', True: '1,016,856 (63.3%)'} +Tx pattern: {'1-to-1': '50,094 (3.1%)', '1-to-2': '799,646 (49.7%)', '1-to-many': '319,071 (19.8%)', '2-to-1': '8,543 (0.5%)', '2-to-2': '108,567 (6.8%)', '2-to-many': '54,913 (3.4%)', 'many-to-1': '22,130 (1.4%)', 'many-to-2': '93,287 (5.8%)', 'many-to-many': '151,353 (9.4%)'} +Value similarity (2-out): {'different': '197,342 (12.3%)', 'moderate': '86,609 (5.4%)', 'nearly_equal': '17,657 (1.1%)', 'similar': '35,369 (2.2%)', 'very_different': '661,640 (41.2%)'} +Micro-round sats: {False: '1,395,827 (86.8%)', True: '211,777 (13.2%)'} +Phase USD (1% tol): {False: '1,114,523 (69.3%)', True: '493,081 (30.7%)'} +Phase USD (2% tol): {False: '816,485 (50.8%)', True: '791,119 (49.2%)'} +Phase USD (5% tol): {False: '97,894 (6.1%)', True: '1,509,710 (93.9%)'} +Phase USD (10% tol): {True: '1,607,604 (100.0%)'} + +Top 10 bins: + Bin 60: 206,424 (12.8%) + Bin 59: 146,016 (9.1%) + Bin 58: 129,956 (8.1%) + Bin 61: 113,602 (7.1%) + Bin 57: 112,543 (7.0%) + Bin 56: 112,181 (7.0%) + Bin 62: 102,330 (6.4%) + Bin 63: 93,408 (5.8%) + Bin 55: 79,844 (5.0%) + Bin 64: 70,583 (4.4%) + +================================================== +NOISE (no decade matches) (n=10,811,729) +================================================== + +Output count: {1: '527,535 (4.9%)', 2: '6,749,077 (62.4%)', 3: '360,971 (3.3%)', 4: '176,902 (1.6%)', 5: '2,997,244 (27.7%)'} +Input count: {1: '7,413,044 (68.6%)', 2: '1,324,251 (12.2%)', 3: '542,985 (5.0%)', 4: '306,366 (2.8%)', 5: '1,225,083 (11.3%)'} +Output type: {'opreturn': '4 (0.0%)', 'p2ms': '53 (0.0%)', 'p2pk33': '11,631 (0.1%)', 'p2pk65': '1 (0.0%)', 'p2pkh': '8,922,080 (82.5%)', 'p2sh': '1,877,960 (17.4%)'} +Is round BTC: {False: '9,969,008 (92.2%)', True: '842,721 (7.8%)'} +Both outputs round: {False: '10,792,727 (99.8%)', True: '19,002 (0.2%)'} +Same-day spend: {False: '2,936,076 (27.2%)', True: '7,875,653 (72.8%)'} +Has OP_RETURN: {False: '10,622,179 (98.2%)', True: '189,550 (1.8%)'} +Witness size: {'0': '10,811,729 (100.0%)'} +Value range (sats): {'100M+': '1,347,545 (12.5%)', '100k-1M': '2,777,034 (25.7%)', '10M-100M': '2,009,567 (18.6%)', '10k-100k': '975,156 (9.0%)', '1M-10M': '3,353,935 (31.0%)', '<10k': '348,492 (3.2%)'} +Decade (10^N sats): {3: '348,492 (3.2%)', 4: '975,156 (9.0%)', 5: '2,777,034 (25.7%)', 6: '3,353,935 (31.0%)', 7: '2,009,567 (18.6%)', 8: '930,219 (8.6%)', 9: '347,070 (3.2%)', 10: '67,731 (0.6%)', 11: '2,219 (0.0%)', 12: '306 (0.0%)'} +Implied USD value: {'$1-$10': '1,580,075 (14.6%)', '$10-$100': '3,699,651 (34.2%)', '$100-$1k': '2,590,782 (24.0%)', '$10k+': '773,807 (7.2%)', '$1k-$10k': '1,438,134 (13.3%)', '<$1': '729,280 (6.7%)'} +Output index (2-out only): {0: '3,326,017 (30.8%)', 1: '3,423,060 (31.7%)'} +Is smaller output (2-out): {False: '3,364,943 (31.1%)', True: '3,384,134 (31.3%)'} +Round pattern (2-out): {'both_round': '19,002 (0.2%)', 'neither_round': '5,826,516 (53.9%)', 'only_other_round': '398,046 (3.7%)', 'only_this_round': '505,513 (4.7%)'} +Value ratio (2-out): {'10-30%': '834,019 (7.7%)', '30-50%': '620,753 (5.7%)', '50-70%': '589,265 (5.5%)', '70-90%': '829,294 (7.7%)', '<10%': '1,929,362 (17.8%)', '>90%': '1,946,384 (18.0%)'} +Error from actual price: {'15-25%': '1,489,345 (13.8%)', '25-50%': '4,327,733 (40.0%)', '50%+': '4,994,651 (46.2%)'} +Tx total value: {'0.01-0.1 BTC': '2,578,291 (23.8%)', '0.1-1 BTC': '2,746,952 (25.4%)', '1-10 BTC': '2,343,407 (21.7%)', '10+ BTC': '2,184,711 (20.2%)', '<0.01 BTC': '958,368 (8.9%)'} +Round USD (10% tol): {False: '6,629,892 (61.3%)', True: '4,181,837 (38.7%)'} +Round USD (5% tol): {False: '7,989,703 (73.9%)', True: '2,822,026 (26.1%)'} +Round USD (2% tol): {False: '8,898,136 (82.3%)', True: '1,913,593 (17.7%)'} +Round USD (1% tol): {False: '9,183,052 (84.9%)', True: '1,628,677 (15.1%)'} +Tx pattern: {'1-to-1': '340,511 (3.1%)', '1-to-2': '5,006,551 (46.3%)', '1-to-many': '2,065,982 (19.1%)', '2-to-1': '62,032 (0.6%)', '2-to-2': '891,101 (8.2%)', '2-to-many': '371,118 (3.4%)', 'many-to-1': '124,992 (1.2%)', 'many-to-2': '851,425 (7.9%)', 'many-to-many': '1,098,017 (10.2%)'} +Value similarity (2-out): {'different': '1,176,936 (10.9%)', 'moderate': '626,974 (5.8%)', 'nearly_equal': '115,877 (1.1%)', 'similar': '252,359 (2.3%)', 'very_different': '4,466,708 (41.3%)'} +Micro-round sats: {False: '9,464,304 (87.5%)', True: '1,347,425 (12.5%)'} +Phase USD (1% tol): {False: '6,450,861 (59.7%)', True: '4,360,868 (40.3%)'} +Phase USD (2% tol): {False: '3,994,262 (36.9%)', True: '6,817,467 (63.1%)'} +Phase USD (5% tol): {False: '801,022 (7.4%)', True: '10,010,707 (92.6%)'} +Phase USD (10% tol): {True: '10,811,729 (100.0%)'} + +Top 10 bins: + Bin 0: 699,919 (6.5%) + Bin 99: 310,764 (2.9%) + Bin 47: 297,541 (2.8%) + Bin 69: 264,525 (2.4%) + Bin 95: 237,549 (2.2%) + Bin 39: 227,984 (2.1%) + Bin 98: 225,291 (2.1%) + Bin 90: 212,135 (2.0%) + Bin 87: 209,467 (1.9%) + Bin 4: 209,132 (1.9%) + +================================================== +KEY DIFFERENCES (accurate vs noise): +================================================== + +Output count: + +Input count: + 1: 72.7% vs 68.6% (diff: +4.1%) + +Output type: + +Is round BTC: + False: 98.2% vs 92.2% (diff: +6.0%) + True: 1.8% vs 7.8% (diff: -6.0%) + +Both round: + +Same-day spend: + +OP_RETURN: + +Witness size: + +Value range (sats): + 100M+: 10.0% vs 12.5% (diff: -2.5%) + 100k-1M: 29.2% vs 25.7% (diff: +3.6%) + +Decade (10^N sats): + 5: 29.2% vs 25.7% (diff: +3.6%) + +Implied USD: + $1-$10: 16.7% vs 14.6% (diff: +2.1%) + +Output index (2-out): + +Is smaller (2-out): + +Round pattern (2-out): + neither_round: 58.3% vs 53.9% (diff: +4.4%) + only_this_round: 0.9% vs 4.7% (diff: -3.8%) + +Value ratio (2-out): + +Error from price: + 15-25%: 0.0% vs 13.8% (diff: -13.8%) + 25-50%: 0.0% vs 40.0% (diff: -40.0%) + 50%+: 0.0% vs 46.2% (diff: -46.2%) + <5%: 100.0% vs 0.0% (diff: +100.0%) + +Tx total value: + <0.01 BTC: 11.1% vs 8.9% (diff: +2.3%) + +Round USD (10%): + False: 2.4% vs 61.3% (diff: -58.9%) + True: 97.6% vs 38.7% (diff: +58.9%) + +Round USD (5%): + False: 4.2% vs 73.9% (diff: -69.7%) + True: 95.8% vs 26.1% (diff: +69.7%) + +Round USD (2%): + False: 27.9% vs 82.3% (diff: -54.4%) + True: 72.1% vs 17.7% (diff: +54.4%) + +Round USD (1%): + False: 36.7% vs 84.9% (diff: -48.2%) + True: 63.3% vs 15.1% (diff: +48.2%) + +Phase USD (1%): + False: 69.3% vs 59.7% (diff: +9.7%) + True: 30.7% vs 40.3% (diff: -9.7%) + +Phase USD (2%): + False: 50.8% vs 36.9% (diff: +13.8%) + True: 49.2% vs 63.1% (diff: -13.8%) + +Phase USD (5%): + +Phase USD (10%): + +Tx pattern: + 1-to-2: 49.7% vs 46.3% (diff: +3.4%) + many-to-2: 5.8% vs 7.9% (diff: -2.1%) + +Value similarity (2-out): + +Micro-round sats: + +================================================== +EXCLUSION CANDIDATES (overrepresented in noise): +================================================== +Characteristics where noise% > accurate% suggest exclusion filters: + + EXCLUDE Round USD 5%=False: noise 73.9% vs accurate 4.2% (+69.7%) + EXCLUDE Round USD 10%=False: noise 61.3% vs accurate 2.4% (+58.9%) + EXCLUDE Round USD 2%=False: noise 82.3% vs accurate 27.9% (+54.4%) + EXCLUDE Round USD 1%=False: noise 84.9% vs accurate 36.7% (+48.2%) + EXCLUDE Phase USD 2%=True: noise 63.1% vs accurate 49.2% (+13.8%) + EXCLUDE Phase USD 1%=True: noise 40.3% vs accurate 30.7% (+9.7%) + EXCLUDE Is round BTC=True: noise 7.8% vs accurate 1.8% (+6.0%) + +================================================== +INCLUSION SIGNALS (overrepresented in accurate): +================================================== +Characteristics where accurate% > noise% are good signals: + + KEEP Round USD 5%=True: accurate 95.8% vs noise 26.1% (+69.7%) + KEEP Round USD 10%=True: accurate 97.6% vs noise 38.7% (+58.9%) + KEEP Round USD 2%=True: accurate 72.1% vs noise 17.7% (+54.4%) + KEEP Round USD 1%=True: accurate 63.3% vs noise 15.1% (+48.2%) + KEEP Phase USD 2%=False: accurate 50.8% vs noise 36.9% (+13.8%) + KEEP Phase USD 1%=False: accurate 69.3% vs noise 59.7% (+9.7%) + KEEP Is round BTC=False: accurate 98.2% vs noise 92.2% (+6.0%) + KEEP Value range=100k-1M: accurate 29.2% vs noise 25.7% (+3.6%) + KEEP Decade=5: accurate 29.2% vs noise 25.7% (+3.6%) + KEEP Tx pattern=1-to-2: accurate 49.7% vs noise 46.3% (+3.4%) + + +############################################################ +# 2017-08 +############################################################ +Date range: 2017-08-01 to 2017-08-31 (dateindex 3127-3158) + +--- SUMMARY --- +Total outputs analyzed: 20,166,437 + Accurate (≤15% error): 1,672,991 (8.3%) + Close (15-30% error): 1,877,107 (9.3%) + Wrong decade: 5,632,879 (27.9%) + Noise: 10,983,460 (54.5%) + +================================================== +ACCURATE (within 15% of actual price) (n=1,672,991) +================================================== + +Output count: {1: '74,550 (4.5%)', 2: '1,022,143 (61.1%)', 3: '56,355 (3.4%)', 4: '20,375 (1.2%)', 5: '499,568 (29.9%)'} +Input count: {1: '1,214,228 (72.6%)', 2: '177,794 (10.6%)', 3: '70,232 (4.2%)', 4: '40,507 (2.4%)', 5: '170,230 (10.2%)'} +Output type: {'p2pkh': '1,394,605 (83.4%)', 'p2sh': '278,378 (16.6%)', 'p2wpkh': '8 (0.0%)'} +Is round BTC: {False: '1,623,504 (97.0%)', True: '49,487 (3.0%)'} +Both outputs round: {False: '1,671,928 (99.9%)', True: '1,063 (0.1%)'} +Same-day spend: {False: '434,178 (26.0%)', True: '1,238,813 (74.0%)'} +Has OP_RETURN: {False: '1,653,234 (98.8%)', True: '19,757 (1.2%)'} +Witness size: {'0': '1,671,619 (99.9%)', '1-499': '1,072 (0.1%)', '1000-2499': '121 (0.0%)', '2500+': '20 (0.0%)', '500-999': '159 (0.0%)'} +Value range (sats): {'100M+': '169,077 (10.1%)', '100k-1M': '457,055 (27.3%)', '10M-100M': '315,901 (18.9%)', '10k-100k': '141,553 (8.5%)', '1M-10M': '556,041 (33.2%)', '<10k': '33,364 (2.0%)'} +Decade (10^N sats): {3: '33,364 (2.0%)', 4: '141,553 (8.5%)', 5: '457,055 (27.3%)', 6: '556,041 (33.2%)', 7: '315,901 (18.9%)', 8: '121,889 (7.3%)', 9: '38,139 (2.3%)', 10: '8,713 (0.5%)', 11: '335 (0.0%)', 12: '1 (0.0%)'} +Implied USD value: {'$1-$10': '231,293 (13.8%)', '$10-$100': '554,066 (33.1%)', '$100-$1k': '436,472 (26.1%)', '$10k+': '111,388 (6.7%)', '$1k-$10k': '229,778 (13.7%)', '<$1': '109,994 (6.6%)'} +Output index (2-out only): {0: '529,324 (31.6%)', 1: '492,819 (29.5%)'} +Is smaller output (2-out): {False: '501,321 (30.0%)', True: '520,822 (31.1%)'} +Round pattern (2-out): {'both_round': '1,063 (0.1%)', 'neither_round': '947,034 (56.6%)', 'only_other_round': '47,108 (2.8%)', 'only_this_round': '26,938 (1.6%)'} +Value ratio (2-out): {'10-30%': '137,202 (8.2%)', '30-50%': '85,965 (5.1%)', '50-70%': '92,700 (5.5%)', '70-90%': '131,106 (7.8%)', '<10%': '297,655 (17.8%)', '>90%': '277,515 (16.6%)'} +Error from actual price: {'<5%': '1,672,991 (100.0%)'} +Tx total value: {'0.01-0.1 BTC': '372,421 (22.3%)', '0.1-1 BTC': '405,248 (24.2%)', '1-10 BTC': '372,593 (22.3%)', '10+ BTC': '331,794 (19.8%)', '<0.01 BTC': '190,935 (11.4%)'} +Round USD (10% tol): {False: '42,413 (2.5%)', True: '1,630,578 (97.5%)'} +Round USD (5% tol): {False: '75,009 (4.5%)', True: '1,597,982 (95.5%)'} +Round USD (2% tol): {False: '498,153 (29.8%)', True: '1,174,838 (70.2%)'} +Round USD (1% tol): {False: '659,437 (39.4%)', True: '1,013,554 (60.6%)'} +Tx pattern: {'1-to-1': '52,831 (3.2%)', '1-to-2': '835,536 (49.9%)', '1-to-many': '325,861 (19.5%)', '2-to-1': '9,469 (0.6%)', '2-to-2': '100,865 (6.0%)', '2-to-many': '67,460 (4.0%)', 'many-to-1': '12,250 (0.7%)', 'many-to-2': '85,742 (5.1%)', 'many-to-many': '182,977 (10.9%)'} +Value similarity (2-out): {'different': '200,370 (12.0%)', 'moderate': '88,142 (5.3%)', 'nearly_equal': '18,570 (1.1%)', 'similar': '37,195 (2.2%)', 'very_different': '670,856 (40.1%)'} +Micro-round sats: {False: '1,489,321 (89.0%)', True: '183,670 (11.0%)'} +Phase USD (1% tol): {False: '1,191,710 (71.2%)', True: '481,281 (28.8%)'} +Phase USD (2% tol): {False: '851,975 (50.9%)', True: '821,016 (49.1%)'} +Phase USD (5% tol): {False: '57,626 (3.4%)', True: '1,615,365 (96.6%)'} +Phase USD (10% tol): {True: '1,672,991 (100.0%)'} + +Top 10 bins: + Bin 36: 149,993 (9.0%) + Bin 39: 146,660 (8.8%) + Bin 38: 144,940 (8.7%) + Bin 37: 111,222 (6.6%) + Bin 47: 102,420 (6.1%) + Bin 35: 92,657 (5.5%) + Bin 34: 92,494 (5.5%) + Bin 41: 77,942 (4.7%) + Bin 46: 71,248 (4.3%) + Bin 40: 70,581 (4.2%) + +================================================== +NOISE (no decade matches) (n=10,983,460) +================================================== + +Output count: {1: '538,998 (4.9%)', 2: '6,772,945 (61.7%)', 3: '396,688 (3.6%)', 4: '141,771 (1.3%)', 5: '3,133,058 (28.5%)'} +Input count: {1: '7,819,778 (71.2%)', 2: '1,190,546 (10.8%)', 3: '520,398 (4.7%)', 4: '277,855 (2.5%)', 5: '1,174,883 (10.7%)'} +Output type: {'opreturn': '1 (0.0%)', 'p2ms': '8 (0.0%)', 'p2pk33': '19,284 (0.2%)', 'p2pk65': '1 (0.0%)', 'p2pkh': '9,088,285 (82.7%)', 'p2sh': '1,875,687 (17.1%)', 'p2wpkh': '191 (0.0%)', 'p2wsh': '3 (0.0%)'} +Is round BTC: {False: '10,433,951 (95.0%)', True: '549,509 (5.0%)'} +Both outputs round: {False: '10,970,848 (99.9%)', True: '12,612 (0.1%)'} +Same-day spend: {False: '2,792,399 (25.4%)', True: '8,191,061 (74.6%)'} +Has OP_RETURN: {False: '10,804,759 (98.4%)', True: '178,701 (1.6%)'} +Witness size: {'0': '10,972,720 (99.9%)', '1-499': '8,964 (0.1%)', '1000-2499': '735 (0.0%)', '2500+': '101 (0.0%)', '500-999': '940 (0.0%)'} +Value range (sats): {'100M+': '1,367,046 (12.4%)', '100k-1M': '2,939,646 (26.8%)', '10M-100M': '2,051,279 (18.7%)', '10k-100k': '988,262 (9.0%)', '1M-10M': '3,351,568 (30.5%)', '<10k': '285,659 (2.6%)'} +Decade (10^N sats): {3: '285,659 (2.6%)', 4: '988,262 (9.0%)', 5: '2,939,646 (26.8%)', 6: '3,351,568 (30.5%)', 7: '2,051,279 (18.7%)', 8: '933,837 (8.5%)', 9: '376,248 (3.4%)', 10: '54,461 (0.5%)', 11: '2,420 (0.0%)', 12: '80 (0.0%)'} +Implied USD value: {'$1-$10': '1,515,350 (13.8%)', '$10-$100': '3,385,557 (30.8%)', '$100-$1k': '2,944,087 (26.8%)', '$10k+': '998,446 (9.1%)', '$1k-$10k': '1,637,285 (14.9%)', '<$1': '502,735 (4.6%)'} +Output index (2-out only): {0: '3,319,576 (30.2%)', 1: '3,453,369 (31.4%)'} +Is smaller output (2-out): {False: '3,540,794 (32.2%)', True: '3,232,151 (29.4%)'} +Round pattern (2-out): {'both_round': '12,612 (0.1%)', 'neither_round': '6,003,536 (54.7%)', 'only_other_round': '425,756 (3.9%)', 'only_this_round': '331,041 (3.0%)'} +Value ratio (2-out): {'10-30%': '808,179 (7.4%)', '30-50%': '594,936 (5.4%)', '50-70%': '585,198 (5.3%)', '70-90%': '866,994 (7.9%)', '<10%': '1,829,036 (16.7%)', '>90%': '2,088,602 (19.0%)'} +Error from actual price: {'15-25%': '1,628,600 (14.8%)', '25-50%': '4,788,417 (43.6%)', '50%+': '4,566,443 (41.6%)'} +Tx total value: {'0.01-0.1 BTC': '2,470,309 (22.5%)', '0.1-1 BTC': '2,611,556 (23.8%)', '1-10 BTC': '2,310,929 (21.0%)', '10+ BTC': '2,417,054 (22.0%)', '<0.01 BTC': '1,173,612 (10.7%)'} +Round USD (10% tol): {False: '7,391,312 (67.3%)', True: '3,592,148 (32.7%)'} +Round USD (5% tol): {False: '8,720,337 (79.4%)', True: '2,263,123 (20.6%)'} +Round USD (2% tol): {False: '9,475,785 (86.3%)', True: '1,507,675 (13.7%)'} +Round USD (1% tol): {False: '9,728,098 (88.6%)', True: '1,255,362 (11.4%)'} +Tx pattern: {'1-to-1': '366,982 (3.3%)', '1-to-2': '5,332,335 (48.5%)', '1-to-many': '2,120,461 (19.3%)', '2-to-1': '72,965 (0.7%)', '2-to-2': '735,890 (6.7%)', '2-to-many': '381,691 (3.5%)', 'many-to-1': '99,051 (0.9%)', 'many-to-2': '704,720 (6.4%)', 'many-to-many': '1,169,365 (10.6%)'} +Value similarity (2-out): {'different': '1,188,080 (10.8%)', 'moderate': '647,623 (5.9%)', 'nearly_equal': '92,368 (0.8%)', 'similar': '220,547 (2.0%)', 'very_different': '4,533,932 (41.3%)'} +Micro-round sats: {False: '9,812,262 (89.3%)', True: '1,171,198 (10.7%)'} +Phase USD (1% tol): {False: '7,233,534 (65.9%)', True: '3,749,926 (34.1%)'} +Phase USD (2% tol): {False: '5,097,496 (46.4%)', True: '5,885,964 (53.6%)'} +Phase USD (5% tol): {False: '732,133 (6.7%)', True: '10,251,327 (93.3%)'} +Phase USD (10% tol): {True: '10,983,460 (100.0%)'} + +Top 10 bins: + Bin 69: 480,525 (4.4%) + Bin 30: 237,599 (2.2%) + Bin 77: 230,666 (2.1%) + Bin 60: 221,914 (2.0%) + Bin 84: 209,320 (1.9%) + Bin 73: 204,893 (1.9%) + Bin 66: 194,716 (1.8%) + Bin 47: 190,200 (1.7%) + Bin 65: 189,386 (1.7%) + Bin 64: 177,114 (1.6%) + +================================================== +KEY DIFFERENCES (accurate vs noise): +================================================== + +Output count: + +Input count: + +Output type: + +Is round BTC: + False: 97.0% vs 95.0% (diff: +2.0%) + True: 3.0% vs 5.0% (diff: -2.0%) + +Both round: + +Same-day spend: + +OP_RETURN: + +Witness size: + +Value range (sats): + 100M+: 10.1% vs 12.4% (diff: -2.3%) + 1M-10M: 33.2% vs 30.5% (diff: +2.7%) + +Decade (10^N sats): + 6: 33.2% vs 30.5% (diff: +2.7%) + +Implied USD: + $10-$100: 33.1% vs 30.8% (diff: +2.3%) + $10k+: 6.7% vs 9.1% (diff: -2.4%) + +Output index (2-out): + +Is smaller (2-out): + False: 30.0% vs 32.2% (diff: -2.3%) + +Round pattern (2-out): + +Value ratio (2-out): + >90%: 16.6% vs 19.0% (diff: -2.4%) + +Error from price: + 15-25%: 0.0% vs 14.8% (diff: -14.8%) + 25-50%: 0.0% vs 43.6% (diff: -43.6%) + 50%+: 0.0% vs 41.6% (diff: -41.6%) + <5%: 100.0% vs 0.0% (diff: +100.0%) + +Tx total value: + 10+ BTC: 19.8% vs 22.0% (diff: -2.2%) + +Round USD (10%): + False: 2.5% vs 67.3% (diff: -64.8%) + True: 97.5% vs 32.7% (diff: +64.8%) + +Round USD (5%): + False: 4.5% vs 79.4% (diff: -74.9%) + True: 95.5% vs 20.6% (diff: +74.9%) + +Round USD (2%): + False: 29.8% vs 86.3% (diff: -56.5%) + True: 70.2% vs 13.7% (diff: +56.5%) + +Round USD (1%): + False: 39.4% vs 88.6% (diff: -49.2%) + True: 60.6% vs 11.4% (diff: +49.2%) + +Phase USD (1%): + False: 71.2% vs 65.9% (diff: +5.4%) + True: 28.8% vs 34.1% (diff: -5.4%) + +Phase USD (2%): + False: 50.9% vs 46.4% (diff: +4.5%) + True: 49.1% vs 53.6% (diff: -4.5%) + +Phase USD (5%): + False: 3.4% vs 6.7% (diff: -3.2%) + True: 96.6% vs 93.3% (diff: +3.2%) + +Phase USD (10%): + +Tx pattern: + +Value similarity (2-out): + +Micro-round sats: + +================================================== +EXCLUSION CANDIDATES (overrepresented in noise): +================================================== +Characteristics where noise% > accurate% suggest exclusion filters: + + EXCLUDE Round USD 5%=False: noise 79.4% vs accurate 4.5% (+74.9%) + EXCLUDE Round USD 10%=False: noise 67.3% vs accurate 2.5% (+64.8%) + EXCLUDE Round USD 2%=False: noise 86.3% vs accurate 29.8% (+56.5%) + EXCLUDE Round USD 1%=False: noise 88.6% vs accurate 39.4% (+49.2%) + EXCLUDE Phase USD 1%=True: noise 34.1% vs accurate 28.8% (+5.4%) + EXCLUDE Phase USD 2%=True: noise 53.6% vs accurate 49.1% (+4.5%) + EXCLUDE Phase USD 5%=False: noise 6.7% vs accurate 3.4% (+3.2%) + +================================================== +INCLUSION SIGNALS (overrepresented in accurate): +================================================== +Characteristics where accurate% > noise% are good signals: + + KEEP Round USD 5%=True: accurate 95.5% vs noise 20.6% (+74.9%) + KEEP Round USD 10%=True: accurate 97.5% vs noise 32.7% (+64.8%) + KEEP Round USD 2%=True: accurate 70.2% vs noise 13.7% (+56.5%) + KEEP Round USD 1%=True: accurate 60.6% vs noise 11.4% (+49.2%) + KEEP Phase USD 1%=False: accurate 71.2% vs noise 65.9% (+5.4%) + KEEP Phase USD 2%=False: accurate 50.9% vs noise 46.4% (+4.5%) + KEEP Phase USD 5%=True: accurate 96.6% vs noise 93.3% (+3.2%) + + +############################################################ +# 2017-09 +############################################################ +Date range: 2017-09-01 to 2017-09-30 (dateindex 3158-3188) + +--- SUMMARY --- +Total outputs analyzed: 18,951,675 + Accurate (≤15% error): 1,783,262 (9.4%) + Close (15-30% error): 1,638,085 (8.6%) + Wrong decade: 5,177,109 (27.3%) + Noise: 10,353,219 (54.6%) + +================================================== +ACCURATE (within 15% of actual price) (n=1,783,262) +================================================== + +Output count: {1: '73,128 (4.1%)', 2: '1,097,112 (61.5%)', 3: '65,799 (3.7%)', 4: '25,516 (1.4%)', 5: '521,707 (29.3%)'} +Input count: {1: '1,291,147 (72.4%)', 2: '191,074 (10.7%)', 3: '72,430 (4.1%)', 4: '40,765 (2.3%)', 5: '187,846 (10.5%)'} +Output type: {'p2pk33': '147 (0.0%)', 'p2pkh': '1,461,938 (82.0%)', 'p2sh': '321,048 (18.0%)', 'p2wpkh': '2 (0.0%)', 'p2wsh': '127 (0.0%)'} +Is round BTC: {False: '1,736,267 (97.4%)', True: '46,995 (2.6%)'} +Both outputs round: {False: '1,780,896 (99.9%)', True: '2,366 (0.1%)'} +Same-day spend: {False: '458,160 (25.7%)', True: '1,325,102 (74.3%)'} +Has OP_RETURN: {False: '1,762,018 (98.8%)', True: '21,244 (1.2%)'} +Witness size: {'0': '1,723,182 (96.6%)', '1-499': '41,816 (2.3%)', '1000-2499': '7,339 (0.4%)', '2500+': '3,828 (0.2%)', '500-999': '7,097 (0.4%)'} +Value range (sats): {'100M+': '208,385 (11.7%)', '100k-1M': '476,534 (26.7%)', '10M-100M': '358,364 (20.1%)', '10k-100k': '109,042 (6.1%)', '1M-10M': '593,341 (33.3%)', '<10k': '37,596 (2.1%)'} +Decade (10^N sats): {3: '37,596 (2.1%)', 4: '109,042 (6.1%)', 5: '476,534 (26.7%)', 6: '593,341 (33.3%)', 7: '358,364 (20.1%)', 8: '146,566 (8.2%)', 9: '49,772 (2.8%)', 10: '11,365 (0.6%)', 11: '649 (0.0%)', 12: '33 (0.0%)'} +Implied USD value: {'$1-$10': '251,514 (14.1%)', '$10-$100': '564,506 (31.7%)', '$100-$1k': '481,495 (27.0%)', '$10k+': '138,369 (7.8%)', '$1k-$10k': '257,959 (14.5%)', '<$1': '89,419 (5.0%)'} +Output index (2-out only): {0: '568,859 (31.9%)', 1: '528,253 (29.6%)'} +Is smaller output (2-out): {False: '535,431 (30.0%)', True: '561,681 (31.5%)'} +Round pattern (2-out): {'both_round': '2,366 (0.1%)', 'neither_round': '1,011,910 (56.7%)', 'only_other_round': '55,741 (3.1%)', 'only_this_round': '27,095 (1.5%)'} +Value ratio (2-out): {'10-30%': '151,084 (8.5%)', '30-50%': '90,970 (5.1%)', '50-70%': '100,131 (5.6%)', '70-90%': '131,405 (7.4%)', '<10%': '319,627 (17.9%)', '>90%': '303,895 (17.0%)'} +Error from actual price: {'<5%': '1,783,262 (100.0%)'} +Tx total value: {'0.01-0.1 BTC': '377,814 (21.2%)', '0.1-1 BTC': '418,755 (23.5%)', '1-10 BTC': '395,150 (22.2%)', '10+ BTC': '394,439 (22.1%)', '<0.01 BTC': '197,104 (11.1%)'} +Round USD (10% tol): {False: '49,643 (2.8%)', True: '1,733,619 (97.2%)'} +Round USD (5% tol): {False: '111,392 (6.2%)', True: '1,671,870 (93.8%)'} +Round USD (2% tol): {False: '525,893 (29.5%)', True: '1,257,369 (70.5%)'} +Round USD (1% tol): {False: '684,636 (38.4%)', True: '1,098,626 (61.6%)'} +Tx pattern: {'1-to-1': '48,638 (2.7%)', '1-to-2': '893,553 (50.1%)', '1-to-many': '348,956 (19.6%)', '2-to-1': '10,121 (0.6%)', '2-to-2': '107,067 (6.0%)', '2-to-many': '73,886 (4.1%)', 'many-to-1': '14,369 (0.8%)', 'many-to-2': '96,492 (5.4%)', 'many-to-many': '190,180 (10.7%)'} +Value similarity (2-out): {'different': '211,147 (11.8%)', 'moderate': '93,696 (5.3%)', 'nearly_equal': '19,490 (1.1%)', 'similar': '40,446 (2.3%)', 'very_different': '723,048 (40.5%)'} +Micro-round sats: {False: '1,592,065 (89.3%)', True: '191,197 (10.7%)'} +Phase USD (1% tol): {False: '1,212,942 (68.0%)', True: '570,320 (32.0%)'} +Phase USD (2% tol): {False: '849,495 (47.6%)', True: '933,767 (52.4%)'} +Phase USD (5% tol): {False: '12,739 (0.7%)', True: '1,770,523 (99.3%)'} +Phase USD (10% tol): {True: '1,783,262 (100.0%)'} + +Top 10 bins: + Bin 39: 175,799 (9.9%) + Bin 40: 152,483 (8.6%) + Bin 38: 143,694 (8.1%) + Bin 36: 137,340 (7.7%) + Bin 43: 118,641 (6.7%) + Bin 41: 116,939 (6.6%) + Bin 37: 114,347 (6.4%) + Bin 34: 98,369 (5.5%) + Bin 42: 96,395 (5.4%) + Bin 35: 95,508 (5.4%) + +================================================== +NOISE (no decade matches) (n=10,353,219) +================================================== + +Output count: {1: '467,722 (4.5%)', 2: '6,225,272 (60.1%)', 3: '362,401 (3.5%)', 4: '144,569 (1.4%)', 5: '3,153,255 (30.5%)'} +Input count: {1: '7,578,866 (73.2%)', 2: '1,054,101 (10.2%)', 3: '424,880 (4.1%)', 4: '231,089 (2.2%)', 5: '1,064,283 (10.3%)'} +Output type: {'p2ms': '19 (0.0%)', 'p2pk33': '32,246 (0.3%)', 'p2pkh': '8,494,123 (82.0%)', 'p2sh': '1,824,412 (17.6%)', 'p2wpkh': '1,574 (0.0%)', 'p2wsh': '845 (0.0%)'} +Is round BTC: {False: '9,927,608 (95.9%)', True: '425,611 (4.1%)'} +Both outputs round: {False: '10,345,885 (99.9%)', True: '7,334 (0.1%)'} +Same-day spend: {False: '2,497,492 (24.1%)', True: '7,855,727 (75.9%)'} +Has OP_RETURN: {False: '10,210,157 (98.6%)', True: '143,062 (1.4%)'} +Witness size: {'0': '10,010,971 (96.7%)', '1-499': '252,280 (2.4%)', '1000-2499': '34,942 (0.3%)', '2500+': '17,070 (0.2%)', '500-999': '37,956 (0.4%)'} +Value range (sats): {'100M+': '1,344,772 (13.0%)', '100k-1M': '2,916,201 (28.2%)', '10M-100M': '1,905,288 (18.4%)', '10k-100k': '795,595 (7.7%)', '1M-10M': '3,143,371 (30.4%)', '<10k': '247,992 (2.4%)'} +Decade (10^N sats): {3: '247,992 (2.4%)', 4: '795,595 (7.7%)', 5: '2,916,201 (28.2%)', 6: '3,143,371 (30.4%)', 7: '1,905,288 (18.4%)', 8: '894,935 (8.6%)', 9: '390,997 (3.8%)', 10: '56,578 (0.5%)', 11: '2,207 (0.0%)', 12: '55 (0.0%)'} +Implied USD value: {'$1-$10': '1,408,758 (13.6%)', '$10-$100': '3,186,446 (30.8%)', '$100-$1k': '2,774,977 (26.8%)', '$10k+': '1,017,857 (9.8%)', '$1k-$10k': '1,534,515 (14.8%)', '<$1': '430,666 (4.2%)'} +Output index (2-out only): {0: '3,055,578 (29.5%)', 1: '3,169,694 (30.6%)'} +Is smaller output (2-out): {False: '3,272,800 (31.6%)', True: '2,952,472 (28.5%)'} +Round pattern (2-out): {'both_round': '7,334 (0.1%)', 'neither_round': '5,576,637 (53.9%)', 'only_other_round': '391,459 (3.8%)', 'only_this_round': '249,842 (2.4%)'} +Value ratio (2-out): {'10-30%': '730,992 (7.1%)', '30-50%': '534,562 (5.2%)', '50-70%': '542,362 (5.2%)', '70-90%': '803,088 (7.8%)', '<10%': '1,686,918 (16.3%)', '>90%': '1,927,350 (18.6%)'} +Error from actual price: {'15-25%': '1,599,250 (15.4%)', '25-50%': '4,647,742 (44.9%)', '50%+': '4,106,227 (39.7%)'} +Tx total value: {'0.01-0.1 BTC': '2,217,672 (21.4%)', '0.1-1 BTC': '2,283,899 (22.1%)', '1-10 BTC': '2,183,173 (21.1%)', '10+ BTC': '2,559,727 (24.7%)', '<0.01 BTC': '1,108,748 (10.7%)'} +Round USD (10% tol): {False: '6,699,067 (64.7%)', True: '3,654,152 (35.3%)'} +Round USD (5% tol): {False: '7,908,438 (76.4%)', True: '2,444,781 (23.6%)'} +Round USD (2% tol): {False: '8,672,055 (83.8%)', True: '1,681,164 (16.2%)'} +Round USD (1% tol): {False: '8,924,695 (86.2%)', True: '1,428,524 (13.8%)'} +Tx pattern: {'1-to-1': '304,731 (2.9%)', '1-to-2': '4,980,293 (48.1%)', '1-to-many': '2,293,842 (22.2%)', '2-to-1': '63,760 (0.6%)', '2-to-2': '659,928 (6.4%)', '2-to-many': '330,413 (3.2%)', 'many-to-1': '99,231 (1.0%)', 'many-to-2': '585,051 (5.7%)', 'many-to-many': '1,035,970 (10.0%)'} +Value similarity (2-out): {'different': '1,080,907 (10.4%)', 'moderate': '601,801 (5.8%)', 'nearly_equal': '83,397 (0.8%)', 'similar': '201,644 (1.9%)', 'very_different': '4,191,973 (40.5%)'} +Micro-round sats: {False: '9,337,490 (90.2%)', True: '1,015,729 (9.8%)'} +Phase USD (1% tol): {False: '7,018,851 (67.8%)', True: '3,334,368 (32.2%)'} +Phase USD (2% tol): {False: '5,079,623 (49.1%)', True: '5,273,596 (50.9%)'} +Phase USD (5% tol): {False: '880,802 (8.5%)', True: '9,472,417 (91.5%)'} +Phase USD (10% tol): {True: '10,353,219 (100.0%)'} + +Top 10 bins: + Bin 69: 461,353 (4.5%) + Bin 60: 254,628 (2.5%) + Bin 30: 251,427 (2.4%) + Bin 17: 224,934 (2.2%) + Bin 77: 216,097 (2.1%) + Bin 20: 188,545 (1.8%) + Bin 18: 182,618 (1.8%) + Bin 23: 180,868 (1.7%) + Bin 73: 178,035 (1.7%) + Bin 70: 175,515 (1.7%) + +================================================== +KEY DIFFERENCES (accurate vs noise): +================================================== + +Output count: + +Input count: + +Output type: + +Is round BTC: + +Both round: + +Same-day spend: + +OP_RETURN: + +Witness size: + +Value range (sats): + 1M-10M: 33.3% vs 30.4% (diff: +2.9%) + +Decade (10^N sats): + 6: 33.3% vs 30.4% (diff: +2.9%) + +Implied USD: + $10k+: 7.8% vs 9.8% (diff: -2.1%) + +Output index (2-out): + 0: 31.9% vs 29.5% (diff: +2.4%) + +Is smaller (2-out): + True: 31.5% vs 28.5% (diff: +3.0%) + +Round pattern (2-out): + neither_round: 56.7% vs 53.9% (diff: +2.9%) + +Value ratio (2-out): + +Error from price: + 15-25%: 0.0% vs 15.4% (diff: -15.4%) + 25-50%: 0.0% vs 44.9% (diff: -44.9%) + 50%+: 0.0% vs 39.7% (diff: -39.7%) + <5%: 100.0% vs 0.0% (diff: +100.0%) + +Tx total value: + 10+ BTC: 22.1% vs 24.7% (diff: -2.6%) + +Round USD (10%): + False: 2.8% vs 64.7% (diff: -61.9%) + True: 97.2% vs 35.3% (diff: +61.9%) + +Round USD (5%): + False: 6.2% vs 76.4% (diff: -70.1%) + True: 93.8% vs 23.6% (diff: +70.1%) + +Round USD (2%): + False: 29.5% vs 83.8% (diff: -54.3%) + True: 70.5% vs 16.2% (diff: +54.3%) + +Round USD (1%): + False: 38.4% vs 86.2% (diff: -47.8%) + True: 61.6% vs 13.8% (diff: +47.8%) + +Phase USD (1%): + +Phase USD (2%): + +Phase USD (5%): + False: 0.7% vs 8.5% (diff: -7.8%) + True: 99.3% vs 91.5% (diff: +7.8%) + +Phase USD (10%): + +Tx pattern: + 1-to-2: 50.1% vs 48.1% (diff: +2.0%) + 1-to-many: 19.6% vs 22.2% (diff: -2.6%) + +Value similarity (2-out): + +Micro-round sats: + +================================================== +EXCLUSION CANDIDATES (overrepresented in noise): +================================================== +Characteristics where noise% > accurate% suggest exclusion filters: + + EXCLUDE Round USD 5%=False: noise 76.4% vs accurate 6.2% (+70.1%) + EXCLUDE Round USD 10%=False: noise 64.7% vs accurate 2.8% (+61.9%) + EXCLUDE Round USD 2%=False: noise 83.8% vs accurate 29.5% (+54.3%) + EXCLUDE Round USD 1%=False: noise 86.2% vs accurate 38.4% (+47.8%) + EXCLUDE Phase USD 5%=False: noise 8.5% vs accurate 0.7% (+7.8%) + +================================================== +INCLUSION SIGNALS (overrepresented in accurate): +================================================== +Characteristics where accurate% > noise% are good signals: + + KEEP Round USD 5%=True: accurate 93.8% vs noise 23.6% (+70.1%) + KEEP Round USD 10%=True: accurate 97.2% vs noise 35.3% (+61.9%) + KEEP Round USD 2%=True: accurate 70.5% vs noise 16.2% (+54.3%) + KEEP Round USD 1%=True: accurate 61.6% vs noise 13.8% (+47.8%) + KEEP Phase USD 5%=True: accurate 99.3% vs noise 91.5% (+7.8%) + + +############################################################ +# 2017-10 +############################################################ +Date range: 2017-10-01 to 2017-10-31 (dateindex 3188-3219) + +--- SUMMARY --- +Total outputs analyzed: 22,683,316 + Accurate (≤15% error): 1,972,498 (8.7%) + Close (15-30% error): 2,186,020 (9.6%) + Wrong decade: 5,366,189 (23.7%) + Noise: 13,158,609 (58.0%) + +================================================== +ACCURATE (within 15% of actual price) (n=1,972,498) +================================================== + +Output count: {1: '86,885 (4.4%)', 2: '1,239,193 (62.8%)', 3: '40,624 (2.1%)', 4: '21,823 (1.1%)', 5: '583,973 (29.6%)'} +Input count: {1: '1,459,589 (74.0%)', 2: '201,244 (10.2%)', 3: '78,053 (4.0%)', 4: '44,605 (2.3%)', 5: '189,007 (9.6%)'} +Output type: {'p2pkh': '1,590,090 (80.6%)', 'p2sh': '380,942 (19.3%)', 'p2wpkh': '10 (0.0%)', 'p2wsh': '1,456 (0.1%)'} +Is round BTC: {False: '1,930,212 (97.9%)', True: '42,286 (2.1%)'} +Both outputs round: {False: '1,972,067 (100.0%)', True: '431 (0.0%)'} +Same-day spend: {False: '490,906 (24.9%)', True: '1,481,592 (75.1%)'} +Has OP_RETURN: {False: '1,965,588 (99.6%)', True: '6,910 (0.4%)'} +Witness size: {'0': '1,796,912 (91.1%)', '1-499': '136,317 (6.9%)', '1000-2499': '12,813 (0.6%)', '2500+': '10,281 (0.5%)', '500-999': '16,175 (0.8%)'} +Value range (sats): {'100M+': '223,595 (11.3%)', '100k-1M': '579,993 (29.4%)', '10M-100M': '362,186 (18.4%)', '10k-100k': '121,053 (6.1%)', '1M-10M': '659,831 (33.5%)', '<10k': '25,840 (1.3%)'} +Decade (10^N sats): {3: '25,840 (1.3%)', 4: '121,053 (6.1%)', 5: '579,993 (29.4%)', 6: '659,831 (33.5%)', 7: '362,186 (18.4%)', 8: '133,290 (6.8%)', 9: '80,107 (4.1%)', 10: '9,994 (0.5%)', 11: '202 (0.0%)', 12: '2 (0.0%)'} +Implied USD value: {'$1-$10': '309,845 (15.7%)', '$10-$100': '641,388 (32.5%)', '$100-$1k': '517,139 (26.2%)', '$10k+': '156,536 (7.9%)', '$1k-$10k': '265,579 (13.5%)', '<$1': '82,011 (4.2%)'} +Output index (2-out only): {0: '648,234 (32.9%)', 1: '590,959 (30.0%)'} +Is smaller output (2-out): {False: '619,776 (31.4%)', True: '619,417 (31.4%)'} +Round pattern (2-out): {'both_round': '431 (0.0%)', 'neither_round': '1,151,181 (58.4%)', 'only_other_round': '62,296 (3.2%)', 'only_this_round': '25,285 (1.3%)'} +Value ratio (2-out): {'10-30%': '162,055 (8.2%)', '30-50%': '100,175 (5.1%)', '50-70%': '126,721 (6.4%)', '70-90%': '130,654 (6.6%)', '<10%': '357,187 (18.1%)', '>90%': '362,401 (18.4%)'} +Error from actual price: {'<5%': '1,972,498 (100.0%)'} +Tx total value: {'0.01-0.1 BTC': '445,934 (22.6%)', '0.1-1 BTC': '466,154 (23.6%)', '1-10 BTC': '398,467 (20.2%)', '10+ BTC': '427,670 (21.7%)', '<0.01 BTC': '234,273 (11.9%)'} +Round USD (10% tol): {False: '36,038 (1.8%)', True: '1,936,460 (98.2%)'} +Round USD (5% tol): {False: '78,613 (4.0%)', True: '1,893,885 (96.0%)'} +Round USD (2% tol): {False: '641,456 (32.5%)', True: '1,331,042 (67.5%)'} +Round USD (1% tol): {False: '848,109 (43.0%)', True: '1,124,389 (57.0%)'} +Tx pattern: {'1-to-1': '60,724 (3.1%)', '1-to-2': '1,002,378 (50.8%)', '1-to-many': '396,487 (20.1%)', '2-to-1': '9,734 (0.5%)', '2-to-2': '138,877 (7.0%)', '2-to-many': '52,633 (2.7%)', 'many-to-1': '16,427 (0.8%)', 'many-to-2': '97,938 (5.0%)', 'many-to-many': '197,300 (10.0%)'} +Value similarity (2-out): {'different': '204,583 (10.4%)', 'moderate': '120,997 (6.1%)', 'nearly_equal': '20,991 (1.1%)', 'similar': '43,661 (2.2%)', 'very_different': '847,014 (42.9%)'} +Micro-round sats: {False: '1,803,587 (91.4%)', True: '168,911 (8.6%)'} +Phase USD (1% tol): {False: '1,767,879 (89.6%)', True: '204,619 (10.4%)'} +Phase USD (2% tol): {False: '1,574,587 (79.8%)', True: '397,911 (20.2%)'} +Phase USD (5% tol): {False: '404,735 (20.5%)', True: '1,567,763 (79.5%)'} +Phase USD (10% tol): {True: '1,972,498 (100.0%)'} + +Top 10 bins: + Bin 24: 204,957 (10.4%) + Bin 23: 199,778 (10.1%) + Bin 25: 196,723 (10.0%) + Bin 22: 159,128 (8.1%) + Bin 26: 128,143 (6.5%) + Bin 21: 122,925 (6.2%) + Bin 27: 102,683 (5.2%) + Bin 36: 90,122 (4.6%) + Bin 30: 86,471 (4.4%) + Bin 20: 83,579 (4.2%) + +================================================== +NOISE (no decade matches) (n=13,158,609) +================================================== + +Output count: {1: '634,898 (4.8%)', 2: '8,340,358 (63.4%)', 3: '330,635 (2.5%)', 4: '202,245 (1.5%)', 5: '3,650,473 (27.7%)'} +Input count: {1: '9,660,188 (73.4%)', 2: '1,356,349 (10.3%)', 3: '558,819 (4.2%)', 4: '326,914 (2.5%)', 5: '1,256,339 (9.5%)'} +Output type: {'opreturn': '56 (0.0%)', 'p2ms': '61 (0.0%)', 'p2pk33': '32,988 (0.3%)', 'p2pkh': '10,406,343 (79.1%)', 'p2sh': '2,702,826 (20.5%)', 'p2wpkh': '1,014 (0.0%)', 'p2wsh': '15,321 (0.1%)'} +Is round BTC: {False: '12,278,850 (93.3%)', True: '879,759 (6.7%)'} +Both outputs round: {False: '13,145,480 (99.9%)', True: '13,129 (0.1%)'} +Same-day spend: {False: '3,320,900 (25.2%)', True: '9,837,709 (74.8%)'} +Has OP_RETURN: {False: '12,996,391 (98.8%)', True: '162,218 (1.2%)'} +Witness size: {'0': '11,852,338 (90.1%)', '1-499': '1,067,936 (8.1%)', '1000-2499': '77,828 (0.6%)', '2500+': '53,724 (0.4%)', '500-999': '106,783 (0.8%)'} +Value range (sats): {'100M+': '1,458,723 (11.1%)', '100k-1M': '3,726,699 (28.3%)', '10M-100M': '2,273,584 (17.3%)', '10k-100k': '1,291,967 (9.8%)', '1M-10M': '4,047,434 (30.8%)', '<10k': '360,202 (2.7%)'} +Decade (10^N sats): {3: '360,202 (2.7%)', 4: '1,291,967 (9.8%)', 5: '3,726,699 (28.3%)', 6: '4,047,434 (30.8%)', 7: '2,273,584 (17.3%)', 8: '941,140 (7.2%)', 9: '448,997 (3.4%)', 10: '66,674 (0.5%)', 11: '1,898 (0.0%)', 12: '14 (0.0%)'} +Implied USD value: {'$1-$10': '1,763,099 (13.4%)', '$10-$100': '4,330,099 (32.9%)', '$100-$1k': '3,453,922 (26.2%)', '$10k+': '1,110,055 (8.4%)', '$1k-$10k': '1,815,494 (13.8%)', '<$1': '685,940 (5.2%)'} +Output index (2-out only): {0: '4,116,639 (31.3%)', 1: '4,223,719 (32.1%)'} +Is smaller output (2-out): {False: '4,282,360 (32.5%)', True: '4,057,998 (30.8%)'} +Round pattern (2-out): {'both_round': '13,129 (0.1%)', 'neither_round': '7,217,919 (54.9%)', 'only_other_round': '518,569 (3.9%)', 'only_this_round': '590,741 (4.5%)'} +Value ratio (2-out): {'10-30%': '1,057,500 (8.0%)', '30-50%': '725,518 (5.5%)', '50-70%': '753,326 (5.7%)', '70-90%': '1,078,632 (8.2%)', '<10%': '2,274,980 (17.3%)', '>90%': '2,450,402 (18.6%)'} +Error from actual price: {'15-25%': '1,785,215 (13.6%)', '25-50%': '5,366,801 (40.8%)', '50%+': '6,006,593 (45.6%)'} +Tx total value: {'0.01-0.1 BTC': '3,131,341 (23.8%)', '0.1-1 BTC': '2,904,803 (22.1%)', '1-10 BTC': '2,392,339 (18.2%)', '10+ BTC': '2,841,496 (21.6%)', '<0.01 BTC': '1,888,630 (14.4%)'} +Round USD (10% tol): {False: '9,092,089 (69.1%)', True: '4,066,520 (30.9%)'} +Round USD (5% tol): {False: '10,823,144 (82.3%)', True: '2,335,465 (17.7%)'} +Round USD (2% tol): {False: '11,685,428 (88.8%)', True: '1,473,181 (11.2%)'} +Round USD (1% tol): {False: '11,958,371 (90.9%)', True: '1,200,238 (9.1%)'} +Tx pattern: {'1-to-1': '445,567 (3.4%)', '1-to-2': '6,608,466 (50.2%)', '1-to-many': '2,606,155 (19.8%)', '2-to-1': '78,556 (0.6%)', '2-to-2': '934,581 (7.1%)', '2-to-many': '343,212 (2.6%)', 'many-to-1': '110,775 (0.8%)', 'many-to-2': '797,311 (6.1%)', 'many-to-many': '1,233,986 (9.4%)'} +Value similarity (2-out): {'different': '1,543,129 (11.7%)', 'moderate': '787,636 (6.0%)', 'nearly_equal': '125,325 (1.0%)', 'similar': '303,081 (2.3%)', 'very_different': '5,510,652 (41.9%)'} +Micro-round sats: {False: '11,900,046 (90.4%)', True: '1,258,563 (9.6%)'} +Phase USD (1% tol): {False: '9,073,255 (69.0%)', True: '4,085,354 (31.0%)'} +Phase USD (2% tol): {False: '6,841,108 (52.0%)', True: '6,317,501 (48.0%)'} +Phase USD (5% tol): {False: '1,716,086 (13.0%)', True: '11,442,523 (87.0%)'} +Phase USD (10% tol): {True: '13,158,609 (100.0%)'} + +Top 10 bins: + Bin 0: 576,291 (4.4%) + Bin 69: 500,680 (3.8%) + Bin 47: 406,717 (3.1%) + Bin 60: 289,204 (2.2%) + Bin 99: 237,582 (1.8%) + Bin 54: 233,270 (1.8%) + Bin 65: 209,383 (1.6%) + Bin 11: 207,138 (1.6%) + Bin 46: 200,753 (1.5%) + Bin 48: 200,584 (1.5%) + +================================================== +KEY DIFFERENCES (accurate vs noise): +================================================== + +Output count: + +Input count: + +Output type: + +Is round BTC: + False: 97.9% vs 93.3% (diff: +4.5%) + True: 2.1% vs 6.7% (diff: -4.5%) + +Both round: + +Same-day spend: + +OP_RETURN: + +Witness size: + +Value range (sats): + 10k-100k: 6.1% vs 9.8% (diff: -3.7%) + 1M-10M: 33.5% vs 30.8% (diff: +2.7%) + +Decade (10^N sats): + 4: 6.1% vs 9.8% (diff: -3.7%) + 6: 33.5% vs 30.8% (diff: +2.7%) + +Implied USD: + $1-$10: 15.7% vs 13.4% (diff: +2.3%) + +Output index (2-out): + 1: 30.0% vs 32.1% (diff: -2.1%) + +Is smaller (2-out): + +Round pattern (2-out): + neither_round: 58.4% vs 54.9% (diff: +3.5%) + only_this_round: 1.3% vs 4.5% (diff: -3.2%) + +Value ratio (2-out): + +Error from price: + 15-25%: 0.0% vs 13.6% (diff: -13.6%) + 25-50%: 0.0% vs 40.8% (diff: -40.8%) + 50%+: 0.0% vs 45.6% (diff: -45.6%) + <5%: 100.0% vs 0.0% (diff: +100.0%) + +Tx total value: + 1-10 BTC: 20.2% vs 18.2% (diff: +2.0%) + <0.01 BTC: 11.9% vs 14.4% (diff: -2.5%) + +Round USD (10%): + False: 1.8% vs 69.1% (diff: -67.3%) + True: 98.2% vs 30.9% (diff: +67.3%) + +Round USD (5%): + False: 4.0% vs 82.3% (diff: -78.3%) + True: 96.0% vs 17.7% (diff: +78.3%) + +Round USD (2%): + False: 32.5% vs 88.8% (diff: -56.3%) + True: 67.5% vs 11.2% (diff: +56.3%) + +Round USD (1%): + False: 43.0% vs 90.9% (diff: -47.9%) + True: 57.0% vs 9.1% (diff: +47.9%) + +Phase USD (1%): + False: 89.6% vs 69.0% (diff: +20.7%) + True: 10.4% vs 31.0% (diff: -20.7%) + +Phase USD (2%): + False: 79.8% vs 52.0% (diff: +27.8%) + True: 20.2% vs 48.0% (diff: -27.8%) + +Phase USD (5%): + False: 20.5% vs 13.0% (diff: +7.5%) + True: 79.5% vs 87.0% (diff: -7.5%) + +Phase USD (10%): + +Tx pattern: + +Value similarity (2-out): + +Micro-round sats: + +================================================== +EXCLUSION CANDIDATES (overrepresented in noise): +================================================== +Characteristics where noise% > accurate% suggest exclusion filters: + + EXCLUDE Round USD 5%=False: noise 82.3% vs accurate 4.0% (+78.3%) + EXCLUDE Round USD 10%=False: noise 69.1% vs accurate 1.8% (+67.3%) + EXCLUDE Round USD 2%=False: noise 88.8% vs accurate 32.5% (+56.3%) + EXCLUDE Round USD 1%=False: noise 90.9% vs accurate 43.0% (+47.9%) + EXCLUDE Phase USD 2%=True: noise 48.0% vs accurate 20.2% (+27.8%) + EXCLUDE Phase USD 1%=True: noise 31.0% vs accurate 10.4% (+20.7%) + EXCLUDE Phase USD 5%=True: noise 87.0% vs accurate 79.5% (+7.5%) + EXCLUDE Is round BTC=True: noise 6.7% vs accurate 2.1% (+4.5%) + EXCLUDE Value range=10k-100k: noise 9.8% vs accurate 6.1% (+3.7%) + EXCLUDE Decade=4: noise 9.8% vs accurate 6.1% (+3.7%) + +================================================== +INCLUSION SIGNALS (overrepresented in accurate): +================================================== +Characteristics where accurate% > noise% are good signals: + + KEEP Round USD 5%=True: accurate 96.0% vs noise 17.7% (+78.3%) + KEEP Round USD 10%=True: accurate 98.2% vs noise 30.9% (+67.3%) + KEEP Round USD 2%=True: accurate 67.5% vs noise 11.2% (+56.3%) + KEEP Round USD 1%=True: accurate 57.0% vs noise 9.1% (+47.9%) + KEEP Phase USD 2%=False: accurate 79.8% vs noise 52.0% (+27.8%) + KEEP Phase USD 1%=False: accurate 89.6% vs noise 69.0% (+20.7%) + KEEP Phase USD 5%=False: accurate 20.5% vs noise 13.0% (+7.5%) + KEEP Is round BTC=False: accurate 97.9% vs noise 93.3% (+4.5%) + + +############################################################ +# 2017-11 +############################################################ +Date range: 2017-11-01 to 2017-11-30 (dateindex 3219-3249) + +--- SUMMARY --- +Total outputs analyzed: 24,697,882 + Accurate (≤15% error): 2,542,111 (10.3%) + Close (15-30% error): 2,205,473 (8.9%) + Wrong decade: 5,469,629 (22.1%) + Noise: 14,480,669 (58.6%) + +================================================== +ACCURATE (within 15% of actual price) (n=2,542,111) +================================================== + +Output count: {1: '99,848 (3.9%)', 2: '1,610,319 (63.3%)', 3: '50,632 (2.0%)', 4: '29,842 (1.2%)', 5: '751,470 (29.6%)'} +Input count: {1: '1,802,245 (70.9%)', 2: '280,852 (11.0%)', 3: '113,582 (4.5%)', 4: '61,855 (2.4%)', 5: '283,577 (11.2%)'} +Output type: {'opreturn': '2 (0.0%)', 'p2pk33': '3 (0.0%)', 'p2pkh': '2,040,267 (80.3%)', 'p2sh': '499,251 (19.6%)', 'p2wpkh': '210 (0.0%)', 'p2wsh': '2,378 (0.1%)'} +Is round BTC: {False: '2,357,621 (92.7%)', True: '184,490 (7.3%)'} +Both outputs round: {False: '2,539,572 (99.9%)', True: '2,539 (0.1%)'} +Same-day spend: {False: '670,240 (26.4%)', True: '1,871,871 (73.6%)'} +Has OP_RETURN: {False: '2,531,384 (99.6%)', True: '10,727 (0.4%)'} +Witness size: {'0': '2,233,219 (87.8%)', '1-499': '226,843 (8.9%)', '1000-2499': '24,666 (1.0%)', '2500+': '23,060 (0.9%)', '500-999': '34,323 (1.4%)'} +Value range (sats): {'100M+': '292,363 (11.5%)', '100k-1M': '591,375 (23.3%)', '10M-100M': '506,028 (19.9%)', '10k-100k': '198,401 (7.8%)', '1M-10M': '919,954 (36.2%)', '<10k': '33,990 (1.3%)'} +Decade (10^N sats): {3: '33,990 (1.3%)', 4: '198,401 (7.8%)', 5: '591,375 (23.3%)', 6: '919,954 (36.2%)', 7: '506,028 (19.9%)', 8: '185,268 (7.3%)', 9: '91,857 (3.6%)', 10: '12,682 (0.5%)', 11: '2,474 (0.1%)', 12: '82 (0.0%)'} +Implied USD value: {'$1-$10': '287,899 (11.3%)', '$10-$100': '830,003 (32.7%)', '$100-$1k': '710,528 (28.0%)', '$10k+': '199,406 (7.8%)', '$1k-$10k': '357,882 (14.1%)', '<$1': '156,393 (6.2%)'} +Output index (2-out only): {0: '840,385 (33.1%)', 1: '769,934 (30.3%)'} +Is smaller output (2-out): {False: '759,833 (29.9%)', True: '850,486 (33.5%)'} +Round pattern (2-out): {'both_round': '2,539 (0.1%)', 'neither_round': '1,380,322 (54.3%)', 'only_other_round': '88,333 (3.5%)', 'only_this_round': '139,125 (5.5%)'} +Value ratio (2-out): {'10-30%': '221,527 (8.7%)', '30-50%': '144,942 (5.7%)', '50-70%': '170,883 (6.7%)', '70-90%': '170,726 (6.7%)', '<10%': '484,017 (19.0%)', '>90%': '418,224 (16.5%)'} +Error from actual price: {'<5%': '2,542,111 (100.0%)'} +Tx total value: {'0.01-0.1 BTC': '575,870 (22.7%)', '0.1-1 BTC': '606,112 (23.8%)', '1-10 BTC': '513,350 (20.2%)', '10+ BTC': '547,964 (21.6%)', '<0.01 BTC': '298,815 (11.8%)'} +Round USD (10% tol): {False: '44,944 (1.8%)', True: '2,497,167 (98.2%)'} +Round USD (5% tol): {False: '141,111 (5.6%)', True: '2,401,000 (94.4%)'} +Round USD (2% tol): {False: '767,619 (30.2%)', True: '1,774,492 (69.8%)'} +Round USD (1% tol): {False: '972,503 (38.3%)', True: '1,569,608 (61.7%)'} +Tx pattern: {'1-to-1': '70,315 (2.8%)', '1-to-2': '1,281,989 (50.4%)', '1-to-many': '449,941 (17.7%)', '2-to-1': '13,204 (0.5%)', '2-to-2': '186,938 (7.4%)', '2-to-many': '80,710 (3.2%)', 'many-to-1': '16,329 (0.6%)', 'many-to-2': '141,392 (5.6%)', 'many-to-many': '301,293 (11.9%)'} +Value similarity (2-out): {'different': '266,902 (10.5%)', 'moderate': '164,146 (6.5%)', 'nearly_equal': '32,001 (1.3%)', 'similar': '69,806 (2.7%)', 'very_different': '1,074,182 (42.3%)'} +Micro-round sats: {False: '2,315,925 (91.1%)', True: '226,186 (8.9%)'} +Phase USD (1% tol): {False: '1,837,920 (72.3%)', True: '704,191 (27.7%)'} +Phase USD (2% tol): {False: '1,501,556 (59.1%)', True: '1,040,555 (40.9%)'} +Phase USD (5% tol): {False: '771,633 (30.4%)', True: '1,770,478 (69.6%)'} +Phase USD (10% tol): {True: '2,542,111 (100.0%)'} + +Top 10 bins: + Bin 0: 247,254 (9.7%) + Bin 14: 171,453 (6.7%) + Bin 11: 169,280 (6.7%) + Bin 17: 148,628 (5.8%) + Bin 13: 137,771 (5.4%) + Bin 8: 132,568 (5.2%) + Bin 12: 130,313 (5.1%) + Bin 15: 121,853 (4.8%) + Bin 10: 111,277 (4.4%) + Bin 16: 109,652 (4.3%) + +================================================== +NOISE (no decade matches) (n=14,480,669) +================================================== + +Output count: {1: '626,731 (4.3%)', 2: '8,820,015 (60.9%)', 3: '334,586 (2.3%)', 4: '185,591 (1.3%)', 5: '4,513,746 (31.2%)'} +Input count: {1: '10,232,105 (70.7%)', 2: '1,599,553 (11.0%)', 3: '654,747 (4.5%)', 4: '359,445 (2.5%)', 5: '1,634,819 (11.3%)'} +Output type: {'opreturn': '12 (0.0%)', 'p2ms': '323 (0.0%)', 'p2pk33': '1,316 (0.0%)', 'p2pk65': '1 (0.0%)', 'p2pkh': '11,543,608 (79.7%)', 'p2sh': '2,912,876 (20.1%)', 'p2wpkh': '2,048 (0.0%)', 'p2wsh': '20,485 (0.1%)'} +Is round BTC: {False: '13,402,274 (92.6%)', True: '1,078,395 (7.4%)'} +Both outputs round: {False: '14,468,907 (99.9%)', True: '11,762 (0.1%)'} +Same-day spend: {False: '3,792,983 (26.2%)', True: '10,687,686 (73.8%)'} +Has OP_RETURN: {False: '14,276,450 (98.6%)', True: '204,219 (1.4%)'} +Witness size: {'0': '12,776,997 (88.2%)', '1-499': '1,304,678 (9.0%)', '1000-2499': '121,242 (0.8%)', '2500+': '96,470 (0.7%)', '500-999': '181,282 (1.3%)'} +Value range (sats): {'100M+': '1,555,989 (10.7%)', '100k-1M': '4,350,847 (30.0%)', '10M-100M': '2,395,420 (16.5%)', '10k-100k': '1,399,528 (9.7%)', '1M-10M': '4,348,755 (30.0%)', '<10k': '430,130 (3.0%)'} +Decade (10^N sats): {3: '430,130 (3.0%)', 4: '1,399,528 (9.7%)', 5: '4,350,847 (30.0%)', 6: '4,348,755 (30.0%)', 7: '2,395,420 (16.5%)', 8: '1,038,941 (7.2%)', 9: '426,124 (2.9%)', 10: '82,445 (0.6%)', 11: '8,241 (0.1%)', 12: '238 (0.0%)'} +Implied USD value: {'$1-$10': '1,344,553 (9.3%)', '$10-$100': '4,675,965 (32.3%)', '$100-$1k': '4,088,998 (28.2%)', '$10k+': '1,394,871 (9.6%)', '$1k-$10k': '2,252,822 (15.6%)', '<$1': '723,460 (5.0%)'} +Output index (2-out only): {0: '4,336,546 (29.9%)', 1: '4,483,469 (31.0%)'} +Is smaller output (2-out): {False: '4,502,522 (31.1%)', True: '4,317,493 (29.8%)'} +Round pattern (2-out): {'both_round': '11,762 (0.1%)', 'neither_round': '7,435,856 (51.4%)', 'only_other_round': '631,974 (4.4%)', 'only_this_round': '740,423 (5.1%)'} +Value ratio (2-out): {'10-30%': '1,167,728 (8.1%)', '30-50%': '780,401 (5.4%)', '50-70%': '794,649 (5.5%)', '70-90%': '1,128,877 (7.8%)', '<10%': '2,369,364 (16.4%)', '>90%': '2,578,996 (17.8%)'} +Error from actual price: {'15-25%': '2,368,062 (16.4%)', '25-50%': '6,607,639 (45.6%)', '50%+': '5,504,968 (38.0%)'} +Tx total value: {'0.01-0.1 BTC': '3,299,969 (22.8%)', '0.1-1 BTC': '3,131,058 (21.6%)', '1-10 BTC': '2,854,502 (19.7%)', '10+ BTC': '3,215,935 (22.2%)', '<0.01 BTC': '1,979,205 (13.7%)'} +Round USD (10% tol): {False: '9,628,845 (66.5%)', True: '4,851,824 (33.5%)'} +Round USD (5% tol): {False: '11,280,033 (77.9%)', True: '3,200,636 (22.1%)'} +Round USD (2% tol): {False: '12,248,063 (84.6%)', True: '2,232,606 (15.4%)'} +Round USD (1% tol): {False: '12,564,577 (86.8%)', True: '1,916,092 (13.2%)'} +Tx pattern: {'1-to-1': '423,157 (2.9%)', '1-to-2': '6,885,667 (47.6%)', '1-to-many': '2,923,281 (20.2%)', '2-to-1': '87,060 (0.6%)', '2-to-2': '1,044,548 (7.2%)', '2-to-many': '467,945 (3.2%)', 'many-to-1': '116,514 (0.8%)', 'many-to-2': '889,800 (6.1%)', 'many-to-many': '1,642,697 (11.3%)'} +Value similarity (2-out): {'different': '1,628,124 (11.2%)', 'moderate': '836,376 (5.8%)', 'nearly_equal': '135,219 (0.9%)', 'similar': '323,269 (2.2%)', 'very_different': '5,794,120 (40.0%)'} +Micro-round sats: {False: '13,169,159 (90.9%)', True: '1,311,510 (9.1%)'} +Phase USD (1% tol): {False: '9,031,479 (62.4%)', True: '5,449,190 (37.6%)'} +Phase USD (2% tol): {False: '5,948,682 (41.1%)', True: '8,531,987 (58.9%)'} +Phase USD (5% tol): {False: '1,011,037 (7.0%)', True: '13,469,632 (93.0%)'} +Phase USD (10% tol): {True: '14,480,669 (100.0%)'} + +Top 10 bins: + Bin 0: 847,461 (5.9%) + Bin 30: 546,287 (3.8%) + Bin 47: 373,730 (2.6%) + Bin 99: 308,355 (2.1%) + Bin 39: 299,026 (2.1%) + Bin 43: 260,511 (1.8%) + Bin 38: 251,051 (1.7%) + Bin 95: 246,679 (1.7%) + Bin 29: 233,437 (1.6%) + Bin 32: 227,252 (1.6%) + +================================================== +KEY DIFFERENCES (accurate vs noise): +================================================== + +Output count: + 2: 63.3% vs 60.9% (diff: +2.4%) + +Input count: + +Output type: + +Is round BTC: + +Both round: + +Same-day spend: + +OP_RETURN: + +Witness size: + +Value range (sats): + 100k-1M: 23.3% vs 30.0% (diff: -6.8%) + 10M-100M: 19.9% vs 16.5% (diff: +3.4%) + 1M-10M: 36.2% vs 30.0% (diff: +6.2%) + +Decade (10^N sats): + 5: 23.3% vs 30.0% (diff: -6.8%) + 6: 36.2% vs 30.0% (diff: +6.2%) + 7: 19.9% vs 16.5% (diff: +3.4%) + +Implied USD: + $1-$10: 11.3% vs 9.3% (diff: +2.0%) + +Output index (2-out): + 0: 33.1% vs 29.9% (diff: +3.1%) + +Is smaller (2-out): + True: 33.5% vs 29.8% (diff: +3.6%) + +Round pattern (2-out): + neither_round: 54.3% vs 51.4% (diff: +2.9%) + +Value ratio (2-out): + <10%: 19.0% vs 16.4% (diff: +2.7%) + +Error from price: + 15-25%: 0.0% vs 16.4% (diff: -16.4%) + 25-50%: 0.0% vs 45.6% (diff: -45.6%) + 50%+: 0.0% vs 38.0% (diff: -38.0%) + <5%: 100.0% vs 0.0% (diff: +100.0%) + +Tx total value: + 0.1-1 BTC: 23.8% vs 21.6% (diff: +2.2%) + +Round USD (10%): + False: 1.8% vs 66.5% (diff: -64.7%) + True: 98.2% vs 33.5% (diff: +64.7%) + +Round USD (5%): + False: 5.6% vs 77.9% (diff: -72.3%) + True: 94.4% vs 22.1% (diff: +72.3%) + +Round USD (2%): + False: 30.2% vs 84.6% (diff: -54.4%) + True: 69.8% vs 15.4% (diff: +54.4%) + +Round USD (1%): + False: 38.3% vs 86.8% (diff: -48.5%) + True: 61.7% vs 13.2% (diff: +48.5%) + +Phase USD (1%): + False: 72.3% vs 62.4% (diff: +9.9%) + True: 27.7% vs 37.6% (diff: -9.9%) + +Phase USD (2%): + False: 59.1% vs 41.1% (diff: +18.0%) + True: 40.9% vs 58.9% (diff: -18.0%) + +Phase USD (5%): + False: 30.4% vs 7.0% (diff: +23.4%) + True: 69.6% vs 93.0% (diff: -23.4%) + +Phase USD (10%): + +Tx pattern: + 1-to-2: 50.4% vs 47.6% (diff: +2.9%) + 1-to-many: 17.7% vs 20.2% (diff: -2.5%) + +Value similarity (2-out): + very_different: 42.3% vs 40.0% (diff: +2.2%) + +Micro-round sats: + +================================================== +EXCLUSION CANDIDATES (overrepresented in noise): +================================================== +Characteristics where noise% > accurate% suggest exclusion filters: + + EXCLUDE Round USD 5%=False: noise 77.9% vs accurate 5.6% (+72.3%) + EXCLUDE Round USD 10%=False: noise 66.5% vs accurate 1.8% (+64.7%) + EXCLUDE Round USD 2%=False: noise 84.6% vs accurate 30.2% (+54.4%) + EXCLUDE Round USD 1%=False: noise 86.8% vs accurate 38.3% (+48.5%) + EXCLUDE Phase USD 5%=True: noise 93.0% vs accurate 69.6% (+23.4%) + EXCLUDE Phase USD 2%=True: noise 58.9% vs accurate 40.9% (+18.0%) + EXCLUDE Phase USD 1%=True: noise 37.6% vs accurate 27.7% (+9.9%) + EXCLUDE Value range=100k-1M: noise 30.0% vs accurate 23.3% (+6.8%) + EXCLUDE Decade=5: noise 30.0% vs accurate 23.3% (+6.8%) + +================================================== +INCLUSION SIGNALS (overrepresented in accurate): +================================================== +Characteristics where accurate% > noise% are good signals: + + KEEP Round USD 5%=True: accurate 94.4% vs noise 22.1% (+72.3%) + KEEP Round USD 10%=True: accurate 98.2% vs noise 33.5% (+64.7%) + KEEP Round USD 2%=True: accurate 69.8% vs noise 15.4% (+54.4%) + KEEP Round USD 1%=True: accurate 61.7% vs noise 13.2% (+48.5%) + KEEP Phase USD 5%=False: accurate 30.4% vs noise 7.0% (+23.4%) + KEEP Phase USD 2%=False: accurate 59.1% vs noise 41.1% (+18.0%) + KEEP Phase USD 1%=False: accurate 72.3% vs noise 62.4% (+9.9%) + KEEP Value range=1M-10M: accurate 36.2% vs noise 30.0% (+6.2%) + KEEP Decade=6: accurate 36.2% vs noise 30.0% (+6.2%) + KEEP Is smaller (2-out)=True: accurate 33.5% vs noise 29.8% (+3.6%) + KEEP Value range=10M-100M: accurate 19.9% vs noise 16.5% (+3.4%) + KEEP Decade=7: accurate 19.9% vs noise 16.5% (+3.4%) + + +############################################################ +# 2017-12 +############################################################ +Date range: 2017-12-01 to 2017-12-31 (dateindex 3249-3280) diff --git a/research/test_phase_detection.py b/research/test_phase_detection.py new file mode 100644 index 000000000..f4603de57 --- /dev/null +++ b/research/test_phase_detection.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python3 +""" +Test price phase detection from outputs alone. +The idea: Round USD outputs create a fingerprint pattern that reveals the price phase. +""" + +import math +import http.client +import json +import time +from collections import defaultdict + +API_HOST = "localhost" +API_PORT = 3110 + +# Round USD phases (fixed fingerprint) +# These are frac(log10(usd_cents)) for round USD values +ROUND_USD_PHASES = [ + 0.00, # $1, $10, $100, $1000 + 0.18, # $1.50, $15, $150 + 0.30, # $2, $20, $200 + 0.40, # $2.50, $25, $250 + 0.48, # $3, $30, $300 + 0.60, # $4, $40, $400 + 0.70, # $5, $50, $500 + 0.78, # $6, $60, $600 + 0.85, # $7, $70, $700 + 0.90, # $8, $80, $800 + 0.95, # $9, $90, $900 +] + +_conn = None + +def get_conn(): + global _conn + if _conn is None: + _conn = http.client.HTTPConnection(API_HOST, API_PORT, timeout=300) + return _conn + +def reset_conn(): + global _conn + if _conn: + try: + _conn.close() + except: + pass + _conn = None + +def fetch(path: str, retries: int = 3): + for attempt in range(retries): + try: + conn = get_conn() + conn.request("GET", path) + resp = conn.getresponse() + data = resp.read().decode('utf-8') + return json.loads(data) + except Exception as e: + reset_conn() + if attempt < retries - 1: + time.sleep(2) + else: + raise + +def fetch_chunked(path_template: str, start: int, end: int, chunk_size: int = 25000) -> list: + result = [] + for chunk_start in range(start, end, chunk_size): + chunk_end = min(chunk_start + chunk_size, end) + path = path_template.format(start=chunk_start, end=chunk_end) + data = fetch(path)["data"] + result.extend(data) + return result + + +def get_sats_phase(sats: int) -> float: + """Get the phase (fractional part of log10) for a sats value.""" + if sats <= 0: + return 0.0 + return math.log10(sats) % 1.0 + + +def count_round_usd_matches(outputs: list, price_phase: float, tolerance: float = 0.02) -> int: + """ + Count how many outputs match round USD bins at the given price phase. + + At price_phase P, round USD outputs should appear at sats_phase = (usd_phase - P) mod 1 + """ + # Compute expected sats phases for round USD at this price phase + expected_phases = [(usd_phase - price_phase) % 1.0 for usd_phase in ROUND_USD_PHASES] + + count = 0 + for sats in outputs: + if sats is None or sats < 1000: + continue + sats_phase = get_sats_phase(sats) + + # Check if sats_phase matches any expected phase + for exp_phase in expected_phases: + diff = abs(sats_phase - exp_phase) + # Handle wraparound (0.99 is close to 0.01) + if diff < tolerance or diff > (1.0 - tolerance): + count += 1 + break + + return count + + +def find_best_price_phase(outputs: list, tolerance: float = 0.02, resolution: int = 100) -> tuple: + """ + Find the price phase that maximizes round USD matches. + Returns (best_phase, best_count, all_counts). + """ + counts = [] + best_phase = 0.0 + best_count = 0 + + for i in range(resolution): + price_phase = i / resolution + count = count_round_usd_matches(outputs, price_phase, tolerance) + counts.append(count) + + if count > best_count: + best_count = count + best_phase = price_phase + + return best_phase, best_count, counts + + +def actual_price_phase(price: float) -> float: + """Get the actual price phase from a price.""" + return math.log10(price) % 1.0 + + +def analyze_day(date_str: str, start_height: int, end_height: int, actual_price: float): + """Analyze a single day's outputs.""" + + # Get transaction range for these heights + first_tx = fetch(f"/api/metric/first_txindex/height?start={start_height}&end={end_height}") + first_txs = first_tx["data"] + if not first_txs or len(first_txs) < 2: + return None + + tx_start = first_txs[0] + tx_end = first_txs[-1] + + # Get output range + tx_first_out = fetch_chunked("/api/metric/first_txoutindex/txindex?start={start}&end={end}", tx_start, tx_end) + if not tx_first_out: + return None + + out_start = tx_first_out[0] + out_end = tx_first_out[-1] + 10 # estimate + + # Fetch output values + out_values = fetch_chunked("/api/metric/value/txoutindex?start={start}&end={end}", out_start, out_end) + + # Filter to reasonable range (1000 sats to 100 BTC) + outputs = [v for v in out_values if v and 1000 <= v <= 10_000_000_000] + + if len(outputs) < 1000: + return None + + # Find best price phase + detected_phase, match_count, _ = find_best_price_phase(outputs, tolerance=0.02) + + # Compare with actual + actual_phase = actual_price_phase(actual_price) + + # Phase error (handle wraparound) + phase_error = abs(detected_phase - actual_phase) + if phase_error > 0.5: + phase_error = 1.0 - phase_error + + return { + 'date': date_str, + 'actual_price': actual_price, + 'actual_phase': actual_phase, + 'detected_phase': detected_phase, + 'phase_error': phase_error, + 'match_count': match_count, + 'total_outputs': len(outputs), + 'match_pct': 100 * match_count / len(outputs), + } + + +def main(): + print("=" * 60) + print("PRICE PHASE DETECTION TEST") + print("=" * 60) + print("\nIdea: Round USD outputs form a fingerprint pattern.") + print("Sliding this pattern across the histogram reveals the price phase.\n") + + # Fetch dates + print("Fetching date index...") + dates = fetch("/api/metric/date/dateindex?start=0&end=4000")["data"] + + # Fetch daily OHLC + print("Fetching daily prices...") + ohlc_data = fetch("/api/metric/price_ohlc/dateindex?start=2800&end=3600")["data"] + + # Fetch heights + print("Fetching heights...") + heights = fetch("/api/metric/first_height/dateindex?start=2800&end=3600")["data"] + + results = [] + + # Test on 2017-2018 (roughly dateindex 2900-3600) + # Sample every 7 days to speed up + for di in range(2900, 3550, 7): + if di - 2800 >= len(ohlc_data) or di - 2800 >= len(heights): + continue + + ohlc = ohlc_data[di - 2800] + if not ohlc or len(ohlc) < 4: + continue + + # Use close price as "actual" + actual_price = ohlc[3] + if not actual_price or actual_price <= 0: + continue + + date_str = dates[di] if di < len(dates) else f"di={di}" + + start_height = heights[di - 2800] + end_height = heights[di - 2800 + 1] if di - 2800 + 1 < len(heights) else start_height + 144 + + if not start_height: + continue + + print(f"\nAnalyzing {date_str} (${actual_price:.0f})...") + + try: + result = analyze_day(date_str, start_height, end_height, actual_price) + if result: + results.append(result) + print(f" Actual phase: {result['actual_phase']:.3f}") + print(f" Detected phase: {result['detected_phase']:.3f}") + print(f" Phase error: {result['phase_error']:.3f} ({result['phase_error']*100:.1f}%)") + print(f" Matches: {result['match_count']:,} / {result['total_outputs']:,} ({result['match_pct']:.1f}%)") + except Exception as e: + print(f" Error: {e}") + continue + + # Summary + if results: + print("\n" + "=" * 60) + print("SUMMARY") + print("=" * 60) + + errors = [r['phase_error'] for r in results] + avg_error = sum(errors) / len(errors) + + # Count how many are within various thresholds + within_01 = sum(1 for e in errors if e <= 0.01) + within_02 = sum(1 for e in errors if e <= 0.02) + within_05 = sum(1 for e in errors if e <= 0.05) + within_10 = sum(1 for e in errors if e <= 0.10) + + print(f"\nTotal days analyzed: {len(results)}") + print(f"Average phase error: {avg_error:.3f} ({avg_error*100:.1f}%)") + print(f"\nPhase error distribution:") + print(f" ≤1%: {within_01:3d} / {len(results)} ({100*within_01/len(results):.0f}%)") + print(f" ≤2%: {within_02:3d} / {len(results)} ({100*within_02/len(results):.0f}%)") + print(f" ≤5%: {within_05:3d} / {len(results)} ({100*within_05/len(results):.0f}%)") + print(f" ≤10%: {within_10:3d} / {len(results)} ({100*within_10/len(results):.0f}%)") + + # Show worst cases + print(f"\nWorst cases:") + worst = sorted(results, key=lambda r: -r['phase_error'])[:5] + for r in worst: + print(f" {r['date']}: detected {r['detected_phase']:.2f} vs actual {r['actual_phase']:.2f} " + f"(error {r['phase_error']:.2f}, ${r['actual_price']:.0f})") + + # Show best cases + print(f"\nBest cases:") + best = sorted(results, key=lambda r: r['phase_error'])[:5] + for r in best: + print(f" {r['date']}: detected {r['detected_phase']:.2f} vs actual {r['actual_phase']:.2f} " + f"(error {r['phase_error']:.3f}, ${r['actual_price']:.0f})") + + +if __name__ == "__main__": + main() diff --git a/website/scripts/options/market/index.js b/website/scripts/options/market/index.js index a987e03eb..070914438 100644 --- a/website/scripts/options/market/index.js +++ b/website/scripts/options/market/index.js @@ -37,24 +37,30 @@ export function createMarketSection(ctx) { title: "Bitcoin Price", ...(localhost && { top: [ - candlestick({ - metric: price.oracle.ohlcDollars, - name: "Oracle", - unit: Unit.usd, - colors: [colors.cyan, colors.purple], - }), + // candlestick({ + // metric: price.oracle.ohlcDollars, + // name: "Oracle base", + // unit: Unit.usd, + // colors: [colors.cyan, colors.purple], + // }), line({ metric: price.oracle.phaseDailyDollars.median, - name: "Oracle2 median", - unit: Unit.usd, - color: colors.blue, - }), - line({ - metric: price.oracle.phaseDailyDollars.average, - name: "Oracle2 average", + name: "o. p50", unit: Unit.usd, color: colors.yellow, }), + line({ + metric: price.oracle.phaseDailyDollars.max, + name: "o. max", + unit: Unit.usd, + color: colors.lime, + }), + line({ + metric: price.oracle.phaseDailyDollars.min, + name: "o. min", + unit: Unit.usd, + color: colors.rose, + }), ], }), },