mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-08-12 02:03:09 -07:00
brk: metric trimming part X + 4
This commit is contained in:
Generated
-4
@@ -1291,8 +1291,6 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
[[package]]
|
||||
name = "fjall"
|
||||
version = "3.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "420a84699b8ccbb1ed573e38e88f4f23637b45beab6432066452f834be469c57"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"byteview",
|
||||
@@ -2029,8 +2027,6 @@ checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
[[package]]
|
||||
name = "lsm-tree"
|
||||
version = "3.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "055a908d502129cf63bedae52f2db222e4436d2da32a69df9b84ac9fb9147761"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"byteview",
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ color-eyre = "0.6.5"
|
||||
corepc-jsonrpc = { package = "jsonrpc", version = "0.19.0", features = ["simple_http"], default-features = false }
|
||||
corepc-types = { version = "0.15.0", features = ["std"], default-features = false }
|
||||
derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] }
|
||||
fjall = "3.1.8"
|
||||
fjall = { path = "../fjall", version = "3.1.8" }
|
||||
indexmap = { version = "2.14.0", features = ["serde"] }
|
||||
jiff = { version = "0.2.34", features = ["perf-inline", "tz-system"], default-features = false }
|
||||
owo-colors = "4.3.0"
|
||||
|
||||
@@ -109,7 +109,8 @@ impl<'a> Ctx<'a> {
|
||||
for &name in BLOCK_FIELDS {
|
||||
obj.insert(
|
||||
name.into(),
|
||||
self.block_field(name, None, &[]).expect("known block field"),
|
||||
self.block_field(name, None, &[])
|
||||
.expect("known block field"),
|
||||
);
|
||||
}
|
||||
Value::Object(obj)
|
||||
|
||||
@@ -46,7 +46,7 @@ fn run() -> Result<()> {
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(2)
|
||||
/ 2)
|
||||
.max(1);
|
||||
.max(1);
|
||||
for block in reader.range_with(start, end, parser_threads)?.iter() {
|
||||
let block = block?;
|
||||
let line = formatter.format(&Ctx::new(&block, network))?;
|
||||
|
||||
@@ -16,9 +16,7 @@ impl Mode {
|
||||
));
|
||||
}
|
||||
if compact && n_fields == 0 {
|
||||
return Err(Error::Parse(
|
||||
"--compact requires at least one field".into(),
|
||||
));
|
||||
return Err(Error::Parse("--compact requires at least one field".into()));
|
||||
}
|
||||
Ok(if pretty {
|
||||
Self::Pretty
|
||||
|
||||
@@ -181,7 +181,8 @@ fn ex(cmd: &str, note: &str) {
|
||||
}
|
||||
|
||||
fn bold(s: &str) -> String {
|
||||
s.if_supports_color(Stream::Stdout, |t| t.bold()).to_string()
|
||||
s.if_supports_color(Stream::Stdout, |t| t.bold())
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn dim(s: &str) -> String {
|
||||
|
||||
+257
-206
@@ -1388,42 +1388,6 @@ impl CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern2 {
|
||||
pub cap: CentsDeltaToUsdPattern,
|
||||
pub capitalized: PricePattern,
|
||||
pub gross_pnl: BlockCumulativeSumPattern,
|
||||
pub loss: BlockCumulativeNegativeSumPattern,
|
||||
pub mvrv: SeriesPattern1<StoredF32>,
|
||||
pub net_pnl: BlockChangeCumulativeDeltaSumPattern,
|
||||
pub peak_regret: BlockCumulativeSumPattern,
|
||||
pub price: CentsPpmRatioSatsUsdPattern,
|
||||
pub profit: BlockCumulativeSumPattern,
|
||||
pub profit_to_loss_ratio: _1m1w1y24hPattern<StoredF64>,
|
||||
pub sell_side_risk_ratio: _1m1w1y24hPattern8,
|
||||
pub sopr: RatioValuePattern2,
|
||||
}
|
||||
|
||||
impl CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern2 {
|
||||
/// Create a new pattern node with accumulated series name.
|
||||
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
|
||||
Self {
|
||||
cap: CentsDeltaToUsdPattern::new(client.clone(), _m(&acc, "realized_cap")),
|
||||
capitalized: PricePattern::new(client.clone(), _m(&acc, "capitalized_price")),
|
||||
gross_pnl: BlockCumulativeSumPattern::new(client.clone(), _m(&acc, "realized_gross_pnl")),
|
||||
loss: BlockCumulativeNegativeSumPattern::new(client.clone(), _m(&acc, "realized_loss")),
|
||||
mvrv: SeriesPattern1::new(client.clone(), _m(&acc, "mvrv")),
|
||||
net_pnl: BlockChangeCumulativeDeltaSumPattern::new(client.clone(), _m(&acc, "net")),
|
||||
peak_regret: BlockCumulativeSumPattern::new(client.clone(), _m(&acc, "realized_peak_regret")),
|
||||
price: CentsPpmRatioSatsUsdPattern::new(client.clone(), _m(&acc, "realized_price")),
|
||||
profit: BlockCumulativeSumPattern::new(client.clone(), _m(&acc, "realized_profit")),
|
||||
profit_to_loss_ratio: _1m1w1y24hPattern::new(client.clone(), _m(&acc, "realized_profit_to_loss_ratio")),
|
||||
sell_side_risk_ratio: _1m1w1y24hPattern8::new(client.clone(), _m(&acc, "sell_side_risk_ratio")),
|
||||
sopr: RatioValuePattern2::new(client.clone(), acc.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct EmptyOpP2aP2msP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshUnknownPattern2 {
|
||||
pub empty: _1m1w1y24hPercentPpmRatioPattern,
|
||||
@@ -1877,32 +1841,6 @@ pub struct ActiveInputOutputSpendablePattern {
|
||||
pub spendable_output_to_reused_addr_share: _1m1w1y24hPercentPpmRatioPattern,
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2 {
|
||||
pub activity: CoindaysCoinyearsDormancyTransferPattern,
|
||||
pub cost_basis: InMaxMinPerSupplyPattern,
|
||||
pub invested_capital: InPattern,
|
||||
pub outputs: SpentUnspentPattern,
|
||||
pub realized: CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern2,
|
||||
pub supply: DeltaDominanceHalfInTotalPattern2,
|
||||
pub unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2,
|
||||
}
|
||||
|
||||
impl ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2 {
|
||||
/// Create a new pattern node with accumulated series name.
|
||||
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
|
||||
Self {
|
||||
activity: CoindaysCoinyearsDormancyTransferPattern::new(client.clone(), acc.clone()),
|
||||
cost_basis: InMaxMinPerSupplyPattern::new(client.clone(), acc.clone()),
|
||||
invested_capital: InPattern::new(client.clone(), _m(&acc, "invested_capital_in")),
|
||||
outputs: SpentUnspentPattern::new(client.clone(), acc.clone()),
|
||||
realized: CapCapitalizedGrossLossMvrvNetPeakPriceProfitSellSoprPattern2::new(client.clone(), acc.clone()),
|
||||
supply: DeltaDominanceHalfInTotalPattern2::new(client.clone(), _m(&acc, "supply")),
|
||||
unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2::new(client.clone(), acc.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct CapLossMvrvNetPriceProfitSoprPattern {
|
||||
pub cap: CentsDeltaUsdPattern,
|
||||
@@ -2033,30 +1971,6 @@ impl<T: DeserializeOwned> MaxMedianMinPct10Pct25Pct75Pct90Pattern<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct ActivityAddrOutputsRealizedSupplyUnrealizedPattern {
|
||||
pub activity: TransferPattern,
|
||||
pub addr_count: BaseDeltaPattern,
|
||||
pub outputs: SpentUnspentPattern,
|
||||
pub realized: CapLossMvrvPriceProfitPattern,
|
||||
pub supply: DeltaDominanceTotalPattern,
|
||||
pub unrealized: NuplPattern,
|
||||
}
|
||||
|
||||
impl ActivityAddrOutputsRealizedSupplyUnrealizedPattern {
|
||||
/// Create a new pattern node with accumulated series name.
|
||||
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
|
||||
Self {
|
||||
activity: TransferPattern::new(client.clone(), _m(&acc, "transfer_volume")),
|
||||
addr_count: BaseDeltaPattern::new(client.clone(), _m(&acc, "addr_count")),
|
||||
outputs: SpentUnspentPattern::new(client.clone(), acc.clone()),
|
||||
realized: CapLossMvrvPriceProfitPattern::new(client.clone(), acc.clone()),
|
||||
supply: DeltaDominanceTotalPattern::new(client.clone(), _m(&acc, "supply")),
|
||||
unrealized: NuplPattern::new(client.clone(), _m(&acc, "nupl")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct AverageBlockCumulativeInSumPattern {
|
||||
pub average: _1m1w1y24hPattern3,
|
||||
@@ -2241,6 +2155,28 @@ impl ActivityOutputsRealizedSupplyUnrealizedPattern {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct ActivityAddrOutputsRealizedSupplyPattern {
|
||||
pub activity: TransferPattern,
|
||||
pub addr_count: BaseDeltaPattern,
|
||||
pub outputs: UnspentPattern,
|
||||
pub realized: CapLossProfitPattern,
|
||||
pub supply: DeltaDominanceTotalPattern,
|
||||
}
|
||||
|
||||
impl ActivityAddrOutputsRealizedSupplyPattern {
|
||||
/// Create a new pattern node with accumulated series name.
|
||||
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
|
||||
Self {
|
||||
activity: TransferPattern::new(client.clone(), _m(&acc, "transfer_volume")),
|
||||
addr_count: BaseDeltaPattern::new(client.clone(), _m(&acc, "addr_count")),
|
||||
outputs: UnspentPattern::new(client.clone(), _m(&acc, "utxo_count")),
|
||||
realized: CapLossProfitPattern::new(client.clone(), _m(&acc, "realized")),
|
||||
supply: DeltaDominanceTotalPattern::new(client.clone(), _m(&acc, "supply")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct ActivityOutputsRealizedSupplyUnrealizedPattern3 {
|
||||
pub activity: TransferPattern,
|
||||
@@ -3013,6 +2949,24 @@ impl BlocksDominanceRewardsPattern {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct CapLossProfitPattern {
|
||||
pub cap: CentsDeltaUsdPattern,
|
||||
pub loss: BlockCumulativeSumPattern,
|
||||
pub profit: BlockCumulativeSumPattern,
|
||||
}
|
||||
|
||||
impl CapLossProfitPattern {
|
||||
/// Create a new pattern node with accumulated series name.
|
||||
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
|
||||
Self {
|
||||
cap: CentsDeltaUsdPattern::new(client.clone(), _m(&acc, "cap")),
|
||||
loss: BlockCumulativeSumPattern::new(client.clone(), _m(&acc, "loss")),
|
||||
profit: BlockCumulativeSumPattern::new(client.clone(), _m(&acc, "profit")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct CentsSatsUsdPattern3 {
|
||||
pub cents: SeriesPattern2<Cents>,
|
||||
@@ -3726,22 +3680,6 @@ impl PpmRatioPattern {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct RatioValuePattern2 {
|
||||
pub ratio: _1m1w1y24hPattern<StoredF64>,
|
||||
pub value_destroyed: AverageBlockCumulativeSumPattern<Cents>,
|
||||
}
|
||||
|
||||
impl RatioValuePattern2 {
|
||||
/// Create a new pattern node with accumulated series name.
|
||||
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
|
||||
Self {
|
||||
ratio: _1m1w1y24hPattern::new(client.clone(), _m(&acc, "sopr")),
|
||||
value_destroyed: AverageBlockCumulativeSumPattern::new(client.clone(), _m(&acc, "value_destroyed")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct RatioValuePattern {
|
||||
pub ratio: _24hPattern,
|
||||
@@ -3908,6 +3846,20 @@ impl TransferPattern {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern struct for repeated tree structure.
|
||||
pub struct UnspentPattern {
|
||||
pub unspent_count: BaseDeltaPattern,
|
||||
}
|
||||
|
||||
impl UnspentPattern {
|
||||
/// Create a new pattern node with accumulated series name.
|
||||
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
|
||||
Self {
|
||||
unspent_count: BaseDeltaPattern::new(client.clone(), acc.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Series tree
|
||||
|
||||
/// Series tree node.
|
||||
@@ -5952,8 +5904,10 @@ pub struct SeriesTree_Cointime_AgeRange {
|
||||
pub _3m_to_4m: CoindaysLivelinessRatioSupplyVaultednessPattern,
|
||||
pub _4m_to_5m: CoindaysLivelinessRatioSupplyVaultednessPattern,
|
||||
pub _5m_to_6m: CoindaysLivelinessRatioSupplyVaultednessPattern,
|
||||
pub _6m_to_1y: CoindaysLivelinessRatioSupplyVaultednessPattern,
|
||||
pub _1y_to_2y: CoindaysLivelinessRatioSupplyVaultednessPattern,
|
||||
pub _6m_to_9m: CoindaysLivelinessRatioSupplyVaultednessPattern,
|
||||
pub _9m_to_1y: CoindaysLivelinessRatioSupplyVaultednessPattern,
|
||||
pub _1y_to_18m: CoindaysLivelinessRatioSupplyVaultednessPattern,
|
||||
pub _18m_to_2y: CoindaysLivelinessRatioSupplyVaultednessPattern,
|
||||
pub _2y_to_3y: CoindaysLivelinessRatioSupplyVaultednessPattern,
|
||||
pub _3y_to_4y: CoindaysLivelinessRatioSupplyVaultednessPattern,
|
||||
pub _4y_to_5y: CoindaysLivelinessRatioSupplyVaultednessPattern,
|
||||
@@ -5978,8 +5932,10 @@ impl SeriesTree_Cointime_AgeRange {
|
||||
_3m_to_4m: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_3m_to_4m_old".to_string()),
|
||||
_4m_to_5m: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_4m_to_5m_old".to_string()),
|
||||
_5m_to_6m: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_5m_to_6m_old".to_string()),
|
||||
_6m_to_1y: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_6m_to_1y_old".to_string()),
|
||||
_1y_to_2y: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_1y_to_2y_old".to_string()),
|
||||
_6m_to_9m: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_6m_to_9m_old".to_string()),
|
||||
_9m_to_1y: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_9m_to_1y_old".to_string()),
|
||||
_1y_to_18m: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_1y_to_18m_old".to_string()),
|
||||
_18m_to_2y: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_18m_to_2y_old".to_string()),
|
||||
_2y_to_3y: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_2y_to_3y_old".to_string()),
|
||||
_3y_to_4y: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_3y_to_4y_old".to_string()),
|
||||
_4y_to_5y: CoindaysLivelinessRatioSupplyVaultednessPattern::new(client.clone(), "utxos_4y_to_5y_old".to_string()),
|
||||
@@ -6157,8 +6113,10 @@ pub struct SeriesTree_Coinflow_AgeRange {
|
||||
pub _3m_to_4m: MobilitySpendingSupplyPattern,
|
||||
pub _4m_to_5m: MobilitySpendingSupplyPattern,
|
||||
pub _5m_to_6m: MobilitySpendingSupplyPattern,
|
||||
pub _6m_to_1y: MobilitySpendingSupplyPattern,
|
||||
pub _1y_to_2y: MobilitySpendingSupplyPattern,
|
||||
pub _6m_to_9m: MobilitySpendingSupplyPattern,
|
||||
pub _9m_to_1y: MobilitySpendingSupplyPattern,
|
||||
pub _1y_to_18m: MobilitySpendingSupplyPattern,
|
||||
pub _18m_to_2y: MobilitySpendingSupplyPattern,
|
||||
pub _2y_to_3y: MobilitySpendingSupplyPattern,
|
||||
pub _3y_to_4y: MobilitySpendingSupplyPattern,
|
||||
pub _4y_to_5y: MobilitySpendingSupplyPattern,
|
||||
@@ -6183,8 +6141,10 @@ impl SeriesTree_Coinflow_AgeRange {
|
||||
_3m_to_4m: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_3m_to_4m_old".to_string()),
|
||||
_4m_to_5m: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_4m_to_5m_old".to_string()),
|
||||
_5m_to_6m: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_5m_to_6m_old".to_string()),
|
||||
_6m_to_1y: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_6m_to_1y_old".to_string()),
|
||||
_1y_to_2y: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_1y_to_2y_old".to_string()),
|
||||
_6m_to_9m: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_6m_to_9m_old".to_string()),
|
||||
_9m_to_1y: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_9m_to_1y_old".to_string()),
|
||||
_1y_to_18m: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_1y_to_18m_old".to_string()),
|
||||
_18m_to_2y: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_18m_to_2y_old".to_string()),
|
||||
_2y_to_3y: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_2y_to_3y_old".to_string()),
|
||||
_3y_to_4y: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_3y_to_4y_old".to_string()),
|
||||
_4y_to_5y: MobilitySpendingSupplyPattern::new(client.clone(), "utxos_4y_to_5y_old".to_string()),
|
||||
@@ -8219,7 +8179,7 @@ impl SeriesTree_Cohorts {
|
||||
pub struct SeriesTree_Cohorts_Utxo {
|
||||
pub all: SeriesTree_Cohorts_Utxo_All,
|
||||
pub sth: SeriesTree_Cohorts_Utxo_Sth,
|
||||
pub lth: ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2,
|
||||
pub lth: SeriesTree_Cohorts_Utxo_Lth,
|
||||
pub age_range: SeriesTree_Cohorts_Utxo_AgeRange,
|
||||
pub under_age: SeriesTree_Cohorts_Utxo_UnderAge,
|
||||
pub over_age: SeriesTree_Cohorts_Utxo_OverAge,
|
||||
@@ -8239,7 +8199,7 @@ impl SeriesTree_Cohorts_Utxo {
|
||||
Self {
|
||||
all: SeriesTree_Cohorts_Utxo_All::new(client.clone(), format!("{base_path}_all")),
|
||||
sth: SeriesTree_Cohorts_Utxo_Sth::new(client.clone(), format!("{base_path}_sth")),
|
||||
lth: ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2::new(client.clone(), "lth".to_string()),
|
||||
lth: SeriesTree_Cohorts_Utxo_Lth::new(client.clone(), format!("{base_path}_lth")),
|
||||
age_range: SeriesTree_Cohorts_Utxo_AgeRange::new(client.clone(), format!("{base_path}_age_range")),
|
||||
under_age: SeriesTree_Cohorts_Utxo_UnderAge::new(client.clone(), format!("{base_path}_under_age")),
|
||||
over_age: SeriesTree_Cohorts_Utxo_OverAge::new(client.clone(), format!("{base_path}_over_age")),
|
||||
@@ -8537,6 +8497,81 @@ impl SeriesTree_Cohorts_Utxo_Sth {
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Lth {
|
||||
pub supply: DeltaDominanceHalfInTotalPattern2,
|
||||
pub outputs: SpentUnspentPattern,
|
||||
pub activity: CoindaysCoinyearsDormancyTransferPattern,
|
||||
pub realized: SeriesTree_Cohorts_Utxo_Lth_Realized,
|
||||
pub cost_basis: InMaxMinPerSupplyPattern,
|
||||
pub unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2,
|
||||
pub invested_capital: InPattern,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Lth {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
supply: DeltaDominanceHalfInTotalPattern2::new(client.clone(), "lth_supply".to_string()),
|
||||
outputs: SpentUnspentPattern::new(client.clone(), "lth".to_string()),
|
||||
activity: CoindaysCoinyearsDormancyTransferPattern::new(client.clone(), "lth".to_string()),
|
||||
realized: SeriesTree_Cohorts_Utxo_Lth_Realized::new(client.clone(), format!("{base_path}_realized")),
|
||||
cost_basis: InMaxMinPerSupplyPattern::new(client.clone(), "lth".to_string()),
|
||||
unrealized: CapitalizedGrossInvestedLossNetNuplProfitSentimentPattern2::new(client.clone(), "lth".to_string()),
|
||||
invested_capital: InPattern::new(client.clone(), "lth_invested_capital_in".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Lth_Realized {
|
||||
pub cap: CentsDeltaToUsdPattern,
|
||||
pub profit: BlockCumulativeSumPattern,
|
||||
pub loss: BlockCumulativeNegativeSumPattern,
|
||||
pub price: CentsPpmRatioSatsUsdPattern,
|
||||
pub mvrv: SeriesPattern1<StoredF32>,
|
||||
pub net_pnl: BlockChangeCumulativeDeltaSumPattern,
|
||||
pub sopr: SeriesTree_Cohorts_Utxo_Lth_Realized_Sopr,
|
||||
pub gross_pnl: BlockCumulativeSumPattern,
|
||||
pub sell_side_risk_ratio: _1m1w1y24hPattern8,
|
||||
pub peak_regret: BlockCumulativeSumPattern,
|
||||
pub capitalized: PricePattern,
|
||||
pub profit_to_loss_ratio: _1m1w1y24hPattern<StoredF64>,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Lth_Realized {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
cap: CentsDeltaToUsdPattern::new(client.clone(), "lth_realized_cap".to_string()),
|
||||
profit: BlockCumulativeSumPattern::new(client.clone(), "lth_realized_profit".to_string()),
|
||||
loss: BlockCumulativeNegativeSumPattern::new(client.clone(), "lth_realized_loss".to_string()),
|
||||
price: CentsPpmRatioSatsUsdPattern::new(client.clone(), "lth_realized_price".to_string()),
|
||||
mvrv: SeriesPattern1::new(client.clone(), "lth_mvrv".to_string()),
|
||||
net_pnl: BlockChangeCumulativeDeltaSumPattern::new(client.clone(), "lth_net".to_string()),
|
||||
sopr: SeriesTree_Cohorts_Utxo_Lth_Realized_Sopr::new(client.clone(), format!("{base_path}_sopr")),
|
||||
gross_pnl: BlockCumulativeSumPattern::new(client.clone(), "lth_realized_gross_pnl".to_string()),
|
||||
sell_side_risk_ratio: _1m1w1y24hPattern8::new(client.clone(), "lth_sell_side_risk_ratio".to_string()),
|
||||
peak_regret: BlockCumulativeSumPattern::new(client.clone(), "lth_realized_peak_regret".to_string()),
|
||||
capitalized: PricePattern::new(client.clone(), "lth_capitalized_price".to_string()),
|
||||
profit_to_loss_ratio: _1m1w1y24hPattern::new(client.clone(), "lth_realized_profit_to_loss_ratio".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Lth_Realized_Sopr {
|
||||
pub value_destroyed: AverageBlockCumulativeSumPattern<Cents>,
|
||||
pub ratio: _1m1w1y24hPattern<StoredF64>,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Lth_Realized_Sopr {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
value_destroyed: AverageBlockCumulativeSumPattern::new(client.clone(), "lth_value_destroyed".to_string()),
|
||||
ratio: _1m1w1y24hPattern::new(client.clone(), "lth_sopr".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_AgeRange {
|
||||
pub under_1h: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
@@ -8548,8 +8583,10 @@ pub struct SeriesTree_Cohorts_Utxo_AgeRange {
|
||||
pub _3m_to_4m: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _4m_to_5m: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _5m_to_6m: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _6m_to_1y: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _1y_to_2y: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _6m_to_9m: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _9m_to_1y: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _1y_to_18m: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _18m_to_2y: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _2y_to_3y: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _3y_to_4y: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _4y_to_5y: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
@@ -8574,8 +8611,10 @@ impl SeriesTree_Cohorts_Utxo_AgeRange {
|
||||
_3m_to_4m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_3m_to_4m_old".to_string()),
|
||||
_4m_to_5m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_4m_to_5m_old".to_string()),
|
||||
_5m_to_6m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_5m_to_6m_old".to_string()),
|
||||
_6m_to_1y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_6m_to_1y_old".to_string()),
|
||||
_1y_to_2y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_1y_to_2y_old".to_string()),
|
||||
_6m_to_9m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_6m_to_9m_old".to_string()),
|
||||
_9m_to_1y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_9m_to_1y_old".to_string()),
|
||||
_1y_to_18m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_1y_to_18m_old".to_string()),
|
||||
_18m_to_2y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_18m_to_2y_old".to_string()),
|
||||
_2y_to_3y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_2y_to_3y_old".to_string()),
|
||||
_3y_to_4y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_3y_to_4y_old".to_string()),
|
||||
_4y_to_5y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_4y_to_5y_old".to_string()),
|
||||
@@ -8599,7 +8638,9 @@ pub struct SeriesTree_Cohorts_Utxo_UnderAge {
|
||||
pub _4m: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _5m: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _6m: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _9m: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _1y: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _18m: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _2y: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _3y: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _4y: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
@@ -8622,7 +8663,9 @@ impl SeriesTree_Cohorts_Utxo_UnderAge {
|
||||
_4m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_under_4m_old".to_string()),
|
||||
_5m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_under_5m_old".to_string()),
|
||||
_6m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_under_6m_old".to_string()),
|
||||
_9m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_under_9m_old".to_string()),
|
||||
_1y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_under_1y_old".to_string()),
|
||||
_18m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_under_18m_old".to_string()),
|
||||
_2y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_under_2y_old".to_string()),
|
||||
_3y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_under_3y_old".to_string()),
|
||||
_4y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_under_4y_old".to_string()),
|
||||
@@ -8647,7 +8690,9 @@ pub struct SeriesTree_Cohorts_Utxo_OverAge {
|
||||
pub _4m: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _5m: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _6m: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _9m: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _1y: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _18m: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _2y: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _3y: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _4y: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
@@ -8670,7 +8715,9 @@ impl SeriesTree_Cohorts_Utxo_OverAge {
|
||||
_4m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_over_4m_old".to_string()),
|
||||
_5m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_over_5m_old".to_string()),
|
||||
_6m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_over_6m_old".to_string()),
|
||||
_9m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_over_9m_old".to_string()),
|
||||
_1y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_over_1y_old".to_string()),
|
||||
_18m: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_over_18m_old".to_string()),
|
||||
_2y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_over_2y_old".to_string()),
|
||||
_3y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_over_3y_old".to_string()),
|
||||
_4y: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "utxos_over_4y_old".to_string()),
|
||||
@@ -8754,15 +8801,15 @@ impl SeriesTree_Cohorts_Utxo_Class {
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Utxo_Entry {
|
||||
pub discount: ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2,
|
||||
pub premium: ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2,
|
||||
pub discount: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub premium: ActivityOutputsRealizedSupplyUnrealizedPattern,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Utxo_Entry {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
discount: ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2::new(client.clone(), "veteran".to_string()),
|
||||
premium: ActivityCostInvestedOutputsRealizedSupplyUnrealizedPattern2::new(client.clone(), "rookie".to_string()),
|
||||
discount: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "veteran".to_string()),
|
||||
premium: ActivityOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "rookie".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9072,8 +9119,10 @@ pub struct SeriesTree_Cohorts_Utxo_Matured {
|
||||
pub _3m_to_4m: AverageBlockCumulativeSumPattern2,
|
||||
pub _4m_to_5m: AverageBlockCumulativeSumPattern2,
|
||||
pub _5m_to_6m: AverageBlockCumulativeSumPattern2,
|
||||
pub _6m_to_1y: AverageBlockCumulativeSumPattern2,
|
||||
pub _1y_to_2y: AverageBlockCumulativeSumPattern2,
|
||||
pub _6m_to_9m: AverageBlockCumulativeSumPattern2,
|
||||
pub _9m_to_1y: AverageBlockCumulativeSumPattern2,
|
||||
pub _1y_to_18m: AverageBlockCumulativeSumPattern2,
|
||||
pub _18m_to_2y: AverageBlockCumulativeSumPattern2,
|
||||
pub _2y_to_3y: AverageBlockCumulativeSumPattern2,
|
||||
pub _3y_to_4y: AverageBlockCumulativeSumPattern2,
|
||||
pub _4y_to_5y: AverageBlockCumulativeSumPattern2,
|
||||
@@ -9098,8 +9147,10 @@ impl SeriesTree_Cohorts_Utxo_Matured {
|
||||
_3m_to_4m: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_3m_to_4m_old_matured_supply".to_string()),
|
||||
_4m_to_5m: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_4m_to_5m_old_matured_supply".to_string()),
|
||||
_5m_to_6m: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_5m_to_6m_old_matured_supply".to_string()),
|
||||
_6m_to_1y: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_6m_to_1y_old_matured_supply".to_string()),
|
||||
_1y_to_2y: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_1y_to_2y_old_matured_supply".to_string()),
|
||||
_6m_to_9m: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_6m_to_9m_old_matured_supply".to_string()),
|
||||
_9m_to_1y: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_9m_to_1y_old_matured_supply".to_string()),
|
||||
_1y_to_18m: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_1y_to_18m_old_matured_supply".to_string()),
|
||||
_18m_to_2y: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_18m_to_2y_old_matured_supply".to_string()),
|
||||
_2y_to_3y: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_2y_to_3y_old_matured_supply".to_string()),
|
||||
_3y_to_4y: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_3y_to_4y_old_matured_supply".to_string()),
|
||||
_4y_to_5y: AverageBlockCumulativeSumPattern2::new(client.clone(), "utxos_4y_to_5y_old_matured_supply".to_string()),
|
||||
@@ -9133,115 +9184,115 @@ impl SeriesTree_Cohorts_Addr {
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Addr_OverAmount {
|
||||
pub _1sat: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _10sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _100sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _1k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _10k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _100k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _1m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _10m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _1btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _10btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _100btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _1k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _10k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _1sat: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _10sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _100sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _1k_sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _10k_sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _100k_sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _1m_sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _10m_sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _1btc: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _10btc: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _100btc: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _1k_btc: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _10k_btc: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Addr_OverAmount {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
_1sat: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_1sat".to_string()),
|
||||
_10sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_10sats".to_string()),
|
||||
_100sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_100sats".to_string()),
|
||||
_1k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_1k_sats".to_string()),
|
||||
_10k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_10k_sats".to_string()),
|
||||
_100k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_100k_sats".to_string()),
|
||||
_1m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_1m_sats".to_string()),
|
||||
_10m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_10m_sats".to_string()),
|
||||
_1btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_1btc".to_string()),
|
||||
_10btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_10btc".to_string()),
|
||||
_100btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_100btc".to_string()),
|
||||
_1k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_1k_btc".to_string()),
|
||||
_10k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_10k_btc".to_string()),
|
||||
_1sat: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_1sat".to_string()),
|
||||
_10sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_10sats".to_string()),
|
||||
_100sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_100sats".to_string()),
|
||||
_1k_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_1k_sats".to_string()),
|
||||
_10k_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_10k_sats".to_string()),
|
||||
_100k_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_100k_sats".to_string()),
|
||||
_1m_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_1m_sats".to_string()),
|
||||
_10m_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_10m_sats".to_string()),
|
||||
_1btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_1btc".to_string()),
|
||||
_10btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_10btc".to_string()),
|
||||
_100btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_100btc".to_string()),
|
||||
_1k_btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_1k_btc".to_string()),
|
||||
_10k_btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_10k_btc".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Addr_AmountRange {
|
||||
pub _0sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _1sat_to_10sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _10sats_to_100sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _100sats_to_1k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _1k_sats_to_10k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _10k_sats_to_100k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _100k_sats_to_1m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _1m_sats_to_10m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _10m_sats_to_1btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _1btc_to_10btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _10btc_to_100btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _100btc_to_1k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _1k_btc_to_10k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _10k_btc_to_100k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub over_100k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _0sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _1sat_to_10sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _10sats_to_100sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _100sats_to_1k_sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _1k_sats_to_10k_sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _10k_sats_to_100k_sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _100k_sats_to_1m_sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _1m_sats_to_10m_sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _10m_sats_to_1btc: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _1btc_to_10btc: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _10btc_to_100btc: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _100btc_to_1k_btc: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _1k_btc_to_10k_btc: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _10k_btc_to_100k_btc: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub over_100k_btc: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Addr_AmountRange {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
_0sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_0sats".to_string()),
|
||||
_1sat_to_10sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_1sat_to_10sats".to_string()),
|
||||
_10sats_to_100sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_10sats_to_100sats".to_string()),
|
||||
_100sats_to_1k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_100sats_to_1k_sats".to_string()),
|
||||
_1k_sats_to_10k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_1k_sats_to_10k_sats".to_string()),
|
||||
_10k_sats_to_100k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_10k_sats_to_100k_sats".to_string()),
|
||||
_100k_sats_to_1m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_100k_sats_to_1m_sats".to_string()),
|
||||
_1m_sats_to_10m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_1m_sats_to_10m_sats".to_string()),
|
||||
_10m_sats_to_1btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_10m_sats_to_1btc".to_string()),
|
||||
_1btc_to_10btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_1btc_to_10btc".to_string()),
|
||||
_10btc_to_100btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_10btc_to_100btc".to_string()),
|
||||
_100btc_to_1k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_100btc_to_1k_btc".to_string()),
|
||||
_1k_btc_to_10k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_1k_btc_to_10k_btc".to_string()),
|
||||
_10k_btc_to_100k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_10k_btc_to_100k_btc".to_string()),
|
||||
over_100k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_over_100k_btc".to_string()),
|
||||
_0sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_0sats".to_string()),
|
||||
_1sat_to_10sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_1sat_to_10sats".to_string()),
|
||||
_10sats_to_100sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_10sats_to_100sats".to_string()),
|
||||
_100sats_to_1k_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_100sats_to_1k_sats".to_string()),
|
||||
_1k_sats_to_10k_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_1k_sats_to_10k_sats".to_string()),
|
||||
_10k_sats_to_100k_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_10k_sats_to_100k_sats".to_string()),
|
||||
_100k_sats_to_1m_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_100k_sats_to_1m_sats".to_string()),
|
||||
_1m_sats_to_10m_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_1m_sats_to_10m_sats".to_string()),
|
||||
_10m_sats_to_1btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_10m_sats_to_1btc".to_string()),
|
||||
_1btc_to_10btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_1btc_to_10btc".to_string()),
|
||||
_10btc_to_100btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_10btc_to_100btc".to_string()),
|
||||
_100btc_to_1k_btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_100btc_to_1k_btc".to_string()),
|
||||
_1k_btc_to_10k_btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_1k_btc_to_10k_btc".to_string()),
|
||||
_10k_btc_to_100k_btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_10k_btc_to_100k_btc".to_string()),
|
||||
over_100k_btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_over_100k_btc".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Series tree node.
|
||||
pub struct SeriesTree_Cohorts_Addr_UnderAmount {
|
||||
pub _10sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _100sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _1k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _10k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _100k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _1m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _10m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _1btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _10btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _100btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _1k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _10k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _100k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern,
|
||||
pub _10sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _100sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _1k_sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _10k_sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _100k_sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _1m_sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _10m_sats: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _1btc: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _10btc: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _100btc: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _1k_btc: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _10k_btc: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
pub _100k_btc: ActivityAddrOutputsRealizedSupplyPattern,
|
||||
}
|
||||
|
||||
impl SeriesTree_Cohorts_Addr_UnderAmount {
|
||||
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
|
||||
Self {
|
||||
_10sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_10sats".to_string()),
|
||||
_100sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_100sats".to_string()),
|
||||
_1k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_1k_sats".to_string()),
|
||||
_10k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_10k_sats".to_string()),
|
||||
_100k_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_100k_sats".to_string()),
|
||||
_1m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_1m_sats".to_string()),
|
||||
_10m_sats: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_10m_sats".to_string()),
|
||||
_1btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_1btc".to_string()),
|
||||
_10btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_10btc".to_string()),
|
||||
_100btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_100btc".to_string()),
|
||||
_1k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_1k_btc".to_string()),
|
||||
_10k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_10k_btc".to_string()),
|
||||
_100k_btc: ActivityAddrOutputsRealizedSupplyUnrealizedPattern::new(client.clone(), "addrs_under_100k_btc".to_string()),
|
||||
_10sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_10sats".to_string()),
|
||||
_100sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_100sats".to_string()),
|
||||
_1k_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_1k_sats".to_string()),
|
||||
_10k_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_10k_sats".to_string()),
|
||||
_100k_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_100k_sats".to_string()),
|
||||
_1m_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_1m_sats".to_string()),
|
||||
_10m_sats: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_10m_sats".to_string()),
|
||||
_1btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_1btc".to_string()),
|
||||
_10btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_10btc".to_string()),
|
||||
_100btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_100btc".to_string()),
|
||||
_1k_btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_1k_btc".to_string()),
|
||||
_10k_btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_10k_btc".to_string()),
|
||||
_100k_btc: ActivityAddrOutputsRealizedSupplyPattern::new(client.clone(), "addrs_under_100k_btc".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,10 @@ pub const HOURS_3M: usize = 24 * 3 * 30;
|
||||
pub const HOURS_4M: usize = 24 * 4 * 30;
|
||||
pub const HOURS_5M: usize = 24 * 5 * 30; // STH/LTH threshold
|
||||
pub const HOURS_6M: usize = 24 * 6 * 30;
|
||||
pub const HOURS_9M: usize = HOURS_6M + HOURS_3M;
|
||||
pub const HOURS_1Y: usize = 24 * 365;
|
||||
// Keep year-based ranges anchored to the existing 365-day year convention.
|
||||
pub const HOURS_18M: usize = HOURS_1Y + HOURS_6M;
|
||||
pub const HOURS_2Y: usize = 24 * 2 * 365;
|
||||
pub const HOURS_3Y: usize = 24 * 3 * 365;
|
||||
pub const HOURS_4Y: usize = 24 * 4 * 365;
|
||||
@@ -29,12 +32,14 @@ pub const HOURS_10Y: usize = 24 * 10 * 365;
|
||||
pub const HOURS_12Y: usize = 24 * 12 * 365;
|
||||
pub const HOURS_15Y: usize = 24 * 15 * 365;
|
||||
|
||||
pub const AGE_RANGE_COUNT: usize = 23;
|
||||
|
||||
/// Age boundaries in hours. Defines the cohort ranges:
|
||||
/// [0, 1h), [1h, 1d), [1d, 1w), [1w, 1m), ..., [15y, ∞)
|
||||
pub const AGE_BOUNDARIES: [usize; 20] = [
|
||||
pub const AGE_BOUNDARIES: [usize; AGE_RANGE_COUNT - 1] = [
|
||||
HOURS_1H, HOURS_1D, HOURS_1W, HOURS_1M, HOURS_2M, HOURS_3M, HOURS_4M, HOURS_5M, HOURS_6M,
|
||||
HOURS_1Y, HOURS_2Y, HOURS_3Y, HOURS_4Y, HOURS_5Y, HOURS_6Y, HOURS_7Y, HOURS_8Y, HOURS_10Y,
|
||||
HOURS_12Y, HOURS_15Y,
|
||||
HOURS_9M, HOURS_1Y, HOURS_18M, HOURS_2Y, HOURS_3Y, HOURS_4Y, HOURS_5Y, HOURS_6Y, HOURS_7Y,
|
||||
HOURS_8Y, HOURS_10Y, HOURS_12Y, HOURS_15Y,
|
||||
];
|
||||
|
||||
/// Age range bounds (end = usize::MAX means unbounded)
|
||||
@@ -48,8 +53,10 @@ pub const AGE_RANGE_BOUNDS: AgeRange<Range<usize>> = AgeRange {
|
||||
_3m_to_4m: HOURS_3M..HOURS_4M,
|
||||
_4m_to_5m: HOURS_4M..HOURS_5M,
|
||||
_5m_to_6m: HOURS_5M..HOURS_6M,
|
||||
_6m_to_1y: HOURS_6M..HOURS_1Y,
|
||||
_1y_to_2y: HOURS_1Y..HOURS_2Y,
|
||||
_6m_to_9m: HOURS_6M..HOURS_9M,
|
||||
_9m_to_1y: HOURS_9M..HOURS_1Y,
|
||||
_1y_to_18m: HOURS_1Y..HOURS_18M,
|
||||
_18m_to_2y: HOURS_18M..HOURS_2Y,
|
||||
_2y_to_3y: HOURS_2Y..HOURS_3Y,
|
||||
_3y_to_4y: HOURS_3Y..HOURS_4Y,
|
||||
_4y_to_5y: HOURS_4Y..HOURS_5Y,
|
||||
@@ -73,8 +80,10 @@ pub const AGE_RANGE_FILTERS: AgeRange<Filter> = AgeRange {
|
||||
_3m_to_4m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._3m_to_4m)),
|
||||
_4m_to_5m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._4m_to_5m)),
|
||||
_5m_to_6m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._5m_to_6m)),
|
||||
_6m_to_1y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._6m_to_1y)),
|
||||
_1y_to_2y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._1y_to_2y)),
|
||||
_6m_to_9m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._6m_to_9m)),
|
||||
_9m_to_1y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._9m_to_1y)),
|
||||
_1y_to_18m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._1y_to_18m)),
|
||||
_18m_to_2y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._18m_to_2y)),
|
||||
_2y_to_3y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._2y_to_3y)),
|
||||
_3y_to_4y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._3y_to_4y)),
|
||||
_4y_to_5y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._4y_to_5y)),
|
||||
@@ -98,8 +107,10 @@ pub const AGE_RANGE_NAMES: AgeRange<CohortName> = AgeRange {
|
||||
_3m_to_4m: CohortName::new("3m_to_4m_old", "3m-4m", "3 to 4 Months Old"),
|
||||
_4m_to_5m: CohortName::new("4m_to_5m_old", "4m-5m", "4 to 5 Months Old"),
|
||||
_5m_to_6m: CohortName::new("5m_to_6m_old", "5m-6m", "5 to 6 Months Old"),
|
||||
_6m_to_1y: CohortName::new("6m_to_1y_old", "6m-1y", "6 Months to 1 Year Old"),
|
||||
_1y_to_2y: CohortName::new("1y_to_2y_old", "1y-2y", "1 to 2 Years Old"),
|
||||
_6m_to_9m: CohortName::new("6m_to_9m_old", "6m-9m", "6 to 9 Months Old"),
|
||||
_9m_to_1y: CohortName::new("9m_to_1y_old", "9m-1y", "9 Months to 1 Year Old"),
|
||||
_1y_to_18m: CohortName::new("1y_to_18m_old", "1y-18m", "1 Year to 18 Months Old"),
|
||||
_18m_to_2y: CohortName::new("18m_to_2y_old", "18m-2y", "18 Months to 2 Years Old"),
|
||||
_2y_to_3y: CohortName::new("2y_to_3y_old", "2y-3y", "2 to 3 Years Old"),
|
||||
_3y_to_4y: CohortName::new("3y_to_4y_old", "3y-4y", "3 to 4 Years Old"),
|
||||
_4y_to_5y: CohortName::new("4y_to_5y_old", "4y-5y", "4 to 5 Years Old"),
|
||||
@@ -129,8 +140,10 @@ pub struct AgeRange<T> {
|
||||
pub _3m_to_4m: T,
|
||||
pub _4m_to_5m: T,
|
||||
pub _5m_to_6m: T,
|
||||
pub _6m_to_1y: T,
|
||||
pub _1y_to_2y: T,
|
||||
pub _6m_to_9m: T,
|
||||
pub _9m_to_1y: T,
|
||||
pub _1y_to_18m: T,
|
||||
pub _18m_to_2y: T,
|
||||
pub _2y_to_3y: T,
|
||||
pub _3y_to_4y: T,
|
||||
pub _4y_to_5y: T,
|
||||
@@ -157,8 +170,10 @@ impl<T> AgeRange<T> {
|
||||
HOURS_3M..HOURS_4M => &mut self._3m_to_4m,
|
||||
HOURS_4M..HOURS_5M => &mut self._4m_to_5m,
|
||||
HOURS_5M..HOURS_6M => &mut self._5m_to_6m,
|
||||
HOURS_6M..HOURS_1Y => &mut self._6m_to_1y,
|
||||
HOURS_1Y..HOURS_2Y => &mut self._1y_to_2y,
|
||||
HOURS_6M..HOURS_9M => &mut self._6m_to_9m,
|
||||
HOURS_9M..HOURS_1Y => &mut self._9m_to_1y,
|
||||
HOURS_1Y..HOURS_18M => &mut self._1y_to_18m,
|
||||
HOURS_18M..HOURS_2Y => &mut self._18m_to_2y,
|
||||
HOURS_2Y..HOURS_3Y => &mut self._2y_to_3y,
|
||||
HOURS_3Y..HOURS_4Y => &mut self._3y_to_4y,
|
||||
HOURS_4Y..HOURS_5Y => &mut self._4y_to_5y,
|
||||
@@ -185,8 +200,10 @@ impl<T> AgeRange<T> {
|
||||
HOURS_3M..HOURS_4M => &self._3m_to_4m,
|
||||
HOURS_4M..HOURS_5M => &self._4m_to_5m,
|
||||
HOURS_5M..HOURS_6M => &self._5m_to_6m,
|
||||
HOURS_6M..HOURS_1Y => &self._6m_to_1y,
|
||||
HOURS_1Y..HOURS_2Y => &self._1y_to_2y,
|
||||
HOURS_6M..HOURS_9M => &self._6m_to_9m,
|
||||
HOURS_9M..HOURS_1Y => &self._9m_to_1y,
|
||||
HOURS_1Y..HOURS_18M => &self._1y_to_18m,
|
||||
HOURS_18M..HOURS_2Y => &self._18m_to_2y,
|
||||
HOURS_2Y..HOURS_3Y => &self._2y_to_3y,
|
||||
HOURS_3Y..HOURS_4Y => &self._3y_to_4y,
|
||||
HOURS_4Y..HOURS_5Y => &self._4y_to_5y,
|
||||
@@ -200,7 +217,7 @@ impl<T> AgeRange<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_array(arr: [T; 21]) -> Self {
|
||||
pub fn from_array(arr: [T; AGE_RANGE_COUNT]) -> Self {
|
||||
let [
|
||||
a0,
|
||||
a1,
|
||||
@@ -223,6 +240,8 @@ impl<T> AgeRange<T> {
|
||||
a18,
|
||||
a19,
|
||||
a20,
|
||||
a21,
|
||||
a22,
|
||||
] = arr;
|
||||
Self {
|
||||
under_1h: a0,
|
||||
@@ -234,18 +253,20 @@ impl<T> AgeRange<T> {
|
||||
_3m_to_4m: a6,
|
||||
_4m_to_5m: a7,
|
||||
_5m_to_6m: a8,
|
||||
_6m_to_1y: a9,
|
||||
_1y_to_2y: a10,
|
||||
_2y_to_3y: a11,
|
||||
_3y_to_4y: a12,
|
||||
_4y_to_5y: a13,
|
||||
_5y_to_6y: a14,
|
||||
_6y_to_7y: a15,
|
||||
_7y_to_8y: a16,
|
||||
_8y_to_10y: a17,
|
||||
_10y_to_12y: a18,
|
||||
_12y_to_15y: a19,
|
||||
over_15y: a20,
|
||||
_6m_to_9m: a9,
|
||||
_9m_to_1y: a10,
|
||||
_1y_to_18m: a11,
|
||||
_18m_to_2y: a12,
|
||||
_2y_to_3y: a13,
|
||||
_3y_to_4y: a14,
|
||||
_4y_to_5y: a15,
|
||||
_5y_to_6y: a16,
|
||||
_6y_to_7y: a17,
|
||||
_7y_to_8y: a18,
|
||||
_8y_to_10y: a19,
|
||||
_10y_to_12y: a20,
|
||||
_12y_to_15y: a21,
|
||||
over_15y: a22,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,8 +286,10 @@ impl<T> AgeRange<T> {
|
||||
_3m_to_4m: create(f._3m_to_4m.clone(), n._3m_to_4m.id),
|
||||
_4m_to_5m: create(f._4m_to_5m.clone(), n._4m_to_5m.id),
|
||||
_5m_to_6m: create(f._5m_to_6m.clone(), n._5m_to_6m.id),
|
||||
_6m_to_1y: create(f._6m_to_1y.clone(), n._6m_to_1y.id),
|
||||
_1y_to_2y: create(f._1y_to_2y.clone(), n._1y_to_2y.id),
|
||||
_6m_to_9m: create(f._6m_to_9m.clone(), n._6m_to_9m.id),
|
||||
_9m_to_1y: create(f._9m_to_1y.clone(), n._9m_to_1y.id),
|
||||
_1y_to_18m: create(f._1y_to_18m.clone(), n._1y_to_18m.id),
|
||||
_18m_to_2y: create(f._18m_to_2y.clone(), n._18m_to_2y.id),
|
||||
_2y_to_3y: create(f._2y_to_3y.clone(), n._2y_to_3y.id),
|
||||
_3y_to_4y: create(f._3y_to_4y.clone(), n._3y_to_4y.id),
|
||||
_4y_to_5y: create(f._4y_to_5y.clone(), n._4y_to_5y.id),
|
||||
@@ -296,8 +319,10 @@ impl<T> AgeRange<T> {
|
||||
_3m_to_4m: create(f._3m_to_4m.clone(), n._3m_to_4m.id)?,
|
||||
_4m_to_5m: create(f._4m_to_5m.clone(), n._4m_to_5m.id)?,
|
||||
_5m_to_6m: create(f._5m_to_6m.clone(), n._5m_to_6m.id)?,
|
||||
_6m_to_1y: create(f._6m_to_1y.clone(), n._6m_to_1y.id)?,
|
||||
_1y_to_2y: create(f._1y_to_2y.clone(), n._1y_to_2y.id)?,
|
||||
_6m_to_9m: create(f._6m_to_9m.clone(), n._6m_to_9m.id)?,
|
||||
_9m_to_1y: create(f._9m_to_1y.clone(), n._9m_to_1y.id)?,
|
||||
_1y_to_18m: create(f._1y_to_18m.clone(), n._1y_to_18m.id)?,
|
||||
_18m_to_2y: create(f._18m_to_2y.clone(), n._18m_to_2y.id)?,
|
||||
_2y_to_3y: create(f._2y_to_3y.clone(), n._2y_to_3y.id)?,
|
||||
_3y_to_4y: create(f._3y_to_4y.clone(), n._3y_to_4y.id)?,
|
||||
_4y_to_5y: create(f._4y_to_5y.clone(), n._4y_to_5y.id)?,
|
||||
@@ -322,8 +347,10 @@ impl<T> AgeRange<T> {
|
||||
&self._3m_to_4m,
|
||||
&self._4m_to_5m,
|
||||
&self._5m_to_6m,
|
||||
&self._6m_to_1y,
|
||||
&self._1y_to_2y,
|
||||
&self._6m_to_9m,
|
||||
&self._9m_to_1y,
|
||||
&self._1y_to_18m,
|
||||
&self._18m_to_2y,
|
||||
&self._2y_to_3y,
|
||||
&self._3y_to_4y,
|
||||
&self._4y_to_5y,
|
||||
@@ -349,8 +376,10 @@ impl<T> AgeRange<T> {
|
||||
&mut self._3m_to_4m,
|
||||
&mut self._4m_to_5m,
|
||||
&mut self._5m_to_6m,
|
||||
&mut self._6m_to_1y,
|
||||
&mut self._1y_to_2y,
|
||||
&mut self._6m_to_9m,
|
||||
&mut self._9m_to_1y,
|
||||
&mut self._1y_to_18m,
|
||||
&mut self._18m_to_2y,
|
||||
&mut self._2y_to_3y,
|
||||
&mut self._3y_to_4y,
|
||||
&mut self._4y_to_5y,
|
||||
@@ -379,8 +408,10 @@ impl<T> AgeRange<T> {
|
||||
&mut self._3m_to_4m,
|
||||
&mut self._4m_to_5m,
|
||||
&mut self._5m_to_6m,
|
||||
&mut self._6m_to_1y,
|
||||
&mut self._1y_to_2y,
|
||||
&mut self._6m_to_9m,
|
||||
&mut self._9m_to_1y,
|
||||
&mut self._1y_to_18m,
|
||||
&mut self._18m_to_2y,
|
||||
&mut self._2y_to_3y,
|
||||
&mut self._3y_to_4y,
|
||||
&mut self._4y_to_5y,
|
||||
@@ -395,3 +426,46 @@ impl<T> AgeRange<T> {
|
||||
.into_par_iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ranges_are_contiguous_and_match_the_declared_count() {
|
||||
let ranges: Vec<_> = AGE_RANGE_BOUNDS.iter().collect();
|
||||
|
||||
assert_eq!(ranges.len(), AGE_RANGE_COUNT);
|
||||
assert_eq!(ranges.first().unwrap().start, 0);
|
||||
assert_eq!(ranges.last().unwrap().end, usize::MAX);
|
||||
|
||||
for adjacent in ranges.windows(2) {
|
||||
assert_eq!(adjacent[0].end, adjacent[1].start);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_range_names_and_boundaries_match() {
|
||||
assert_eq!(HOURS_9M, HOURS_6M + HOURS_3M);
|
||||
assert_eq!(HOURS_18M, HOURS_1Y + HOURS_6M);
|
||||
assert_eq!(AGE_RANGE_NAMES._6m_to_9m.id, "6m_to_9m_old");
|
||||
assert_eq!(AGE_RANGE_NAMES._9m_to_1y.id, "9m_to_1y_old");
|
||||
assert_eq!(AGE_RANGE_NAMES._1y_to_18m.id, "1y_to_18m_old");
|
||||
assert_eq!(AGE_RANGE_NAMES._18m_to_2y.id, "18m_to_2y_old");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eighteen_month_classifier_stays_anchored_to_one_year_plus_six_months() {
|
||||
let age_at_540_days = Age::new(
|
||||
brk_types::Timestamp::new((HOURS_6M * 3 * 60 * 60) as u32),
|
||||
brk_types::Timestamp::ZERO,
|
||||
);
|
||||
let age_at_18m = Age::new(
|
||||
brk_types::Timestamp::new((HOURS_18M * 60 * 60) as u32),
|
||||
brk_types::Timestamp::ZERO,
|
||||
);
|
||||
|
||||
assert_eq!(AGE_RANGE_NAMES.get(age_at_540_days).id, "1y_to_18m_old");
|
||||
assert_eq!(AGE_RANGE_NAMES.get(age_at_18m).id, "18m_to_2y_old");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ mod amount_filter;
|
||||
mod amount_range;
|
||||
mod by_addr_type;
|
||||
mod by_any_addr;
|
||||
mod by_epoch;
|
||||
mod by_entry;
|
||||
mod by_epoch;
|
||||
mod by_term;
|
||||
mod by_type;
|
||||
mod class;
|
||||
@@ -36,8 +36,8 @@ pub use amount_filter::*;
|
||||
pub use amount_range::*;
|
||||
pub use by_addr_type::*;
|
||||
pub use by_any_addr::*;
|
||||
pub use by_epoch::*;
|
||||
pub use by_entry::*;
|
||||
pub use by_epoch::*;
|
||||
pub use by_term::*;
|
||||
pub use by_type::*;
|
||||
pub use class::*;
|
||||
|
||||
@@ -5,7 +5,7 @@ use serde::Serialize;
|
||||
use super::{
|
||||
CohortName, Filter, HOURS_1D, HOURS_1M, HOURS_1W, HOURS_1Y, HOURS_2M, HOURS_2Y, HOURS_3M,
|
||||
HOURS_3Y, HOURS_4M, HOURS_4Y, HOURS_5M, HOURS_5Y, HOURS_6M, HOURS_6Y, HOURS_7Y, HOURS_8Y,
|
||||
HOURS_10Y, HOURS_12Y, TimeFilter,
|
||||
HOURS_9M, HOURS_10Y, HOURS_12Y, HOURS_18M, TimeFilter,
|
||||
};
|
||||
|
||||
/// Over-age thresholds in hours
|
||||
@@ -18,7 +18,9 @@ pub const OVER_AGE_HOURS: OverAge<usize> = OverAge {
|
||||
_4m: HOURS_4M,
|
||||
_5m: HOURS_5M,
|
||||
_6m: HOURS_6M,
|
||||
_9m: HOURS_9M,
|
||||
_1y: HOURS_1Y,
|
||||
_18m: HOURS_18M,
|
||||
_2y: HOURS_2Y,
|
||||
_3y: HOURS_3Y,
|
||||
_4y: HOURS_4Y,
|
||||
@@ -40,7 +42,9 @@ pub const OVER_AGE_FILTERS: OverAge<Filter> = OverAge {
|
||||
_4m: Filter::Time(TimeFilter::GreaterOrEqual(OVER_AGE_HOURS._4m)),
|
||||
_5m: Filter::Time(TimeFilter::GreaterOrEqual(OVER_AGE_HOURS._5m)),
|
||||
_6m: Filter::Time(TimeFilter::GreaterOrEqual(OVER_AGE_HOURS._6m)),
|
||||
_9m: Filter::Time(TimeFilter::GreaterOrEqual(OVER_AGE_HOURS._9m)),
|
||||
_1y: Filter::Time(TimeFilter::GreaterOrEqual(OVER_AGE_HOURS._1y)),
|
||||
_18m: Filter::Time(TimeFilter::GreaterOrEqual(OVER_AGE_HOURS._18m)),
|
||||
_2y: Filter::Time(TimeFilter::GreaterOrEqual(OVER_AGE_HOURS._2y)),
|
||||
_3y: Filter::Time(TimeFilter::GreaterOrEqual(OVER_AGE_HOURS._3y)),
|
||||
_4y: Filter::Time(TimeFilter::GreaterOrEqual(OVER_AGE_HOURS._4y)),
|
||||
@@ -62,7 +66,9 @@ pub const OVER_AGE_NAMES: OverAge<CohortName> = OverAge {
|
||||
_4m: CohortName::new("over_4m_old", "4m+", "Over 4 Months Old"),
|
||||
_5m: CohortName::new("over_5m_old", "5m+", "Over 5 Months Old"),
|
||||
_6m: CohortName::new("over_6m_old", "6m+", "Over 6 Months Old"),
|
||||
_9m: CohortName::new("over_9m_old", "9m+", "Over 9 Months Old"),
|
||||
_1y: CohortName::new("over_1y_old", "1y+", "Over 1 Year Old"),
|
||||
_18m: CohortName::new("over_18m_old", "18m+", "Over 18 Months Old"),
|
||||
_2y: CohortName::new("over_2y_old", "2y+", "Over 2 Years Old"),
|
||||
_3y: CohortName::new("over_3y_old", "3y+", "Over 3 Years Old"),
|
||||
_4y: CohortName::new("over_4y_old", "4y+", "Over 4 Years Old"),
|
||||
@@ -84,7 +90,9 @@ pub struct OverAge<T> {
|
||||
pub _4m: T,
|
||||
pub _5m: T,
|
||||
pub _6m: T,
|
||||
pub _9m: T,
|
||||
pub _1y: T,
|
||||
pub _18m: T,
|
||||
pub _2y: T,
|
||||
pub _3y: T,
|
||||
pub _4y: T,
|
||||
@@ -118,7 +126,9 @@ impl<T> OverAge<T> {
|
||||
_4m: create(f._4m.clone(), n._4m.id),
|
||||
_5m: create(f._5m.clone(), n._5m.id),
|
||||
_6m: create(f._6m.clone(), n._6m.id),
|
||||
_9m: create(f._9m.clone(), n._9m.id),
|
||||
_1y: create(f._1y.clone(), n._1y.id),
|
||||
_18m: create(f._18m.clone(), n._18m.id),
|
||||
_2y: create(f._2y.clone(), n._2y.id),
|
||||
_3y: create(f._3y.clone(), n._3y.id),
|
||||
_4y: create(f._4y.clone(), n._4y.id),
|
||||
@@ -146,7 +156,9 @@ impl<T> OverAge<T> {
|
||||
_4m: create(f._4m.clone(), n._4m.id)?,
|
||||
_5m: create(f._5m.clone(), n._5m.id)?,
|
||||
_6m: create(f._6m.clone(), n._6m.id)?,
|
||||
_9m: create(f._9m.clone(), n._9m.id)?,
|
||||
_1y: create(f._1y.clone(), n._1y.id)?,
|
||||
_18m: create(f._18m.clone(), n._18m.id)?,
|
||||
_2y: create(f._2y.clone(), n._2y.id)?,
|
||||
_3y: create(f._3y.clone(), n._3y.id)?,
|
||||
_4y: create(f._4y.clone(), n._4y.id)?,
|
||||
@@ -162,8 +174,8 @@ impl<T> OverAge<T> {
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
[
|
||||
&self._1d, &self._1w, &self._1m, &self._2m, &self._3m, &self._4m, &self._5m, &self._6m,
|
||||
&self._1y, &self._2y, &self._3y, &self._4y, &self._5y, &self._6y, &self._7y, &self._8y,
|
||||
&self._10y, &self._12y,
|
||||
&self._9m, &self._1y, &self._18m, &self._2y, &self._3y, &self._4y, &self._5y,
|
||||
&self._6y, &self._7y, &self._8y, &self._10y, &self._12y,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
@@ -178,7 +190,9 @@ impl<T> OverAge<T> {
|
||||
&mut self._4m,
|
||||
&mut self._5m,
|
||||
&mut self._6m,
|
||||
&mut self._9m,
|
||||
&mut self._1y,
|
||||
&mut self._18m,
|
||||
&mut self._2y,
|
||||
&mut self._3y,
|
||||
&mut self._4y,
|
||||
@@ -205,7 +219,9 @@ impl<T> OverAge<T> {
|
||||
&mut self._4m,
|
||||
&mut self._5m,
|
||||
&mut self._6m,
|
||||
&mut self._9m,
|
||||
&mut self._1y,
|
||||
&mut self._18m,
|
||||
&mut self._2y,
|
||||
&mut self._3y,
|
||||
&mut self._4y,
|
||||
@@ -219,3 +235,25 @@ impl<T> OverAge<T> {
|
||||
.into_par_iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::AGE_RANGE_FILTERS;
|
||||
|
||||
#[test]
|
||||
fn new_thresholds_include_only_older_ranges() {
|
||||
assert!(!OVER_AGE_FILTERS._9m.includes(&AGE_RANGE_FILTERS._6m_to_9m));
|
||||
assert!(OVER_AGE_FILTERS._9m.includes(&AGE_RANGE_FILTERS._9m_to_1y));
|
||||
assert!(
|
||||
!OVER_AGE_FILTERS
|
||||
._18m
|
||||
.includes(&AGE_RANGE_FILTERS._1y_to_18m)
|
||||
);
|
||||
assert!(
|
||||
OVER_AGE_FILTERS
|
||||
._18m
|
||||
.includes(&AGE_RANGE_FILTERS._18m_to_2y)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ use serde::Serialize;
|
||||
|
||||
use super::{
|
||||
CohortName, Filter, HOURS_1M, HOURS_1W, HOURS_1Y, HOURS_2M, HOURS_2Y, HOURS_3M, HOURS_3Y,
|
||||
HOURS_4M, HOURS_4Y, HOURS_5M, HOURS_5Y, HOURS_6M, HOURS_6Y, HOURS_7Y, HOURS_8Y, HOURS_10Y,
|
||||
HOURS_12Y, HOURS_15Y, TimeFilter,
|
||||
HOURS_4M, HOURS_4Y, HOURS_5M, HOURS_5Y, HOURS_6M, HOURS_6Y, HOURS_7Y, HOURS_8Y, HOURS_9M,
|
||||
HOURS_10Y, HOURS_12Y, HOURS_15Y, HOURS_18M, TimeFilter,
|
||||
};
|
||||
|
||||
/// Under-age thresholds in hours
|
||||
@@ -17,7 +17,9 @@ pub const UNDER_AGE_HOURS: UnderAge<usize> = UnderAge {
|
||||
_4m: HOURS_4M,
|
||||
_5m: HOURS_5M,
|
||||
_6m: HOURS_6M,
|
||||
_9m: HOURS_9M,
|
||||
_1y: HOURS_1Y,
|
||||
_18m: HOURS_18M,
|
||||
_2y: HOURS_2Y,
|
||||
_3y: HOURS_3Y,
|
||||
_4y: HOURS_4Y,
|
||||
@@ -39,7 +41,9 @@ pub const UNDER_AGE_FILTERS: UnderAge<Filter> = UnderAge {
|
||||
_4m: Filter::Time(TimeFilter::LowerThan(UNDER_AGE_HOURS._4m)),
|
||||
_5m: Filter::Time(TimeFilter::LowerThan(UNDER_AGE_HOURS._5m)),
|
||||
_6m: Filter::Time(TimeFilter::LowerThan(UNDER_AGE_HOURS._6m)),
|
||||
_9m: Filter::Time(TimeFilter::LowerThan(UNDER_AGE_HOURS._9m)),
|
||||
_1y: Filter::Time(TimeFilter::LowerThan(UNDER_AGE_HOURS._1y)),
|
||||
_18m: Filter::Time(TimeFilter::LowerThan(UNDER_AGE_HOURS._18m)),
|
||||
_2y: Filter::Time(TimeFilter::LowerThan(UNDER_AGE_HOURS._2y)),
|
||||
_3y: Filter::Time(TimeFilter::LowerThan(UNDER_AGE_HOURS._3y)),
|
||||
_4y: Filter::Time(TimeFilter::LowerThan(UNDER_AGE_HOURS._4y)),
|
||||
@@ -61,7 +65,9 @@ pub const UNDER_AGE_NAMES: UnderAge<CohortName> = UnderAge {
|
||||
_4m: CohortName::new("under_4m_old", "<4m", "Under 4 Months Old"),
|
||||
_5m: CohortName::new("under_5m_old", "<5m", "Under 5 Months Old"),
|
||||
_6m: CohortName::new("under_6m_old", "<6m", "Under 6 Months Old"),
|
||||
_9m: CohortName::new("under_9m_old", "<9m", "Under 9 Months Old"),
|
||||
_1y: CohortName::new("under_1y_old", "<1y", "Under 1 Year Old"),
|
||||
_18m: CohortName::new("under_18m_old", "<18m", "Under 18 Months Old"),
|
||||
_2y: CohortName::new("under_2y_old", "<2y", "Under 2 Years Old"),
|
||||
_3y: CohortName::new("under_3y_old", "<3y", "Under 3 Years Old"),
|
||||
_4y: CohortName::new("under_4y_old", "<4y", "Under 4 Years Old"),
|
||||
@@ -83,7 +89,9 @@ pub struct UnderAge<T> {
|
||||
pub _4m: T,
|
||||
pub _5m: T,
|
||||
pub _6m: T,
|
||||
pub _9m: T,
|
||||
pub _1y: T,
|
||||
pub _18m: T,
|
||||
pub _2y: T,
|
||||
pub _3y: T,
|
||||
pub _4y: T,
|
||||
@@ -117,7 +125,9 @@ impl<T> UnderAge<T> {
|
||||
_4m: create(f._4m.clone(), n._4m.id),
|
||||
_5m: create(f._5m.clone(), n._5m.id),
|
||||
_6m: create(f._6m.clone(), n._6m.id),
|
||||
_9m: create(f._9m.clone(), n._9m.id),
|
||||
_1y: create(f._1y.clone(), n._1y.id),
|
||||
_18m: create(f._18m.clone(), n._18m.id),
|
||||
_2y: create(f._2y.clone(), n._2y.id),
|
||||
_3y: create(f._3y.clone(), n._3y.id),
|
||||
_4y: create(f._4y.clone(), n._4y.id),
|
||||
@@ -145,7 +155,9 @@ impl<T> UnderAge<T> {
|
||||
_4m: create(f._4m.clone(), n._4m.id)?,
|
||||
_5m: create(f._5m.clone(), n._5m.id)?,
|
||||
_6m: create(f._6m.clone(), n._6m.id)?,
|
||||
_9m: create(f._9m.clone(), n._9m.id)?,
|
||||
_1y: create(f._1y.clone(), n._1y.id)?,
|
||||
_18m: create(f._18m.clone(), n._18m.id)?,
|
||||
_2y: create(f._2y.clone(), n._2y.id)?,
|
||||
_3y: create(f._3y.clone(), n._3y.id)?,
|
||||
_4y: create(f._4y.clone(), n._4y.id)?,
|
||||
@@ -161,9 +173,9 @@ impl<T> UnderAge<T> {
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
[
|
||||
&self._1w, &self._1m, &self._2m, &self._3m, &self._4m, &self._5m, &self._6m, &self._1y,
|
||||
&self._2y, &self._3y, &self._4y, &self._5y, &self._6y, &self._7y, &self._8y,
|
||||
&self._10y, &self._12y, &self._15y,
|
||||
&self._1w, &self._1m, &self._2m, &self._3m, &self._4m, &self._5m, &self._6m, &self._9m,
|
||||
&self._1y, &self._18m, &self._2y, &self._3y, &self._4y, &self._5y, &self._6y,
|
||||
&self._7y, &self._8y, &self._10y, &self._12y, &self._15y,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
@@ -177,7 +189,9 @@ impl<T> UnderAge<T> {
|
||||
&mut self._4m,
|
||||
&mut self._5m,
|
||||
&mut self._6m,
|
||||
&mut self._9m,
|
||||
&mut self._1y,
|
||||
&mut self._18m,
|
||||
&mut self._2y,
|
||||
&mut self._3y,
|
||||
&mut self._4y,
|
||||
@@ -204,7 +218,9 @@ impl<T> UnderAge<T> {
|
||||
&mut self._4m,
|
||||
&mut self._5m,
|
||||
&mut self._6m,
|
||||
&mut self._9m,
|
||||
&mut self._1y,
|
||||
&mut self._18m,
|
||||
&mut self._2y,
|
||||
&mut self._3y,
|
||||
&mut self._4y,
|
||||
@@ -219,3 +235,25 @@ impl<T> UnderAge<T> {
|
||||
.into_par_iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::AGE_RANGE_FILTERS;
|
||||
|
||||
#[test]
|
||||
fn new_thresholds_include_only_younger_ranges() {
|
||||
assert!(UNDER_AGE_FILTERS._9m.includes(&AGE_RANGE_FILTERS._6m_to_9m));
|
||||
assert!(!UNDER_AGE_FILTERS._9m.includes(&AGE_RANGE_FILTERS._9m_to_1y));
|
||||
assert!(
|
||||
UNDER_AGE_FILTERS
|
||||
._18m
|
||||
.includes(&AGE_RANGE_FILTERS._1y_to_18m)
|
||||
);
|
||||
assert!(
|
||||
!UNDER_AGE_FILTERS
|
||||
._18m
|
||||
.includes(&AGE_RANGE_FILTERS._18m_to_2y)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ let realized_cap = computer.distribution.utxo_cohorts.all.metrics.realized.unwra
|
||||
## Cohort System
|
||||
|
||||
UTXO and address cohorts support filtering by:
|
||||
- **Age**: STH (<150d), LTH (≥150d), 21 age bands (<1h, 1h-1d, 1d-1w, 1w-1m, 1m-2m, ..., 6m-1y, 1y-2y, ..., 12y-15y, 15y+)
|
||||
- **Age**: STH (<150d), LTH (≥150d), 23 age bands (<1h, 1h-1d, 1d-1w, 1w-1m, 1m-2m, ..., 6m-9m, 9m-1y, 1y-18m, 18m-2y, ..., 12y-15y, 15y+)
|
||||
- **Amount**: 0-0.001 BTC, 0.001-0.01, ..., 10k+ BTC
|
||||
- **Type**: P2PK, P2PKH, P2MS, P2SH, P2WPKH, P2WSH, P2TR, P2A
|
||||
- **Epoch**: By halving epoch
|
||||
|
||||
@@ -37,15 +37,15 @@ use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Lengths;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Sats, Version};
|
||||
use brk_types::{Cents, Height, Sats, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
|
||||
use vecdb::{AnyStoredVec, CachedBoxedVec, Database, Exit, ReadableVec, Rw, StorageMode};
|
||||
|
||||
use super::{
|
||||
count::AddrCountFundedTotalVecs,
|
||||
supply::{AddrSupplyShareVecs, AddrSupplyVecs},
|
||||
};
|
||||
use crate::{indexes, price};
|
||||
use crate::indexes;
|
||||
|
||||
mod state;
|
||||
|
||||
@@ -66,10 +66,11 @@ impl ExposedAddrVecs {
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
count: AddrCountFundedTotalVecs::forced_import(db, "exposed", version, indexes)?,
|
||||
supply: AddrSupplyVecs::forced_import(db, "exposed", version, indexes)?,
|
||||
supply: AddrSupplyVecs::forced_import(db, "exposed", version, indexes, spot_price)?,
|
||||
supply_share: AddrSupplyShareVecs::forced_import(db, "exposed", version, indexes)?,
|
||||
})
|
||||
}
|
||||
@@ -104,14 +105,11 @@ impl ExposedAddrVecs {
|
||||
pub(crate) fn compute_rest(
|
||||
&mut self,
|
||||
starting_lengths: &Lengths,
|
||||
prices: &price::Vecs,
|
||||
all_supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.count.compute_rest(starting_lengths, exit)?;
|
||||
self.supply
|
||||
.compute_rest(starting_lengths.height, prices, exit)?;
|
||||
self.supply_share.compute_rest(
|
||||
starting_lengths.height,
|
||||
&self.supply,
|
||||
|
||||
@@ -24,9 +24,9 @@ use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Lengths;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Sats, Version};
|
||||
use brk_types::{Cents, Height, Sats, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
|
||||
use vecdb::{AnyStoredVec, CachedBoxedVec, Database, Exit, ReadableVec, Rw, StorageMode};
|
||||
|
||||
use super::{
|
||||
count::AddrCountFundedTotalVecs,
|
||||
@@ -35,7 +35,7 @@ use super::{
|
||||
use crate::{
|
||||
indexes, inputs,
|
||||
internal::{WindowStartVec, Windows},
|
||||
outputs, price,
|
||||
outputs,
|
||||
};
|
||||
|
||||
mod state;
|
||||
@@ -61,11 +61,12 @@ impl ReusedAddrVecs {
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
count: AddrCountFundedTotalVecs::forced_import(db, name, version, indexes)?,
|
||||
events: AddrEventsVecs::forced_import(db, name, version, indexes, cached_starts)?,
|
||||
supply: AddrSupplyVecs::forced_import(db, name, version, indexes)?,
|
||||
supply: AddrSupplyVecs::forced_import(db, name, version, indexes, spot_price)?,
|
||||
supply_share: AddrSupplyShareVecs::forced_import(db, name, version, indexes)?,
|
||||
})
|
||||
}
|
||||
@@ -112,7 +113,6 @@ impl ReusedAddrVecs {
|
||||
starting_lengths: &Lengths,
|
||||
outputs_by_type: &outputs::ByTypeVecs,
|
||||
inputs_by_type: &inputs::ByTypeVecs,
|
||||
prices: &price::Vecs,
|
||||
all_supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
|
||||
exit: &Exit,
|
||||
@@ -120,8 +120,6 @@ impl ReusedAddrVecs {
|
||||
self.count.compute_rest(starting_lengths, exit)?;
|
||||
self.events
|
||||
.compute_rest(starting_lengths, outputs_by_type, inputs_by_type, exit)?;
|
||||
self.supply
|
||||
.compute_rest(starting_lengths.height, prices, exit)?;
|
||||
self.supply_share.compute_rest(
|
||||
starting_lengths.height,
|
||||
&self.supply,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::Version;
|
||||
use brk_types::{Cents, Height, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{Database, Rw, StorageMode};
|
||||
use vecdb::{CachedBoxedVec, Database, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{ValuePerBlock, WithAddrTypes},
|
||||
internal::{SpotValuePerBlock, WithAddrTypes},
|
||||
};
|
||||
|
||||
use super::AddrTypeToSupply;
|
||||
@@ -17,7 +17,7 @@ use super::AddrTypeToSupply;
|
||||
/// sats × spot price.
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct AddrSupplyVecs<M: StorageMode = Rw>(
|
||||
#[traversable(flatten)] pub WithAddrTypes<ValuePerBlock<M>>,
|
||||
#[traversable(flatten)] pub WithAddrTypes<SpotValuePerBlock<M>>,
|
||||
);
|
||||
|
||||
impl AddrSupplyVecs {
|
||||
@@ -26,12 +26,14 @@ impl AddrSupplyVecs {
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(WithAddrTypes::<ValuePerBlock>::forced_import(
|
||||
Ok(Self(WithAddrTypes::<SpotValuePerBlock>::forced_import(
|
||||
db,
|
||||
&format!("{name}_addr_supply"),
|
||||
version,
|
||||
indexes,
|
||||
spot_price,
|
||||
)?))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,46 +1,31 @@
|
||||
use brk_cohort::{AmountBucket, ByAddrType};
|
||||
use brk_error::Result;
|
||||
use brk_types::{Age, Cents, CheckedSub, Height, Sats, Timestamp, TypeIndex};
|
||||
use brk_types::{Cents, Sats, TypeIndex};
|
||||
use rustc_hash::FxHashSet;
|
||||
use vecdb::VecIndex;
|
||||
|
||||
use crate::distribution::{
|
||||
addr::{AddrMetricsState, AddrSendPreState, HeightToAddrTypeToVec},
|
||||
cohorts::AddrCohorts,
|
||||
compute::PriceRangeMax,
|
||||
};
|
||||
|
||||
use super::super::cache::AddrLookup;
|
||||
|
||||
/// Process sent UTXOs for address cohorts: age metrics, cohort membership,
|
||||
/// and empty-address transitions.
|
||||
///
|
||||
/// Takes separate price/timestamp slices rather than `chain_state` so it can
|
||||
/// run in parallel with UTXO cohort processing (which mutates `chain_state`).
|
||||
/// `price_range_max` feeds peak-regret computation via max price during
|
||||
/// each UTXO's holding period.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
/// Process sent UTXOs for address cohort membership and empty-address transitions.
|
||||
pub(crate) fn process_sent(
|
||||
sent_data: HeightToAddrTypeToVec<(TypeIndex, Sats)>,
|
||||
cohorts: &mut AddrCohorts,
|
||||
lookup: &mut AddrLookup<'_>,
|
||||
current_price: Cents,
|
||||
price_range_max: &PriceRangeMax,
|
||||
state: &mut AddrMetricsState,
|
||||
received_addrs: &ByAddrType<FxHashSet<TypeIndex>>,
|
||||
height_to_price: &[Cents],
|
||||
height_to_timestamp: &[Timestamp],
|
||||
current_height: Height,
|
||||
current_timestamp: Timestamp,
|
||||
seen_senders: &mut ByAddrType<FxHashSet<TypeIndex>>,
|
||||
) -> Result<()> {
|
||||
seen_senders.values_mut().for_each(|set| set.clear());
|
||||
|
||||
for (receive_height, by_type) in sent_data.into_iter() {
|
||||
let prev_price = height_to_price[receive_height.to_usize()];
|
||||
let prev_timestamp = height_to_timestamp[receive_height.to_usize()];
|
||||
let age = Age::new(current_timestamp, prev_timestamp);
|
||||
let peak_price = price_range_max.max_between(receive_height, current_height);
|
||||
|
||||
for (output_type, vec) in by_type.unwrap().into_iter() {
|
||||
let type_received = received_addrs.get(output_type);
|
||||
@@ -51,15 +36,11 @@ pub(crate) fn process_sent(
|
||||
let pre = AddrSendPreState::capture(addr_data, output_type);
|
||||
|
||||
let prev_balance = addr_data.balance();
|
||||
let new_balance = prev_balance.checked_sub(value).unwrap();
|
||||
let is_first_encounter = type_seen.insert(type_index);
|
||||
let also_received = type_received.is_some_and(|s| s.contains(&type_index));
|
||||
let will_be_empty = addr_data.has_1_utxos();
|
||||
|
||||
let prev_bucket = AmountBucket::from(prev_balance);
|
||||
let new_bucket = AmountBucket::from(new_balance);
|
||||
let crossing_boundary = prev_bucket != new_bucket;
|
||||
|
||||
let cohort_state = cohorts
|
||||
.amount_range
|
||||
.get_mut_by_bucket(prev_bucket)
|
||||
@@ -68,7 +49,9 @@ pub(crate) fn process_sent(
|
||||
.unwrap();
|
||||
|
||||
// Mutates addr_data.spent_txo_count (+= 1). on_send_applied reads the post-spend view.
|
||||
cohort_state.send(addr_data, value, current_price, prev_price, peak_price, age)?;
|
||||
cohort_state.send(addr_data, value, current_price, prev_price)?;
|
||||
let new_bucket = AmountBucket::from(addr_data.balance());
|
||||
let crossing_boundary = prev_bucket != new_bucket;
|
||||
state.on_send_applied(
|
||||
output_type,
|
||||
addr_data,
|
||||
|
||||
@@ -4,10 +4,10 @@ use brk_cohort::{AddrGroups, AmountRange, Filter, Filtered, OverAmount, UnderAmo
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Lengths;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Sats, Version};
|
||||
use brk_types::{Cents, Height, Sats, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
|
||||
use vecdb::{AnyStoredVec, CachedBoxedVec, Database, Exit, ReadableVec, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
distribution::DynCohortVecs,
|
||||
@@ -32,6 +32,7 @@ impl AddrCohorts {
|
||||
indexes: &indexes::Vecs,
|
||||
states_path: &Path,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let v = version + VERSION;
|
||||
|
||||
@@ -39,7 +40,16 @@ impl AddrCohorts {
|
||||
let create =
|
||||
|filter: Filter, name: &'static str, has_state: bool| -> Result<AddrCohortVecs> {
|
||||
let sp = if has_state { Some(states_path) } else { None };
|
||||
AddrCohortVecs::forced_import(db, filter, name, v, indexes, sp, cached_starts)
|
||||
AddrCohortVecs::forced_import(
|
||||
db,
|
||||
filter,
|
||||
name,
|
||||
v,
|
||||
indexes,
|
||||
sp,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
)
|
||||
};
|
||||
|
||||
let full = |f: Filter, name: &'static str| create(f, name, true);
|
||||
@@ -108,14 +118,13 @@ impl AddrCohorts {
|
||||
/// Second phase of post-processing: compute relative metrics.
|
||||
pub(crate) fn compute_rest_part2(
|
||||
&mut self,
|
||||
prices: &price::Vecs,
|
||||
starting_lengths: &Lengths,
|
||||
all_supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.0
|
||||
.par_iter_mut()
|
||||
.try_for_each(|v| v.compute_rest_part2(prices, starting_lengths, all_supply_sats, exit))
|
||||
.try_for_each(|v| v.compute_rest_part2(starting_lengths, all_supply_sats, exit))
|
||||
}
|
||||
|
||||
/// Returns a parallel iterator over all vecs for parallel writing.
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
use brk_cohort::Filter;
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Lengths;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Sats};
|
||||
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
distribution::metrics::{
|
||||
ActivityMinimal, ImportConfig, OutputsUnspent, RealizedBase, SupplyBase,
|
||||
},
|
||||
price,
|
||||
};
|
||||
|
||||
/// Address-balance metrics: holdings plus economically meaningful flows.
|
||||
///
|
||||
/// Address cohorts intentionally omit spent-output counts, realized price,
|
||||
/// MVRV, and NUPL.
|
||||
#[derive(Traversable)]
|
||||
pub struct AddrCohortMetrics<M: StorageMode = Rw> {
|
||||
#[traversable(skip)]
|
||||
pub filter: Filter,
|
||||
pub supply: Box<SupplyBase<M>>,
|
||||
pub outputs: Box<OutputsUnspent<M>>,
|
||||
pub activity: Box<ActivityMinimal<M>>,
|
||||
pub realized: Box<RealizedBase<M>>,
|
||||
}
|
||||
|
||||
impl AddrCohortMetrics {
|
||||
pub(super) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
|
||||
Ok(Self {
|
||||
filter: cfg.filter.clone(),
|
||||
supply: Box::new(SupplyBase::forced_import(cfg)?),
|
||||
outputs: Box::new(OutputsUnspent::forced_import(cfg)?),
|
||||
activity: Box::new(ActivityMinimal::forced_import(cfg)?),
|
||||
realized: Box::new(RealizedBase::forced_import(cfg)?),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn min_stateful_len(&self) -> usize {
|
||||
self.supply
|
||||
.min_len()
|
||||
.min(self.outputs.min_len())
|
||||
.min(self.activity.min_len())
|
||||
.min(self.realized.min_stateful_len())
|
||||
}
|
||||
|
||||
pub(super) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
let mut vecs = Vec::new();
|
||||
vecs.extend(self.supply.collect_vecs_mut());
|
||||
vecs.extend(self.outputs.collect_vecs_mut());
|
||||
vecs.extend(self.activity.collect_vecs_mut());
|
||||
vecs.extend(self.realized.collect_vecs_mut());
|
||||
vecs
|
||||
}
|
||||
|
||||
pub(super) fn compute_from_sources(
|
||||
&mut self,
|
||||
starting_lengths: &Lengths,
|
||||
others: &[&Self],
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.supply.compute_from_stateful(
|
||||
starting_lengths,
|
||||
&others.iter().map(|v| v.supply.as_ref()).collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
self.outputs.compute_from_stateful(
|
||||
starting_lengths,
|
||||
&others
|
||||
.iter()
|
||||
.map(|v| v.outputs.as_ref())
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
self.activity.compute_from_stateful(
|
||||
starting_lengths,
|
||||
&others
|
||||
.iter()
|
||||
.map(|v| v.activity.as_ref())
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
self.realized.compute_from_stateful(
|
||||
starting_lengths,
|
||||
&others
|
||||
.iter()
|
||||
.map(|v| v.realized.as_ref())
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn compute_rest_part1(
|
||||
&mut self,
|
||||
prices: &price::Vecs,
|
||||
starting_lengths: &Lengths,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.activity
|
||||
.compute_rest_part1(prices, starting_lengths, exit)
|
||||
}
|
||||
|
||||
pub(super) fn compute_rest_part2(
|
||||
&mut self,
|
||||
starting_lengths: &Lengths,
|
||||
all_supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.supply
|
||||
.compute_dominance(starting_lengths.height, all_supply_sats, exit)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
mod groups;
|
||||
mod metrics;
|
||||
mod vecs;
|
||||
|
||||
pub use groups::*;
|
||||
|
||||
@@ -6,27 +6,31 @@ use brk_indexer::Lengths;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Cents, Height, PartsPerMillionSigned64, Sats, StoredI64, StoredU64, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec};
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, CachedBoxedVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
distribution::state::{AddrCohortState, MinimalRealizedState},
|
||||
distribution::state::AddrCohortState,
|
||||
indexes,
|
||||
internal::{PerBlockWithDeltas, WindowStartVec, Windows},
|
||||
price,
|
||||
};
|
||||
|
||||
use crate::distribution::metrics::{ImportConfig, MinimalCohortMetrics};
|
||||
use crate::distribution::metrics::ImportConfig;
|
||||
|
||||
use super::super::traits::{CohortVecs, DynCohortVecs};
|
||||
use super::metrics::AddrCohortMetrics;
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct AddrCohortVecs<M: StorageMode = Rw> {
|
||||
starting_height: Option<Height>,
|
||||
|
||||
#[traversable(skip)]
|
||||
pub state: Option<Box<AddrCohortState<MinimalRealizedState>>>,
|
||||
pub state: Option<Box<AddrCohortState>>,
|
||||
|
||||
#[traversable(flatten)]
|
||||
pub metrics: MinimalCohortMetrics<M>,
|
||||
pub metrics: AddrCohortMetrics<M>,
|
||||
|
||||
pub addr_count: PerBlockWithDeltas<StoredU64, StoredI64, PartsPerMillionSigned64, M>,
|
||||
}
|
||||
@@ -40,6 +44,7 @@ impl AddrCohortVecs {
|
||||
indexes: &indexes::Vecs,
|
||||
states_path: Option<&Path>,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let full_name = CohortContext::Addr.full_name(&filter, name);
|
||||
|
||||
@@ -50,6 +55,7 @@ impl AddrCohortVecs {
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
};
|
||||
|
||||
let addr_count = PerBlockWithDeltas::forced_import(
|
||||
@@ -64,7 +70,7 @@ impl AddrCohortVecs {
|
||||
Ok(Self {
|
||||
starting_height: None,
|
||||
state: states_path.map(|path| Box::new(AddrCohortState::new(path, &full_name))),
|
||||
metrics: MinimalCohortMetrics::forced_import(&cfg)?,
|
||||
metrics: AddrCohortMetrics::forced_import(&cfg)?,
|
||||
addr_count,
|
||||
})
|
||||
}
|
||||
@@ -229,12 +235,11 @@ impl CohortVecs for AddrCohortVecs {
|
||||
|
||||
fn compute_rest_part2(
|
||||
&mut self,
|
||||
prices: &price::Vecs,
|
||||
starting_lengths: &Lengths,
|
||||
all_supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.metrics
|
||||
.compute_rest_part2(prices, starting_lengths, all_supply_sats, exit)
|
||||
.compute_rest_part2(starting_lengths, all_supply_sats, exit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ pub trait CohortVecs: DynCohortVecs {
|
||||
/// Second phase of post-processing computations.
|
||||
fn compute_rest_part2(
|
||||
&mut self,
|
||||
prices: &price::Vecs,
|
||||
starting_lengths: &Lengths,
|
||||
all_supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
exit: &Exit,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use brk_cohort::{Filter, PROFITABILITY_RANGE_COUNT, compute_profitability_boundaries};
|
||||
use brk_cohort::{
|
||||
AGE_RANGE_COUNT, Filter, PROFITABILITY_RANGE_COUNT, compute_profitability_boundaries,
|
||||
};
|
||||
use brk_types::{Cents, CentsCompact, PartsPerMillion32, Sats};
|
||||
|
||||
use crate::{
|
||||
@@ -11,9 +13,6 @@ use crate::{
|
||||
|
||||
use super::COST_BASIS_PRICE_DIGITS;
|
||||
|
||||
/// Number of age range cohorts (21: 20 boundaries + 1 unbounded).
|
||||
const AGE_RANGE_COUNT: usize = 21;
|
||||
|
||||
// Tier boundaries for 5-significant-digit dollar bucketing.
|
||||
// Matches the rounding used by `Cents::round_to_dollar(5)`.
|
||||
const TIER0_COUNT: usize = 100_000; // $0-$99,999 exact dollars
|
||||
@@ -25,15 +24,13 @@ const TIER1_START: usize = TIER0_COUNT;
|
||||
/// Total number of buckets.
|
||||
const TREE_SIZE: usize = TIER0_COUNT + TIER1_COUNT + OVERFLOW; // 190,001
|
||||
|
||||
/// 4-field Fenwick tree node for combined cost basis tracking.
|
||||
/// Fenwick tree node for combined cost basis tracking.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub(super) struct CostBasisNode {
|
||||
all_sats: i64,
|
||||
sth_sats: i64,
|
||||
discount_sats: i64,
|
||||
all_usd: i128,
|
||||
sth_usd: i128,
|
||||
discount_usd: i128,
|
||||
}
|
||||
|
||||
impl CostBasisNode {
|
||||
@@ -42,22 +39,8 @@ impl CostBasisNode {
|
||||
Self {
|
||||
all_sats: sats,
|
||||
sth_sats: if is_sth { sats } else { 0 },
|
||||
discount_sats: 0,
|
||||
all_usd: usd,
|
||||
sth_usd: if is_sth { usd } else { 0 },
|
||||
discount_usd: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn new_discount(sats: i64, usd: i128) -> Self {
|
||||
Self {
|
||||
all_sats: 0,
|
||||
sth_sats: 0,
|
||||
discount_sats: sats,
|
||||
all_usd: 0,
|
||||
sth_usd: 0,
|
||||
discount_usd: usd,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,10 +50,8 @@ impl FenwickNode for CostBasisNode {
|
||||
fn add_assign(&mut self, other: &Self) {
|
||||
self.all_sats += other.all_sats;
|
||||
self.sth_sats += other.sth_sats;
|
||||
self.discount_sats += other.discount_sats;
|
||||
self.all_usd += other.all_usd;
|
||||
self.sth_usd += other.sth_usd;
|
||||
self.discount_usd += other.discount_usd;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,29 +155,10 @@ impl CostBasisFenwick {
|
||||
self.totals.add_assign(&delta);
|
||||
}
|
||||
|
||||
/// Apply a net delta from the discount-entry cohort.
|
||||
///
|
||||
/// Supply totals are maintained from the age-range cohorts; this updates
|
||||
/// only the discount-entry partition so premium can be derived as all - discount.
|
||||
pub(super) fn apply_discount_delta(&mut self, price: CentsCompact, pending: &PendingDelta) {
|
||||
let net_sats = u64::from(pending.inc) as i64 - u64::from(pending.dec) as i64;
|
||||
if net_sats == 0 {
|
||||
return;
|
||||
}
|
||||
let bucket = price_to_bucket(price);
|
||||
let delta =
|
||||
CostBasisNode::new_discount(net_sats, price.as_u128() as i128 * net_sats as i128);
|
||||
self.tree.add(bucket, &delta);
|
||||
self.totals.add_assign(&delta);
|
||||
}
|
||||
|
||||
/// Bulk-initialize from age-range maps plus the discount-entry map.
|
||||
/// Age-range maps maintain all/STH/LTH totals; the discount-entry map
|
||||
/// maintains only the discount partition used to derive premium.
|
||||
pub(super) fn bulk_init_with_discount<'a>(
|
||||
/// Bulk-initialize from age-range maps.
|
||||
pub(super) fn bulk_init<'a>(
|
||||
&mut self,
|
||||
maps: impl Iterator<Item = (&'a std::collections::BTreeMap<CentsCompact, Sats>, bool)>,
|
||||
discount_maps: impl Iterator<Item = &'a std::collections::BTreeMap<CentsCompact, Sats>>,
|
||||
) {
|
||||
self.tree.reset();
|
||||
self.totals = CostBasisNode::default();
|
||||
@@ -212,15 +174,6 @@ impl CostBasisFenwick {
|
||||
}
|
||||
}
|
||||
|
||||
for map in discount_maps {
|
||||
for (&price, &sats) in map.iter() {
|
||||
let bucket = price_to_bucket(price);
|
||||
let s = u64::from(sats) as i64;
|
||||
let node = CostBasisNode::new_discount(s, price.as_u128() as i128 * s as i128);
|
||||
self.tree.add_raw(bucket, &node);
|
||||
self.totals.add_assign(&node);
|
||||
}
|
||||
}
|
||||
self.tree.build_in_place();
|
||||
self.initialized = true;
|
||||
}
|
||||
@@ -259,26 +212,6 @@ impl CostBasisFenwick {
|
||||
)
|
||||
}
|
||||
|
||||
/// Compute percentile prices for discount-entry cohort.
|
||||
pub(super) fn percentiles_discount_entry(&self) -> PercentileResult {
|
||||
self.compute_percentiles(
|
||||
self.totals.discount_sats,
|
||||
self.totals.discount_usd,
|
||||
|n| n.discount_sats,
|
||||
|n| n.discount_usd,
|
||||
)
|
||||
}
|
||||
|
||||
/// Compute percentile prices for premium-entry cohort (all - discount).
|
||||
pub(super) fn percentiles_premium_entry(&self) -> PercentileResult {
|
||||
self.compute_percentiles(
|
||||
self.totals.all_sats - self.totals.discount_sats,
|
||||
self.totals.all_usd - self.totals.discount_usd,
|
||||
|n| n.all_sats - n.discount_sats,
|
||||
|n| n.all_usd - n.discount_usd,
|
||||
)
|
||||
}
|
||||
|
||||
fn compute_percentiles(
|
||||
&self,
|
||||
total_sats: i64,
|
||||
@@ -357,26 +290,6 @@ impl CostBasisFenwick {
|
||||
)
|
||||
}
|
||||
|
||||
/// Compute supply density for entry cohorts: (discount, premium).
|
||||
pub(super) fn entry_density(
|
||||
&self,
|
||||
spot_price: Cents,
|
||||
) -> (PartsPerMillion32, PartsPerMillion32) {
|
||||
if self.totals.all_sats <= 0 {
|
||||
return (PartsPerMillion32::ZERO, PartsPerMillion32::ZERO);
|
||||
}
|
||||
|
||||
let range = self.density_range(spot_price);
|
||||
let discount_range = range.discount_sats.max(0);
|
||||
let premium_range = range.all_sats.max(0) - discount_range;
|
||||
let premium_total = self.totals.all_sats - self.totals.discount_sats;
|
||||
|
||||
(
|
||||
Self::to_ppm(discount_range, self.totals.discount_sats),
|
||||
Self::to_ppm(premium_range, premium_total),
|
||||
)
|
||||
}
|
||||
|
||||
fn density_range(&self, spot_price: Cents) -> CostBasisNode {
|
||||
let spot_f64 = u64::from(spot_price) as f64;
|
||||
let low = Cents::from((spot_f64 * 0.95) as u64);
|
||||
@@ -395,10 +308,8 @@ impl CostBasisFenwick {
|
||||
CostBasisNode {
|
||||
all_sats: cum_high.all_sats - cum_low.all_sats,
|
||||
sth_sats: cum_high.sth_sats - cum_low.sth_sats,
|
||||
discount_sats: cum_high.discount_sats - cum_low.discount_sats,
|
||||
all_usd: cum_high.all_usd - cum_low.all_usd,
|
||||
sth_usd: cum_high.sth_usd - cum_low.sth_usd,
|
||||
discount_usd: cum_high.discount_usd - cum_low.discount_usd,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::path::Path;
|
||||
|
||||
use brk_cohort::{
|
||||
AgeRange, AmountRange, ByEntry, ByEpoch, Class, CohortContext, Filter, Filtered, OverAge,
|
||||
OverAmount, SpendableType, Term, UnderAge, UnderAmount,
|
||||
AGE_RANGE_COUNT, AgeRange, AmountRange, ByEntry, ByEpoch, Class, CohortContext, Filter,
|
||||
Filtered, OverAge, OverAmount, SpendableType, Term, UnderAge, UnderAmount,
|
||||
};
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Lengths;
|
||||
@@ -10,7 +10,8 @@ use brk_traversable::Traversable;
|
||||
use brk_types::{Cents, CentsSquaredSats, Dollars, Height, Sats, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, Database, Exit, ReadOnlyClone, ReadableVec, Rw, StorageMode, WritableVec,
|
||||
AnyStoredVec, AnyVec, CachedBoxedVec, Database, Exit, ReadOnlyClone, ReadableVec, Rw,
|
||||
StorageMode, WritableVec,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -44,7 +45,7 @@ pub struct UTXOCohorts<M: StorageMode = Rw> {
|
||||
pub over_age: OverAge<UTXOCohortVecs<CoreCohortMetrics<M>>>,
|
||||
pub epoch: ByEpoch<UTXOCohortVecs<CoreCohortMetrics<M>>>,
|
||||
pub class: Class<UTXOCohortVecs<CoreCohortMetrics<M>>>,
|
||||
pub entry: ByEntry<UTXOCohortVecs<ExtendedCohortMetrics<M>>>,
|
||||
pub entry: ByEntry<UTXOCohortVecs<CoreCohortMetrics<M>>>,
|
||||
pub over_amount: OverAmount<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
|
||||
pub amount_range: AmountRange<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
|
||||
pub under_amount: UnderAmount<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
|
||||
@@ -63,14 +64,14 @@ pub(crate) struct UTXOCohortsTransientState {
|
||||
/// Cached partition_point positions for tick_tock boundary searches.
|
||||
/// Avoids O(log n) binary search per boundary per block; scans forward
|
||||
/// from last known position (typically O(1) per boundary).
|
||||
pub(super) tick_tock_cached_positions: [usize; 20],
|
||||
pub(super) tick_tock_cached_positions: [usize; AGE_RANGE_COUNT - 1],
|
||||
}
|
||||
|
||||
impl UTXOCohorts<Rw> {
|
||||
/// Separate cohorts currently total 72:
|
||||
/// 21 age + 5 epoch + 18 class + 2 entry + 15 amount + 11 spendable type.
|
||||
/// Separate cohorts currently total 74:
|
||||
/// 23 age + 5 epoch + 18 class + 2 entry + 15 amount + 11 spendable type.
|
||||
/// Keep small headroom because this is only Vec allocation capacity.
|
||||
const SEPARATE_COHORT_CAPACITY: usize = 82;
|
||||
const SEPARATE_COHORT_CAPACITY: usize = 84;
|
||||
|
||||
/// Import all UTXO cohorts from database.
|
||||
pub(crate) fn forced_import(
|
||||
@@ -79,6 +80,7 @@ impl UTXOCohorts<Rw> {
|
||||
indexes: &indexes::Vecs,
|
||||
states_path: &Path,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let v = version + VERSION;
|
||||
|
||||
@@ -91,6 +93,7 @@ impl UTXOCohorts<Rw> {
|
||||
version: v + Version::ONE,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
};
|
||||
let all_supply = SupplyCore::forced_import(&all_cfg)?;
|
||||
|
||||
@@ -107,6 +110,7 @@ impl UTXOCohorts<Rw> {
|
||||
version: v,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
};
|
||||
let state = Some(Box::new(UTXOCohortState::new(states_path, &full_name)));
|
||||
Ok(UTXOCohortVecs::new(
|
||||
@@ -127,6 +131,7 @@ impl UTXOCohorts<Rw> {
|
||||
version: v,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
};
|
||||
let state = Some(Box::new(UTXOCohortState::new(states_path, &full_name)));
|
||||
Ok(UTXOCohortVecs::new(
|
||||
@@ -138,25 +143,7 @@ impl UTXOCohorts<Rw> {
|
||||
let epoch = ByEpoch::try_new(&core_separate)?;
|
||||
let class = Class::try_new(&core_separate)?;
|
||||
|
||||
let extended_separate =
|
||||
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<ExtendedCohortMetrics>> {
|
||||
let full_name = CohortContext::Utxo.full_name(&f, name);
|
||||
let cfg = ImportConfig {
|
||||
db,
|
||||
filter: &f,
|
||||
full_name: &full_name,
|
||||
version: v,
|
||||
indexes,
|
||||
cached_starts,
|
||||
};
|
||||
let state = Some(Box::new(UTXOCohortState::new(states_path, &full_name)));
|
||||
Ok(UTXOCohortVecs::new(
|
||||
state,
|
||||
ExtendedCohortMetrics::forced_import(&cfg)?,
|
||||
))
|
||||
};
|
||||
|
||||
let entry = ByEntry::try_new(&extended_separate)?;
|
||||
let entry = ByEntry::try_new(&core_separate)?;
|
||||
|
||||
// Helper for separate cohorts with MinimalCohortMetrics + MinimalRealizedState
|
||||
let minimal_separate =
|
||||
@@ -169,6 +156,7 @@ impl UTXOCohorts<Rw> {
|
||||
version: v,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
};
|
||||
let state = Some(Box::new(UTXOCohortState::new(states_path, &full_name)));
|
||||
Ok(UTXOCohortVecs::new(
|
||||
@@ -189,6 +177,7 @@ impl UTXOCohorts<Rw> {
|
||||
version: v,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
};
|
||||
let state = Some(Box::new(UTXOCohortState::new(states_path, &full_name)));
|
||||
Ok(UTXOCohortVecs::new(
|
||||
@@ -206,7 +195,8 @@ impl UTXOCohorts<Rw> {
|
||||
);
|
||||
|
||||
// Phase 3b: Import profitability metrics (derived from "all" during k-way merge).
|
||||
let profitability = ProfitabilityMetrics::forced_import(db, v, indexes, cached_starts)?;
|
||||
let profitability =
|
||||
ProfitabilityMetrics::forced_import(db, v, indexes, cached_starts, spot_price)?;
|
||||
|
||||
// Phase 4: Import aggregate cohorts.
|
||||
|
||||
@@ -221,6 +211,7 @@ impl UTXOCohorts<Rw> {
|
||||
version: v,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
};
|
||||
UTXOCohortVecs::new(None, ExtendedAdjustedCohortMetrics::forced_import(&cfg)?)
|
||||
};
|
||||
@@ -236,6 +227,7 @@ impl UTXOCohorts<Rw> {
|
||||
version: v,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
};
|
||||
UTXOCohortVecs::new(None, ExtendedCohortMetrics::forced_import(&cfg)?)
|
||||
};
|
||||
@@ -251,6 +243,7 @@ impl UTXOCohorts<Rw> {
|
||||
version: v,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
};
|
||||
Ok(UTXOCohortVecs::new(
|
||||
None,
|
||||
@@ -274,6 +267,7 @@ impl UTXOCohorts<Rw> {
|
||||
version: v,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
};
|
||||
Ok(UTXOCohortVecs::new(
|
||||
None,
|
||||
@@ -332,7 +326,6 @@ impl UTXOCohorts<Rw> {
|
||||
sth,
|
||||
caches,
|
||||
age_range,
|
||||
entry,
|
||||
..
|
||||
} = self;
|
||||
caches
|
||||
@@ -351,15 +344,7 @@ impl UTXOCohorts<Rw> {
|
||||
Some((map, caches.fenwick.is_sth_at(i)))
|
||||
})
|
||||
.collect();
|
||||
let discount_maps = entry
|
||||
.discount
|
||||
.state
|
||||
.as_ref()
|
||||
.map(|state| state.cost_basis_map())
|
||||
.into_iter();
|
||||
caches
|
||||
.fenwick
|
||||
.bulk_init_with_discount(maps.into_iter(), discount_maps);
|
||||
caches.fenwick.bulk_init(maps.into_iter());
|
||||
}
|
||||
|
||||
/// Apply pending deltas from all age-range cohorts to the Fenwick tree.
|
||||
@@ -370,10 +355,7 @@ impl UTXOCohorts<Rw> {
|
||||
}
|
||||
// Destructure to get separate borrows on caches and age_range
|
||||
let Self {
|
||||
caches,
|
||||
age_range,
|
||||
entry,
|
||||
..
|
||||
caches, age_range, ..
|
||||
} = self;
|
||||
for (i, sub) in age_range.iter().enumerate() {
|
||||
if let Some(state) = sub.state.as_ref() {
|
||||
@@ -383,11 +365,6 @@ impl UTXOCohorts<Rw> {
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(state) = entry.discount.state.as_ref() {
|
||||
state.for_each_cost_basis_pending(|&price, delta| {
|
||||
caches.fenwick.apply_discount_delta(price, delta);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Push maturation sats to the matured vecs for the given height.
|
||||
@@ -712,13 +689,8 @@ impl UTXOCohorts<Rw> {
|
||||
}),
|
||||
Box::new(|| {
|
||||
entry.par_iter_mut().try_for_each(|v| {
|
||||
v.metrics.compute_rest_part2(
|
||||
prices,
|
||||
starting_lengths,
|
||||
height_to_market_cap,
|
||||
ss,
|
||||
exit,
|
||||
)
|
||||
v.metrics
|
||||
.compute_rest_part2(prices, starting_lengths, ss, exit)
|
||||
})
|
||||
}),
|
||||
Box::new(|| {
|
||||
|
||||
@@ -50,22 +50,6 @@ impl UTXOCohorts {
|
||||
let lth = self.caches.fenwick.percentiles_lth();
|
||||
push_cost_basis(<h, lth_d, &mut self.lth.metrics.cost_basis);
|
||||
|
||||
let (discount_d, premium_d) = self.caches.fenwick.entry_density(spot_price);
|
||||
|
||||
let discount = self.caches.fenwick.percentiles_discount_entry();
|
||||
push_cost_basis(
|
||||
&discount,
|
||||
discount_d,
|
||||
&mut self.entry.discount.metrics.cost_basis,
|
||||
);
|
||||
|
||||
let premium = self.caches.fenwick.percentiles_premium_entry();
|
||||
push_cost_basis(
|
||||
&premium,
|
||||
premium_d,
|
||||
&mut self.entry.premium.metrics.cost_basis,
|
||||
);
|
||||
|
||||
let prof = self.caches.fenwick.profitability(spot_price);
|
||||
push_profitability(&prof, &mut self.profitability);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use brk_cohort::{AGE_BOUNDARIES, AgeRange};
|
||||
use brk_cohort::{AGE_BOUNDARIES, AGE_RANGE_COUNT, AgeRange};
|
||||
use brk_types::{CostBasisSnapshot, ONE_HOUR_IN_SEC, Sats, Timestamp};
|
||||
use vecdb::{Rw, unlikely};
|
||||
|
||||
@@ -14,7 +14,7 @@ impl UTXOCohorts<Rw> {
|
||||
///
|
||||
/// Uses cached positions per boundary to avoid binary search.
|
||||
/// Since timestamps are monotonic, positions only advance forward.
|
||||
/// Complexity: O(k * c) where k = 20 boundaries, c = ~1 (forward scan steps).
|
||||
/// Complexity: O(k * c), where k is the boundary count and c is ~1 forward scan step.
|
||||
///
|
||||
/// Returns how many sats matured OUT OF each cohort into the older adjacent one.
|
||||
/// `over_15y` is always zero since nothing ages out of the oldest cohort.
|
||||
@@ -35,12 +35,12 @@ impl UTXOCohorts<Rw> {
|
||||
return AgeRange::default();
|
||||
}
|
||||
|
||||
let mut matured = [Sats::ZERO; 21];
|
||||
let mut matured = [Sats::ZERO; AGE_RANGE_COUNT];
|
||||
|
||||
// Get age_range cohort states (indexed 0..21)
|
||||
// Get age_range cohort states (indexed 0..AGE_RANGE_COUNT)
|
||||
// Cohort i covers hours [BOUNDARIES[i-1], BOUNDARIES[i])
|
||||
// Cohort 0 covers [0, 1) hours
|
||||
// Cohort 20 covers [15*365*24, infinity) hours
|
||||
// The final cohort covers [15*365*24, infinity) hours
|
||||
let mut age_cohorts: Vec<_> = self.age_range.iter_mut().map(|v| &mut v.state).collect();
|
||||
let cached = &mut self.caches.tick_tock_cached_positions;
|
||||
|
||||
|
||||
@@ -74,7 +74,6 @@ pub(crate) fn process_blocks(
|
||||
let tx_index_to_input_count = &indexes.tx_index.input_count;
|
||||
|
||||
let height_to_price_vec = cached_prices;
|
||||
let height_to_timestamp_vec = cached_timestamps;
|
||||
|
||||
let start_usize = starting_height.to_usize();
|
||||
let end_usize = last_height.to_usize() + 1;
|
||||
@@ -440,13 +439,9 @@ pub(crate) fn process_blocks(
|
||||
&mut vecs.addr_cohorts,
|
||||
&mut lookup,
|
||||
block_price,
|
||||
ctx.price_range_max,
|
||||
&mut state,
|
||||
&received_addrs,
|
||||
height_to_price_vec,
|
||||
height_to_timestamp_vec,
|
||||
height,
|
||||
timestamp,
|
||||
&mut seen_senders,
|
||||
)
|
||||
},
|
||||
|
||||
@@ -109,8 +109,6 @@ impl CoreCohortMetrics {
|
||||
starting_lengths: &Lengths,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.supply.compute(prices, starting_lengths.height, exit)?;
|
||||
|
||||
self.activity
|
||||
.compute_rest_part1(prices, starting_lengths, exit)?;
|
||||
|
||||
|
||||
@@ -104,7 +104,6 @@ impl MinimalCohortMetrics {
|
||||
starting_lengths: &Lengths,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.supply.compute(prices, starting_lengths.height, exit)?;
|
||||
self.activity
|
||||
.compute_rest_part1(prices, starting_lengths, exit)?;
|
||||
Ok(())
|
||||
|
||||
@@ -66,7 +66,6 @@ impl TypeCohortMetrics {
|
||||
starting_lengths: &Lengths,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.supply.compute(prices, starting_lengths.height, exit)?;
|
||||
self.activity
|
||||
.compute_rest_part1(prices, starting_lengths, exit)?;
|
||||
Ok(())
|
||||
|
||||
@@ -4,7 +4,7 @@ use brk_types::{
|
||||
Cents, Height, PartsPerMillion32, PartsPerMillionSigned32, PartsPerMillionSigned64, Version,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use vecdb::{BytesVec, BytesVecValue, Database, ImportableVec};
|
||||
use vecdb::{BytesVec, BytesVecValue, CachedBoxedVec, Database, ImportableVec};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
FiatPerBlock, FiatPerBlockCumulativeWithSums, FiatType, NumericValue, PerBlock,
|
||||
PerBlockCumulativeRolling, PercentPerBlock, PercentRollingWindows, Price,
|
||||
PriceWithRatioPerBlock, RatioPerBlock, RollingWindow24hPerBlock, RollingWindows,
|
||||
RollingWindowsFrom1w, ValuePerBlock, ValuePerBlockCumulative,
|
||||
RollingWindowsFrom1w, SpotValuePerBlock, ValuePerBlock, ValuePerBlockCumulative,
|
||||
ValuePerBlockCumulativeRolling, WindowStartVec, Windows,
|
||||
},
|
||||
};
|
||||
@@ -39,7 +39,6 @@ macro_rules! impl_config_import {
|
||||
impl_config_import!(
|
||||
ValuePerBlock,
|
||||
ValuePerBlockCumulative,
|
||||
PriceWithRatioPerBlock,
|
||||
RatioPerBlock<PartsPerMillionSigned32>,
|
||||
PercentPerBlock<PartsPerMillion32>,
|
||||
PercentPerBlock<PartsPerMillionSigned32>,
|
||||
@@ -48,6 +47,30 @@ impl_config_import!(
|
||||
Price<PerBlock<Cents>>,
|
||||
);
|
||||
|
||||
impl ConfigImport for PriceWithRatioPerBlock {
|
||||
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
|
||||
Self::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name(suffix),
|
||||
cfg.version + offset,
|
||||
cfg.indexes,
|
||||
cfg.spot_price,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigImport for SpotValuePerBlock {
|
||||
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
|
||||
Self::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name(suffix),
|
||||
cfg.version + offset,
|
||||
cfg.indexes,
|
||||
cfg.spot_price,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Generic types (macro_rules can't parse generic bounds, so written out)
|
||||
impl<T: NumericValue + JsonSchema> ConfigImport for PerBlock<T> {
|
||||
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
|
||||
@@ -128,6 +151,7 @@ pub struct ImportConfig<'a> {
|
||||
pub version: Version,
|
||||
pub indexes: &'a indexes::Vecs,
|
||||
pub cached_starts: &'a Windows<&'a WindowStartVec>,
|
||||
pub spot_price: &'a CachedBoxedVec<Height, Cents>,
|
||||
}
|
||||
|
||||
impl<'a> ImportConfig<'a> {
|
||||
|
||||
@@ -127,10 +127,11 @@ pub use cohort::{
|
||||
};
|
||||
pub use config::ImportConfig;
|
||||
pub use cost_basis::CostBasis;
|
||||
pub use outputs::OutputsBase;
|
||||
pub use outputs::{OutputsBase, OutputsUnspent};
|
||||
pub use profitability::ProfitabilityMetrics;
|
||||
pub use realized::{
|
||||
AdjustedSopr, RealizedCore, RealizedFull, RealizedFullAccum, RealizedLike, RealizedMinimal,
|
||||
AdjustedSopr, RealizedBase, RealizedCore, RealizedFull, RealizedFullAccum, RealizedLike,
|
||||
RealizedMinimal,
|
||||
};
|
||||
pub use relative::{RelativeForAll, RelativeWithExtended};
|
||||
pub use supply::{AvgAmountMetrics, SupplyBase, SupplyCore};
|
||||
@@ -274,8 +275,6 @@ pub trait CohortMetricsBase:
|
||||
starting_lengths: &Lengths,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.supply_mut()
|
||||
.compute(prices, starting_lengths.height, exit)?;
|
||||
self.activity_mut()
|
||||
.compute_rest_part1(prices, starting_lengths, exit)?;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use brk_error::Result;
|
||||
use brk_indexer::Lengths;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{PartsPerMillionSigned64, StoredI64, StoredU64, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
|
||||
|
||||
use crate::{
|
||||
@@ -12,16 +13,14 @@ use crate::{
|
||||
internal::{PerBlockCumulativeRolling, PerBlockWithDeltas},
|
||||
};
|
||||
|
||||
/// Base output metrics: utxo_count + delta.
|
||||
/// Unspent output metrics.
|
||||
#[derive(Traversable)]
|
||||
pub struct OutputsBase<M: StorageMode = Rw> {
|
||||
pub struct OutputsUnspent<M: StorageMode = Rw> {
|
||||
pub unspent_count: PerBlockWithDeltas<StoredU64, StoredI64, PartsPerMillionSigned64, M>,
|
||||
pub spent_count: PerBlockCumulativeRolling<StoredU64, M>,
|
||||
}
|
||||
|
||||
impl OutputsBase {
|
||||
impl OutputsUnspent {
|
||||
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
|
||||
let v1 = Version::ONE;
|
||||
Ok(Self {
|
||||
unspent_count: PerBlockWithDeltas::forced_import(
|
||||
cfg.db,
|
||||
@@ -31,15 +30,11 @@ impl OutputsBase {
|
||||
cfg.indexes,
|
||||
cfg.cached_starts,
|
||||
)?,
|
||||
spent_count: cfg.import("spent_utxo_count", v1)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn min_len(&self) -> usize {
|
||||
self.unspent_count
|
||||
.height
|
||||
.len()
|
||||
.min(self.spent_count.block.len())
|
||||
self.unspent_count.height.len()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
@@ -47,15 +42,10 @@ impl OutputsBase {
|
||||
self.unspent_count
|
||||
.height
|
||||
.push(StoredU64::from(state.supply.utxo_count));
|
||||
self.spent_count
|
||||
.push_block(StoredU64::from(state.spent_utxo_count));
|
||||
}
|
||||
|
||||
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
vec![
|
||||
&mut self.unspent_count.height as &mut dyn AnyStoredVec,
|
||||
self.spent_count.stored_mut(),
|
||||
]
|
||||
vec![&mut self.unspent_count.height as &mut dyn AnyStoredVec]
|
||||
}
|
||||
|
||||
pub(crate) fn compute_from_stateful(
|
||||
@@ -72,6 +62,55 @@ impl OutputsBase {
|
||||
.collect::<Vec<_>>(),
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Base output metrics: unspent and spent output counts.
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct OutputsBase<M: StorageMode = Rw> {
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
#[traversable(flatten)]
|
||||
pub unspent: OutputsUnspent<M>,
|
||||
pub spent_count: PerBlockCumulativeRolling<StoredU64, M>,
|
||||
}
|
||||
|
||||
impl OutputsBase {
|
||||
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
|
||||
let v1 = Version::ONE;
|
||||
Ok(Self {
|
||||
unspent: OutputsUnspent::forced_import(cfg)?,
|
||||
spent_count: cfg.import("spent_utxo_count", v1)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn min_len(&self) -> usize {
|
||||
self.unspent.min_len().min(self.spent_count.block.len())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_state(&mut self, state: &CohortState<impl RealizedOps, impl CostBasisOps>) {
|
||||
self.unspent.push_state(state);
|
||||
self.spent_count
|
||||
.push_block(StoredU64::from(state.spent_utxo_count));
|
||||
}
|
||||
|
||||
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
let mut vecs = self.unspent.collect_vecs_mut();
|
||||
vecs.push(self.spent_count.stored_mut());
|
||||
vecs
|
||||
}
|
||||
|
||||
pub(crate) fn compute_from_stateful(
|
||||
&mut self,
|
||||
starting_lengths: &Lengths,
|
||||
others: &[&Self],
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let unspent: Vec<_> = others.iter().map(|v| &v.unspent).collect();
|
||||
self.unspent
|
||||
.compute_from_stateful(starting_lengths, &unspent, exit)?;
|
||||
sum_others!(self, starting_lengths, others, exit; spent_count.cumulative.height);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
mod base;
|
||||
|
||||
pub use base::OutputsBase;
|
||||
pub use base::{OutputsBase, OutputsUnspent};
|
||||
|
||||
@@ -2,13 +2,14 @@ use brk_cohort::{Loss, Profit, ProfitabilityRange};
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Lengths;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Bitcoin, Cents, Dollars, PartsPerMillionSigned32, Sats, Version};
|
||||
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec};
|
||||
use brk_types::{Bitcoin, Cents, Dollars, Height, PartsPerMillionSigned32, Sats, Version};
|
||||
use vecdb::{AnyStoredVec, AnyVec, CachedBoxedVec, Database, Exit, Rw, StorageMode, WritableVec};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
PerBlock, RatioPerBlock, ValuePerBlock, ValuePerBlockWithDeltas, WindowStartVec, Windows,
|
||||
PerBlock, RatioPerBlock, SpotValuePerBlock, SpotValuePerBlockWithDeltas, WindowStartVec,
|
||||
Windows,
|
||||
},
|
||||
price,
|
||||
};
|
||||
@@ -21,7 +22,7 @@ pub struct WithSth<All, Sth = All> {
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct ProfitabilityBucket<M: StorageMode = Rw> {
|
||||
pub supply: WithSth<ValuePerBlockWithDeltas<M>, ValuePerBlock<M>>,
|
||||
pub supply: WithSth<SpotValuePerBlockWithDeltas<M>, SpotValuePerBlock<M>>,
|
||||
pub realized_cap: WithSth<PerBlock<Dollars, M>>,
|
||||
pub unrealized_pnl: WithSth<PerBlock<Dollars, M>>,
|
||||
pub nupl: RatioPerBlock<PartsPerMillionSigned32, M>,
|
||||
@@ -45,21 +46,24 @@ impl ProfitabilityBucket {
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
supply: WithSth {
|
||||
all: ValuePerBlockWithDeltas::forced_import(
|
||||
all: SpotValuePerBlockWithDeltas::forced_import(
|
||||
db,
|
||||
&format!("{name}_supply"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
)?,
|
||||
sth: ValuePerBlock::forced_import(
|
||||
sth: SpotValuePerBlock::forced_import(
|
||||
db,
|
||||
&format!("{name}_sth_supply"),
|
||||
version,
|
||||
indexes,
|
||||
spot_price,
|
||||
)?,
|
||||
},
|
||||
realized_cap: WithSth {
|
||||
@@ -122,9 +126,6 @@ impl ProfitabilityBucket {
|
||||
) -> Result<()> {
|
||||
let max_from = starting_lengths.height;
|
||||
|
||||
self.supply.all.compute(prices, max_from, exit)?;
|
||||
self.supply.sth.compute(prices, max_from, exit)?;
|
||||
|
||||
self.unrealized_pnl.all.height.compute_transform3(
|
||||
max_from,
|
||||
&prices.spot.cents.height,
|
||||
@@ -223,9 +224,7 @@ impl ProfitabilityBucket {
|
||||
pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
vec![
|
||||
&mut self.supply.all.inner.sats.height as &mut dyn AnyStoredVec,
|
||||
&mut self.supply.all.inner.cents.height,
|
||||
&mut self.supply.sth.sats.height,
|
||||
&mut self.supply.sth.cents.height,
|
||||
&mut self.realized_cap.all.height,
|
||||
&mut self.realized_cap.sth.height,
|
||||
&mut self.unrealized_pnl.all.height,
|
||||
@@ -269,19 +268,41 @@ impl ProfitabilityMetrics {
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let range = ProfitabilityRange::try_new(|name| {
|
||||
ProfitabilityBucket::forced_import(db, name, version, indexes, cached_starts)
|
||||
ProfitabilityBucket::forced_import(
|
||||
db,
|
||||
name,
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
)
|
||||
})?;
|
||||
|
||||
let aggregate_version = version + Version::TWO;
|
||||
|
||||
let profit = Profit::try_new(|name| {
|
||||
ProfitabilityBucket::forced_import(db, name, aggregate_version, indexes, cached_starts)
|
||||
ProfitabilityBucket::forced_import(
|
||||
db,
|
||||
name,
|
||||
aggregate_version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
)
|
||||
})?;
|
||||
|
||||
let loss = Loss::try_new(|name| {
|
||||
ProfitabilityBucket::forced_import(db, name, aggregate_version, indexes, cached_starts)
|
||||
ProfitabilityBucket::forced_import(
|
||||
db,
|
||||
name,
|
||||
aggregate_version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
spot_price,
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
|
||||
@@ -271,11 +271,6 @@ impl RealizedFull {
|
||||
exit,
|
||||
)?;
|
||||
|
||||
// Capitalized price ratio
|
||||
self.capitalized
|
||||
.price
|
||||
.compute_ratio(starting_lengths, &prices.spot.cents.height, exit)?;
|
||||
|
||||
// Sell-side risk ratios
|
||||
for (ssrr, rv) in self
|
||||
.sell_side_risk_ratio
|
||||
|
||||
@@ -5,6 +5,7 @@ use brk_types::{
|
||||
Bitcoin, Cents, CentsSigned, Height, PartsPerMillion64, PartsPerMillionSigned64, Sats,
|
||||
StoredF32, Version,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, Rw, StorageMode, WritableVec};
|
||||
|
||||
use crate::{
|
||||
@@ -19,40 +20,27 @@ use crate::{
|
||||
use crate::distribution::metrics::ImportConfig;
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct RealizedMinimal<M: StorageMode = Rw> {
|
||||
pub struct RealizedBase<M: StorageMode = Rw> {
|
||||
pub cap: FiatPerBlockWithDeltas<Cents, CentsSigned, PartsPerMillionSigned64, M>,
|
||||
pub profit: FiatPerBlockCumulativeWithSums<Cents, M>,
|
||||
pub loss: FiatPerBlockCumulativeWithSums<Cents, M>,
|
||||
pub price: PriceWithRatioPerBlock<M>,
|
||||
pub mvrv: LazyPerBlock<StoredF32>,
|
||||
}
|
||||
|
||||
impl RealizedMinimal {
|
||||
impl RealizedBase {
|
||||
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
|
||||
let v1 = Version::ONE;
|
||||
|
||||
let cap = FiatPerBlockWithDeltas::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("realized_cap"),
|
||||
cfg.version,
|
||||
Version::TWO,
|
||||
cfg.indexes,
|
||||
cfg.cached_starts,
|
||||
)?;
|
||||
|
||||
let price: PriceWithRatioPerBlock = cfg.import("realized_price", v1)?;
|
||||
let mvrv = LazyPerBlock::from_lazy::<Identity<StoredF32>, PartsPerMillion64>(
|
||||
&cfg.name("mvrv"),
|
||||
cfg.version,
|
||||
&price.ratio,
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
cap,
|
||||
cap: FiatPerBlockWithDeltas::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("realized_cap"),
|
||||
cfg.version,
|
||||
Version::TWO,
|
||||
cfg.indexes,
|
||||
cfg.cached_starts,
|
||||
)?,
|
||||
profit: cfg.import("realized_profit", v1)?,
|
||||
loss: cfg.import("realized_loss", v1)?,
|
||||
price,
|
||||
mvrv,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -99,33 +87,80 @@ impl RealizedMinimal {
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct RealizedMinimal<M: StorageMode = Rw> {
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
#[traversable(flatten)]
|
||||
pub base: RealizedBase<M>,
|
||||
pub price: PriceWithRatioPerBlock<M>,
|
||||
pub mvrv: LazyPerBlock<StoredF32>,
|
||||
}
|
||||
|
||||
impl RealizedMinimal {
|
||||
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
|
||||
let v1 = Version::ONE;
|
||||
|
||||
let base = RealizedBase::forced_import(cfg)?;
|
||||
let price: PriceWithRatioPerBlock = cfg.import("realized_price", v1)?;
|
||||
let mvrv = LazyPerBlock::from_lazy::<Identity<StoredF32>, PartsPerMillion64>(
|
||||
&cfg.name("mvrv"),
|
||||
cfg.version,
|
||||
&price.ratio,
|
||||
);
|
||||
|
||||
Ok(Self { base, price, mvrv })
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.base.min_stateful_len()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_state(&mut self, state: &CohortState<impl RealizedOps, impl CostBasisOps>) {
|
||||
self.base.push_state(state);
|
||||
}
|
||||
|
||||
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
self.base.collect_vecs_mut()
|
||||
}
|
||||
|
||||
pub(crate) fn compute_from_stateful(
|
||||
&mut self,
|
||||
starting_lengths: &Lengths,
|
||||
others: &[&Self],
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let bases: Vec<_> = others.iter().map(|v| &v.base).collect();
|
||||
self.base
|
||||
.compute_from_stateful(starting_lengths, &bases, exit)
|
||||
}
|
||||
|
||||
pub(crate) fn compute_rest_part2(
|
||||
&mut self,
|
||||
prices: &price::Vecs,
|
||||
_prices: &price::Vecs,
|
||||
starting_lengths: &Lengths,
|
||||
height_to_supply: &impl ReadableVec<Height, Bitcoin>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let cap = &self.cap.cents.height;
|
||||
self.price
|
||||
.compute_all(prices, starting_lengths, exit, |v| {
|
||||
Ok(v.compute_transform2(
|
||||
starting_lengths.height,
|
||||
cap,
|
||||
height_to_supply,
|
||||
|(i, cap_cents, supply, ..)| {
|
||||
let cap = cap_cents.as_u128();
|
||||
let supply_sats = Sats::from(supply).as_u128();
|
||||
let cents = (cap * Sats::ONE_BTC_U128)
|
||||
.checked_div(supply_sats)
|
||||
.map(Cents::from)
|
||||
.unwrap_or(Cents::ZERO);
|
||||
(i, cents)
|
||||
},
|
||||
exit,
|
||||
)?)
|
||||
})?;
|
||||
let cap = &self.base.cap.cents.height;
|
||||
self.price.cents.height.compute_transform2(
|
||||
starting_lengths.height,
|
||||
cap,
|
||||
height_to_supply,
|
||||
|(i, cap_cents, supply, ..)| {
|
||||
let cap = cap_cents.as_u128();
|
||||
let supply_sats = Sats::from(supply).as_u128();
|
||||
let cents = (cap * Sats::ONE_BTC_U128)
|
||||
.checked_div(supply_sats)
|
||||
.map(Cents::from)
|
||||
.unwrap_or(Cents::ZERO);
|
||||
(i, cents)
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ mod minimal;
|
||||
pub use self::core::RealizedCore;
|
||||
pub use adjusted::AdjustedSopr;
|
||||
pub use full::{RealizedFull, RealizedFullAccum};
|
||||
pub use minimal::RealizedMinimal;
|
||||
pub use minimal::{RealizedBase, RealizedMinimal};
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Lengths;
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Sats, StoredU64, Version};
|
||||
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec};
|
||||
use brk_types::{Cents, Height, Sats, StoredU64, Version};
|
||||
use vecdb::{
|
||||
AnyStoredVec, CachedBoxedVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec,
|
||||
};
|
||||
|
||||
use crate::{indexes, internal::ValuePerBlock, price};
|
||||
use crate::{indexes, internal::SpotValuePerBlock};
|
||||
|
||||
/// Average amount held per UTXO and per funded address.
|
||||
///
|
||||
/// `utxo = supply / utxo_count`, `addr = supply / funded_addr_count`.
|
||||
#[derive(Traversable)]
|
||||
pub struct AvgAmountMetrics<M: StorageMode = Rw> {
|
||||
pub utxo: ValuePerBlock<M>,
|
||||
pub addr: ValuePerBlock<M>,
|
||||
pub utxo: SpotValuePerBlock<M>,
|
||||
pub addr: SpotValuePerBlock<M>,
|
||||
}
|
||||
|
||||
impl AvgAmountMetrics {
|
||||
@@ -20,6 +22,7 @@ impl AvgAmountMetrics {
|
||||
prefix: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let name = |suffix: &str| {
|
||||
if prefix.is_empty() {
|
||||
@@ -29,31 +32,38 @@ impl AvgAmountMetrics {
|
||||
}
|
||||
};
|
||||
Ok(Self {
|
||||
utxo: ValuePerBlock::forced_import(db, &name("avg_utxo_amount"), version, indexes)?,
|
||||
addr: ValuePerBlock::forced_import(db, &name("avg_addr_amount"), version, indexes)?,
|
||||
utxo: SpotValuePerBlock::forced_import(
|
||||
db,
|
||||
&name("avg_utxo_amount"),
|
||||
version,
|
||||
indexes,
|
||||
spot_price,
|
||||
)?,
|
||||
addr: SpotValuePerBlock::forced_import(
|
||||
db,
|
||||
&name("avg_addr_amount"),
|
||||
version,
|
||||
indexes,
|
||||
spot_price,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
vec![
|
||||
&mut self.utxo.sats.height as &mut dyn AnyStoredVec,
|
||||
&mut self.utxo.cents.height,
|
||||
&mut self.addr.sats.height,
|
||||
&mut self.addr.cents.height,
|
||||
]
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.utxo.sats.height.reset()?;
|
||||
self.utxo.cents.height.reset()?;
|
||||
self.addr.sats.height.reset()?;
|
||||
self.addr.cents.height.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn compute(
|
||||
&mut self,
|
||||
prices: &price::Vecs,
|
||||
supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
utxo_count: &impl ReadableVec<Height, StoredU64>,
|
||||
funded_addr_count: &impl ReadableVec<Height, StoredU64>,
|
||||
@@ -64,13 +74,11 @@ impl AvgAmountMetrics {
|
||||
.sats
|
||||
.height
|
||||
.compute_divide(max_from, supply_sats, utxo_count, exit)?;
|
||||
self.utxo.compute(prices, max_from, exit)?;
|
||||
|
||||
self.addr
|
||||
.sats
|
||||
.height
|
||||
.compute_divide(max_from, supply_sats, funded_addr_count, exit)?;
|
||||
self.addr.compute(prices, max_from, exit)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,13 +4,10 @@ use brk_traversable::Traversable;
|
||||
use brk_types::{Height, PartsPerMillion32, PartsPerMillionSigned64, Sats, SatsSigned, Version};
|
||||
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, Rw, StorageMode, WritableVec};
|
||||
|
||||
use crate::{
|
||||
distribution::state::{CohortState, CostBasisOps, RealizedOps},
|
||||
price,
|
||||
};
|
||||
use crate::distribution::state::{CohortState, CostBasisOps, RealizedOps};
|
||||
|
||||
use crate::internal::{
|
||||
LazyRollingDeltasAmountFromHeight, PercentPerBlock, RatioSats, ValuePerBlock,
|
||||
LazyRollingDeltasAmountFromHeight, PercentPerBlock, RatioSats, SpotValuePerBlock,
|
||||
};
|
||||
|
||||
use crate::distribution::metrics::ImportConfig;
|
||||
@@ -18,7 +15,7 @@ use crate::distribution::metrics::ImportConfig;
|
||||
/// Base supply metrics: total supply + dominance (share of circulating).
|
||||
#[derive(Traversable)]
|
||||
pub struct SupplyBase<M: StorageMode = Rw> {
|
||||
pub total: ValuePerBlock<M>,
|
||||
pub total: SpotValuePerBlock<M>,
|
||||
pub delta: LazyRollingDeltasAmountFromHeight<Sats, SatsSigned, PartsPerMillionSigned64>,
|
||||
#[traversable(rename = "dominance")]
|
||||
pub dominance: PercentPerBlock<PartsPerMillion32, M>,
|
||||
@@ -26,7 +23,7 @@ pub struct SupplyBase<M: StorageMode = Rw> {
|
||||
|
||||
impl SupplyBase {
|
||||
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
|
||||
let supply: ValuePerBlock = cfg.import("supply", Version::ZERO)?;
|
||||
let supply: SpotValuePerBlock = cfg.import("supply", Version::ZERO)?;
|
||||
|
||||
let delta = LazyRollingDeltasAmountFromHeight::new(
|
||||
&cfg.name("supply_delta"),
|
||||
@@ -57,20 +54,10 @@ impl SupplyBase {
|
||||
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
vec![
|
||||
&mut self.total.sats.height as &mut dyn AnyStoredVec,
|
||||
&mut self.total.cents.height,
|
||||
&mut self.dominance.ppm.height,
|
||||
]
|
||||
}
|
||||
|
||||
pub(crate) fn compute(
|
||||
&mut self,
|
||||
prices: &price::Vecs,
|
||||
max_from: Height,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.total.compute(prices, max_from, exit)
|
||||
}
|
||||
|
||||
pub(crate) fn compute_dominance(
|
||||
&mut self,
|
||||
max_from: Height,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Lengths;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Version};
|
||||
use brk_types::Version;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
|
||||
|
||||
use crate::{distribution::state::UnrealizedState, price};
|
||||
use crate::distribution::state::UnrealizedState;
|
||||
|
||||
use crate::internal::{
|
||||
HalveCents, HalveDollars, HalveSats, HalveSatsToBitcoin, LazyValuePerBlock, ValuePerBlock,
|
||||
HalveCents, HalveDollars, HalveSats, HalveSatsToBitcoin, LazyValuePerBlock, SpotValuePerBlock,
|
||||
};
|
||||
|
||||
use crate::distribution::metrics::ImportConfig;
|
||||
@@ -24,8 +24,8 @@ pub struct SupplyCore<M: StorageMode = Rw> {
|
||||
pub base: SupplyBase<M>,
|
||||
|
||||
pub half: LazyValuePerBlock,
|
||||
pub in_profit: ValuePerBlock<M>,
|
||||
pub in_loss: ValuePerBlock<M>,
|
||||
pub in_profit: SpotValuePerBlock<M>,
|
||||
pub in_loss: SpotValuePerBlock<M>,
|
||||
}
|
||||
|
||||
impl SupplyCore {
|
||||
@@ -33,7 +33,7 @@ impl SupplyCore {
|
||||
let v0 = Version::ZERO;
|
||||
let base = SupplyBase::forced_import(cfg)?;
|
||||
|
||||
let half = LazyValuePerBlock::from_block_source::<
|
||||
let half = LazyValuePerBlock::from_spot_block_source::<
|
||||
HalveSats,
|
||||
HalveSatsToBitcoin,
|
||||
HalveCents,
|
||||
@@ -64,24 +64,10 @@ impl SupplyCore {
|
||||
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
let mut vecs = self.base.collect_vecs_mut();
|
||||
vecs.push(&mut self.in_profit.sats.height as &mut dyn AnyStoredVec);
|
||||
vecs.push(&mut self.in_profit.cents.height);
|
||||
vecs.push(&mut self.in_loss.sats.height);
|
||||
vecs.push(&mut self.in_loss.cents.height);
|
||||
vecs
|
||||
}
|
||||
|
||||
pub(crate) fn compute(
|
||||
&mut self,
|
||||
prices: &price::Vecs,
|
||||
max_from: Height,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.base.compute(prices, max_from, exit)?;
|
||||
self.in_profit.compute(prices, max_from, exit)?;
|
||||
self.in_loss.compute(prices, max_from, exit)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn validate_computed_versions(&mut self, _base_version: Version) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableCloneableVec, Rw, StorageMode, W
|
||||
|
||||
use crate::{
|
||||
distribution::{metrics::ImportConfig, state::UnrealizedState},
|
||||
internal::{FiatPerBlock, LazyPerBlock, NegCentsUnsignedToDollars, PerBlock},
|
||||
internal::{FiatPerBlock, LazyPerBlock, NegCentsUnsignedToDollars},
|
||||
};
|
||||
|
||||
use super::UnrealizedMinimal;
|
||||
@@ -27,7 +27,7 @@ pub struct UnrealizedBasic<M: StorageMode = Rw> {
|
||||
impl UnrealizedBasic {
|
||||
pub(crate) fn forced_import(
|
||||
cfg: &ImportConfig,
|
||||
mvrv: &PerBlock<PartsPerMillion64>,
|
||||
mvrv: &LazyPerBlock<PartsPerMillion64>,
|
||||
) -> Result<Self> {
|
||||
let v1 = Version::ONE;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use vecdb::{AnyStoredVec, Exit, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
distribution::{metrics::ImportConfig, state::UnrealizedState},
|
||||
internal::{CentsSubtractToCentsSigned, FiatPerBlock, PerBlock},
|
||||
internal::{CentsSubtractToCentsSigned, FiatPerBlock, LazyPerBlock},
|
||||
};
|
||||
|
||||
use super::UnrealizedBasic;
|
||||
@@ -25,7 +25,7 @@ pub struct UnrealizedCore<M: StorageMode = Rw> {
|
||||
impl UnrealizedCore {
|
||||
pub(crate) fn forced_import(
|
||||
cfg: &ImportConfig,
|
||||
mvrv: &PerBlock<PartsPerMillion64>,
|
||||
mvrv: &LazyPerBlock<PartsPerMillion64>,
|
||||
) -> Result<Self> {
|
||||
let basic = UnrealizedBasic::forced_import(cfg, mvrv)?;
|
||||
let net_unrealized_pnl = cfg.import("net_unrealized_pnl", Version::ZERO)?;
|
||||
|
||||
@@ -6,7 +6,7 @@ use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{AnyStoredVec, AnyVec, BytesVec, Exit, ReadableVec, Rw, StorageMode, WritableVec};
|
||||
|
||||
use crate::distribution::state::UnrealizedState;
|
||||
use crate::internal::{CentsSubtractToCentsSigned, FiatPerBlock, PerBlock};
|
||||
use crate::internal::{CentsSubtractToCentsSigned, FiatPerBlock, LazyPerBlock};
|
||||
use crate::{distribution::metrics::ImportConfig, price};
|
||||
|
||||
use super::UnrealizedCore;
|
||||
@@ -43,7 +43,7 @@ pub struct UnrealizedFull<M: StorageMode = Rw> {
|
||||
impl UnrealizedFull {
|
||||
pub(crate) fn forced_import(
|
||||
cfg: &ImportConfig,
|
||||
mvrv: &PerBlock<PartsPerMillion64>,
|
||||
mvrv: &LazyPerBlock<PartsPerMillion64>,
|
||||
) -> Result<Self> {
|
||||
let v0 = Version::ZERO;
|
||||
let inner = UnrealizedCore::forced_import(cfg, mvrv)?;
|
||||
|
||||
@@ -2,7 +2,7 @@ use brk_traversable::Traversable;
|
||||
use brk_types::{PartsPerMillion64, PartsPerMillionSigned32, Version};
|
||||
use vecdb::UnaryTransform;
|
||||
|
||||
use crate::internal::{LazyRatioPerBlock, PerBlock};
|
||||
use crate::internal::{LazyPerBlock, LazyRatioPerBlock};
|
||||
|
||||
use crate::distribution::metrics::ImportConfig;
|
||||
|
||||
@@ -14,9 +14,9 @@ pub struct UnrealizedMinimal {
|
||||
}
|
||||
|
||||
impl UnrealizedMinimal {
|
||||
pub(crate) fn new(cfg: &ImportConfig, mvrv: &PerBlock<PartsPerMillion64>) -> Self {
|
||||
pub(crate) fn new(cfg: &ImportConfig, mvrv: &LazyPerBlock<PartsPerMillion64>) -> Self {
|
||||
Self {
|
||||
nupl: LazyRatioPerBlock::from_source::<MvrvToNupl>(
|
||||
nupl: LazyRatioPerBlock::from_lazy_source::<MvrvToNupl, PartsPerMillion64>(
|
||||
&cfg.name("nupl"),
|
||||
cfg.version + VERSION,
|
||||
mvrv,
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
use std::path::Path;
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_types::{Age, Cents, FundedAddrData, Sats, SupplyState};
|
||||
use brk_types::{Cents, FundedAddrData, Sats, SupplyState};
|
||||
use vecdb::unlikely;
|
||||
|
||||
use super::super::cost_basis::{CostBasisRaw, RealizedOps};
|
||||
use super::super::cost_basis::{CostBasisRaw, MinimalRealizedState};
|
||||
use super::base::CohortState;
|
||||
|
||||
/// Significant digits for address cost basis prices (after rounding to dollars).
|
||||
const COST_BASIS_PRICE_DIGITS: i32 = 4;
|
||||
|
||||
pub struct AddrCohortState<R: RealizedOps> {
|
||||
pub struct AddrCohortState {
|
||||
pub addr_count: u64,
|
||||
pub inner: CohortState<R, CostBasisRaw>,
|
||||
pub inner: CohortState<MinimalRealizedState, CostBasisRaw>,
|
||||
}
|
||||
|
||||
impl<R: RealizedOps> AddrCohortState<R> {
|
||||
impl AddrCohortState {
|
||||
pub(crate) fn new(path: &Path, name: &str) -> Self {
|
||||
Self {
|
||||
addr_count: 0,
|
||||
inner: CohortState::new(path, name).with_price_rounding(COST_BASIS_PRICE_DIGITS),
|
||||
inner: CohortState::new(path, name),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +27,7 @@ impl<R: RealizedOps> AddrCohortState<R> {
|
||||
self.inner.sent = Sats::ZERO;
|
||||
self.inner.spent_utxo_count = 0;
|
||||
self.inner.satdays_destroyed = Sats::ZERO;
|
||||
self.inner.realized = R::default();
|
||||
self.inner.realized = MinimalRealizedState::default();
|
||||
}
|
||||
|
||||
pub(crate) fn send(
|
||||
@@ -39,12 +36,8 @@ impl<R: RealizedOps> AddrCohortState<R> {
|
||||
value: Sats,
|
||||
current_price: Cents,
|
||||
prev_price: Cents,
|
||||
ath: Cents,
|
||||
age: Age,
|
||||
) -> Result<()> {
|
||||
let prev = addr_data.cost_basis_snapshot();
|
||||
addr_data.send(value, prev_price)?;
|
||||
let current = addr_data.cost_basis_snapshot();
|
||||
let prev_ps = addr_data.send(value, prev_price)?;
|
||||
|
||||
self.inner.send_addr(
|
||||
&SupplyState {
|
||||
@@ -52,11 +45,7 @@ impl<R: RealizedOps> AddrCohortState<R> {
|
||||
value,
|
||||
},
|
||||
current_price,
|
||||
prev_price,
|
||||
ath,
|
||||
age,
|
||||
¤t,
|
||||
&prev,
|
||||
prev_ps,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -69,32 +58,29 @@ impl<R: RealizedOps> AddrCohortState<R> {
|
||||
price: Cents,
|
||||
output_count: u32,
|
||||
) {
|
||||
let prev = addr_data.cost_basis_snapshot();
|
||||
addr_data.receive_outputs(value, price, output_count);
|
||||
let current = addr_data.cost_basis_snapshot();
|
||||
let cap = addr_data.receive_outputs(value, price, output_count);
|
||||
|
||||
self.inner.receive_addr(
|
||||
self.inner.increment_addr(
|
||||
&SupplyState {
|
||||
utxo_count: output_count as u64,
|
||||
value,
|
||||
},
|
||||
price,
|
||||
¤t,
|
||||
&prev,
|
||||
cap,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn add(&mut self, addr_data: &FundedAddrData) {
|
||||
self.addr_count += 1;
|
||||
let supply = SupplyState::from(addr_data);
|
||||
self.inner
|
||||
.increment_snapshot(&addr_data.cost_basis_snapshot());
|
||||
.increment_addr(&supply, addr_data.realized_cap_raw);
|
||||
}
|
||||
|
||||
pub(crate) fn subtract(&mut self, addr_data: &FundedAddrData) {
|
||||
let snapshot = addr_data.cost_basis_snapshot();
|
||||
let supply = SupplyState::from(addr_data);
|
||||
|
||||
// Check for potential underflow before it happens
|
||||
if unlikely(self.inner.supply.utxo_count < snapshot.supply_state.utxo_count) {
|
||||
if unlikely(self.inner.supply.utxo_count < supply.utxo_count) {
|
||||
panic!(
|
||||
"AddrCohortState::subtract underflow!\n\
|
||||
Cohort state: addr_count={}, supply={}\n\
|
||||
@@ -105,11 +91,11 @@ impl<R: RealizedOps> AddrCohortState<R> {
|
||||
self.addr_count,
|
||||
self.inner.supply,
|
||||
addr_data,
|
||||
snapshot.supply_state,
|
||||
snapshot.realized_price
|
||||
supply,
|
||||
addr_data.realized_price()
|
||||
);
|
||||
}
|
||||
if unlikely(self.inner.supply.value < snapshot.supply_state.value) {
|
||||
if unlikely(self.inner.supply.value < supply.value) {
|
||||
panic!(
|
||||
"AddrCohortState::subtract value underflow!\n\
|
||||
Cohort state: addr_count={}, supply={}\n\
|
||||
@@ -120,8 +106,8 @@ impl<R: RealizedOps> AddrCohortState<R> {
|
||||
self.addr_count,
|
||||
self.inner.supply,
|
||||
addr_data,
|
||||
snapshot.supply_state,
|
||||
snapshot.realized_price
|
||||
supply,
|
||||
addr_data.realized_price()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -130,10 +116,52 @@ impl<R: RealizedOps> AddrCohortState<R> {
|
||||
"AddrCohortState::subtract addr_count underflow! addr_count=0\n\
|
||||
Addr being subtracted: {}\n\
|
||||
Realized price: {}",
|
||||
addr_data, snapshot.realized_price
|
||||
addr_data,
|
||||
addr_data.realized_price()
|
||||
)
|
||||
});
|
||||
|
||||
self.inner.decrement_snapshot(&snapshot);
|
||||
self.inner
|
||||
.decrement_addr(&supply, addr_data.realized_cap_raw);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::distribution::state::cost_basis::RealizedOps;
|
||||
|
||||
#[test]
|
||||
fn address_updates_preserve_supply_cap_and_transfer_state() {
|
||||
let mut cohort = AddrCohortState::new(Path::new(""), "test");
|
||||
let mut addr = FundedAddrData::default();
|
||||
|
||||
addr.receive(Sats::ONE_BTC, Cents::new(10_000));
|
||||
cohort.add(&addr);
|
||||
cohort.receive_outputs(&mut addr, Sats::new(50_000_000), Cents::new(20_000), 1);
|
||||
cohort
|
||||
.send(
|
||||
&mut addr,
|
||||
Sats::new(25_000_000),
|
||||
Cents::new(15_000),
|
||||
Cents::new(10_000),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
cohort.inner.supply.utxo_count,
|
||||
SupplyState::from(&addr).utxo_count
|
||||
);
|
||||
assert_eq!(cohort.inner.supply.value, SupplyState::from(&addr).value);
|
||||
assert_eq!(cohort.inner.realized.cap(), Cents::new(17_500));
|
||||
assert_eq!(cohort.inner.realized.profit(), Cents::new(1_250));
|
||||
assert_eq!(cohort.inner.sent, Sats::new(25_000_000));
|
||||
assert_eq!(cohort.inner.realized.value_destroyed(), Cents::ZERO);
|
||||
|
||||
cohort.subtract(&addr);
|
||||
assert_eq!(cohort.addr_count, 0);
|
||||
assert_eq!(cohort.inner.supply.utxo_count, 0);
|
||||
assert_eq!(cohort.inner.supply.value, Sats::ZERO);
|
||||
assert_eq!(cohort.inner.realized.cap(), Cents::ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ use brk_types::{
|
||||
};
|
||||
|
||||
use super::super::{
|
||||
cost_basis::{Accumulate, CostBasisData, CostBasisOps, RealizedOps, UnrealizedState},
|
||||
cost_basis::{
|
||||
Accumulate, CostBasisData, CostBasisOps, CostBasisRaw, MinimalRealizedState, RealizedOps,
|
||||
UnrealizedState,
|
||||
},
|
||||
pending::PendingDelta,
|
||||
};
|
||||
|
||||
@@ -76,12 +79,6 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable price rounding for cost basis data.
|
||||
pub(crate) fn with_price_rounding(mut self, digits: i32) -> Self {
|
||||
self.cost_basis = self.cost_basis.with_price_rounding(digits);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
|
||||
self.cost_basis.import_at_or_before(height)
|
||||
}
|
||||
@@ -167,38 +164,6 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn receive_addr(
|
||||
&mut self,
|
||||
supply: &SupplyState,
|
||||
price: Cents,
|
||||
current: &CostBasisSnapshot,
|
||||
prev: &CostBasisSnapshot,
|
||||
) {
|
||||
self.supply += supply;
|
||||
|
||||
if supply.value > Sats::ZERO {
|
||||
self.realized.receive(price, supply.value);
|
||||
|
||||
if current.supply_state.value.is_not_zero() {
|
||||
self.cost_basis.increment(
|
||||
current.realized_price,
|
||||
current.supply_state.value,
|
||||
current.price_sats,
|
||||
current.capitalized_cap_raw,
|
||||
);
|
||||
}
|
||||
|
||||
if prev.supply_state.value.is_not_zero() {
|
||||
self.cost_basis.decrement(
|
||||
prev.realized_price,
|
||||
prev.supply_state.value,
|
||||
prev.price_sats,
|
||||
prev.capitalized_cap_raw,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn send_utxo_precomputed(&mut self, supply: &SupplyState, pre: &SendPrecomputed) {
|
||||
self.supply -= supply;
|
||||
self.sent += pre.sats;
|
||||
@@ -239,63 +204,52 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn write(&mut self, height: Height, cleanup: bool) -> Result<()> {
|
||||
self.cost_basis.write(height, cleanup)
|
||||
}
|
||||
}
|
||||
|
||||
impl CohortState<MinimalRealizedState, CostBasisRaw> {
|
||||
pub(crate) fn increment_addr(&mut self, supply: &SupplyState, cap: CentsSats) {
|
||||
self.supply += supply;
|
||||
|
||||
if supply.value.is_not_zero() {
|
||||
self.realized.increment_cap(cap);
|
||||
self.cost_basis.increment_cap(cap);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn decrement_addr(&mut self, supply: &SupplyState, cap: CentsSats) {
|
||||
self.supply -= supply;
|
||||
|
||||
if supply.value.is_not_zero() {
|
||||
self.realized.decrement_cap(cap);
|
||||
self.cost_basis.decrement_cap(cap);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn send_addr(
|
||||
&mut self,
|
||||
supply: &SupplyState,
|
||||
current_price: Cents,
|
||||
prev_price: Cents,
|
||||
ath: Cents,
|
||||
age: Age,
|
||||
current: &CostBasisSnapshot,
|
||||
prev: &CostBasisSnapshot,
|
||||
prev_ps: CentsSats,
|
||||
) {
|
||||
if supply.utxo_count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.supply -= supply;
|
||||
self.spent_utxo_count += supply.utxo_count;
|
||||
|
||||
if supply.value > Sats::ZERO {
|
||||
self.sent += supply.value;
|
||||
if R::TRACK_ACTIVITY {
|
||||
self.satdays_destroyed += age.satdays_destroyed(supply.value);
|
||||
}
|
||||
|
||||
let sats = supply.value;
|
||||
|
||||
// Compute once for realized.send using typed values
|
||||
let current_ps = CentsSats::from_price_sats(current_price, sats);
|
||||
let prev_ps = CentsSats::from_price_sats(prev_price, sats);
|
||||
let ath_ps = CentsSats::from_price_sats(ath, sats);
|
||||
let prev_capitalized_cap = prev_ps.to_capitalized_cap(prev_price);
|
||||
|
||||
self.realized
|
||||
.send(sats, current_ps, prev_ps, ath_ps, prev_capitalized_cap);
|
||||
|
||||
if current.supply_state.value.is_not_zero() {
|
||||
self.cost_basis.increment(
|
||||
current.realized_price,
|
||||
current.supply_state.value,
|
||||
current.price_sats,
|
||||
current.capitalized_cap_raw,
|
||||
);
|
||||
}
|
||||
|
||||
if prev.supply_state.value.is_not_zero() {
|
||||
self.cost_basis.decrement(
|
||||
prev.realized_price,
|
||||
prev.supply_state.value,
|
||||
prev.price_sats,
|
||||
prev.capitalized_cap_raw,
|
||||
);
|
||||
}
|
||||
if supply.value == Sats::ZERO {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn write(&mut self, height: Height, cleanup: bool) -> Result<()> {
|
||||
self.cost_basis.write(height, cleanup)
|
||||
self.sent += supply.value;
|
||||
|
||||
let sats = supply.value;
|
||||
let current_ps = CentsSats::from_price_sats(current_price, sats);
|
||||
self.realized.realize_spend(current_ps, prev_ps);
|
||||
self.cost_basis.decrement_cap(prev_ps);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ const STATE_TO_KEEP: usize = 10;
|
||||
/// Implemented by `CostBasisRaw` (scalars only) and `CostBasisData` (full map + scalars).
|
||||
pub trait CostBasisOps: Send + Sync + 'static {
|
||||
fn create(path: &Path, name: &str) -> Self;
|
||||
fn with_price_rounding(self, digits: i32) -> Self;
|
||||
fn import_at_or_before(&mut self, height: Height) -> Result<Height>;
|
||||
fn cap_raw(&self) -> CentsSats;
|
||||
fn capitalized_cap_raw(&self) -> CentsSquaredSats;
|
||||
@@ -78,6 +77,16 @@ pub struct CostBasisRaw {
|
||||
}
|
||||
|
||||
impl CostBasisRaw {
|
||||
#[inline]
|
||||
pub(crate) fn increment_cap(&mut self, value: CentsSats) {
|
||||
self.pending_cap.inc += value;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn decrement_cap(&mut self, value: CentsSats) {
|
||||
self.pending_cap.dec += value;
|
||||
}
|
||||
|
||||
pub(super) fn path_state(&self, height: Height) -> PathBuf {
|
||||
self.pathbuf.join(height.to_string())
|
||||
}
|
||||
@@ -151,10 +160,6 @@ impl CostBasisOps for CostBasisRaw {
|
||||
}
|
||||
}
|
||||
|
||||
fn with_price_rounding(self, _digits: i32) -> Self {
|
||||
self
|
||||
}
|
||||
|
||||
fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
|
||||
let files = self.read_dir(None)?;
|
||||
let (&height, path) = files.range(..=height).next_back().ok_or(Error::NotFound(
|
||||
@@ -189,7 +194,7 @@ impl CostBasisOps for CostBasisRaw {
|
||||
price_sats: CentsSats,
|
||||
_capitalized_cap: CentsSquaredSats,
|
||||
) {
|
||||
self.pending_cap.inc += price_sats;
|
||||
self.increment_cap(price_sats);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -200,7 +205,7 @@ impl CostBasisOps for CostBasisRaw {
|
||||
price_sats: CentsSats,
|
||||
_capitalized_cap: CentsSquaredSats,
|
||||
) {
|
||||
self.pending_cap.dec += price_sats;
|
||||
self.decrement_cap(price_sats);
|
||||
}
|
||||
|
||||
fn apply_pending(&mut self) {
|
||||
@@ -237,26 +242,18 @@ impl CostBasisOps for CostBasisRaw {
|
||||
/// Generic over the accumulator `S`:
|
||||
/// - `WithCapital`: tracks all fields including invested capital + capitalized cap (128 bytes)
|
||||
/// - `WithoutCapital`: tracks only supply + unrealized profit/loss (64 bytes, 1 cache line)
|
||||
/// and writes a compact checkpoint without a capitalized-cap slot
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CostBasisData<S: Accumulate> {
|
||||
raw: CostBasisRaw,
|
||||
map: Option<UrpdRaw>,
|
||||
pending: FxHashMap<CentsCompact, PendingDelta>,
|
||||
cache: Option<CachedUnrealizedState<S>>,
|
||||
rounding_digits: Option<i32>,
|
||||
capitalized_cap_raw: CentsSquaredSats,
|
||||
pending_capitalized_cap: PendingCapitalizedCapRawDelta,
|
||||
}
|
||||
|
||||
impl<S: Accumulate> CostBasisData<S> {
|
||||
#[inline]
|
||||
fn round_price(&self, price: Cents) -> Cents {
|
||||
match self.rounding_digits {
|
||||
Some(digits) => price.round_to_dollar(digits),
|
||||
None => price,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map(&self) -> &CostBasisMap {
|
||||
debug_assert!(self.pending.is_empty() && self.raw.pending_cap.is_zero());
|
||||
&self.map.as_ref().unwrap().map
|
||||
@@ -345,17 +342,11 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
|
||||
map: None,
|
||||
pending: FxHashMap::default(),
|
||||
cache: None,
|
||||
rounding_digits: None,
|
||||
capitalized_cap_raw: CentsSquaredSats::ZERO,
|
||||
pending_capitalized_cap: PendingCapitalizedCapRawDelta::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_price_rounding(mut self, digits: i32) -> Self {
|
||||
self.rounding_digits = Some(digits);
|
||||
self
|
||||
}
|
||||
|
||||
fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
|
||||
let files = self.raw.read_dir(None)?;
|
||||
let (&height, path) = files.range(..=height).next_back().ok_or(Error::NotFound(
|
||||
@@ -363,14 +354,18 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
|
||||
))?;
|
||||
let data = fs::read(path)?;
|
||||
let (base, rest) = UrpdRaw::deserialize_with_rest(&data)?;
|
||||
self.map = Some(base);
|
||||
self.raw.state = Some(RawState::deserialize(rest)?);
|
||||
debug_assert!(
|
||||
rest.len() >= 32,
|
||||
rest.len() >= if S::TRACK_CAPITAL { 32 } else { 16 },
|
||||
"CostBasisData state too short: {} bytes",
|
||||
rest.len()
|
||||
);
|
||||
self.capitalized_cap_raw = CentsSquaredSats::from_bytes(&rest[16..32])?;
|
||||
self.map = Some(base);
|
||||
self.raw.state = Some(RawState::deserialize(rest)?);
|
||||
self.capitalized_cap_raw = if S::TRACK_CAPITAL {
|
||||
CentsSquaredSats::from_bytes(&rest[16..32])?
|
||||
} else {
|
||||
CentsSquaredSats::ZERO
|
||||
};
|
||||
self.pending.clear();
|
||||
self.raw.pending_cap = PendingCapDelta::default();
|
||||
self.pending_capitalized_cap = PendingCapitalizedCapRawDelta::default();
|
||||
@@ -394,10 +389,9 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
|
||||
price_sats: CentsSats,
|
||||
capitalized_cap: CentsSquaredSats,
|
||||
) {
|
||||
let price = self.round_price(price);
|
||||
self.pending.entry(price.into()).or_default().inc += sats;
|
||||
self.raw.pending_cap.inc += price_sats;
|
||||
if capitalized_cap != CentsSquaredSats::ZERO {
|
||||
self.raw.increment_cap(price_sats);
|
||||
if S::TRACK_CAPITAL && capitalized_cap != CentsSquaredSats::ZERO {
|
||||
self.pending_capitalized_cap.inc += capitalized_cap;
|
||||
}
|
||||
if let Some(cache) = self.cache.as_mut() {
|
||||
@@ -413,10 +407,9 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
|
||||
price_sats: CentsSats,
|
||||
capitalized_cap: CentsSquaredSats,
|
||||
) {
|
||||
let price = self.round_price(price);
|
||||
self.pending.entry(price.into()).or_default().dec += sats;
|
||||
self.raw.pending_cap.dec += price_sats;
|
||||
if capitalized_cap != CentsSquaredSats::ZERO {
|
||||
self.raw.decrement_cap(price_sats);
|
||||
if S::TRACK_CAPITAL && capitalized_cap != CentsSquaredSats::ZERO {
|
||||
self.pending_capitalized_cap.dec += capitalized_cap;
|
||||
}
|
||||
if let Some(cache) = self.cache.as_mut() {
|
||||
@@ -427,19 +420,21 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
|
||||
fn apply_pending(&mut self) {
|
||||
self.apply_map_pending();
|
||||
self.raw.apply_pending_cap();
|
||||
self.capitalized_cap_raw += self.pending_capitalized_cap.inc;
|
||||
debug_assert!(
|
||||
self.capitalized_cap_raw >= self.pending_capitalized_cap.dec,
|
||||
"CostBasis capitalized_cap_raw underflow!\n\
|
||||
Path: {:?}\n\
|
||||
Current (after increments): {:?}\n\
|
||||
Trying to decrement by: {:?}",
|
||||
self.raw.pathbuf,
|
||||
self.capitalized_cap_raw,
|
||||
self.pending_capitalized_cap.dec
|
||||
);
|
||||
self.capitalized_cap_raw -= self.pending_capitalized_cap.dec;
|
||||
self.pending_capitalized_cap = PendingCapitalizedCapRawDelta::default();
|
||||
if S::TRACK_CAPITAL {
|
||||
self.capitalized_cap_raw += self.pending_capitalized_cap.inc;
|
||||
debug_assert!(
|
||||
self.capitalized_cap_raw >= self.pending_capitalized_cap.dec,
|
||||
"CostBasis capitalized_cap_raw underflow!\n\
|
||||
Path: {:?}\n\
|
||||
Current (after increments): {:?}\n\
|
||||
Trying to decrement by: {:?}",
|
||||
self.raw.pathbuf,
|
||||
self.capitalized_cap_raw,
|
||||
self.pending_capitalized_cap.dec
|
||||
);
|
||||
self.capitalized_cap_raw -= self.pending_capitalized_cap.dec;
|
||||
self.pending_capitalized_cap = PendingCapitalizedCapRawDelta::default();
|
||||
}
|
||||
}
|
||||
|
||||
fn init(&mut self) {
|
||||
@@ -464,9 +459,70 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
|
||||
let raw_state = self.raw.state.as_ref().unwrap();
|
||||
let mut buffer = self.map.as_ref().unwrap().serialize()?;
|
||||
buffer.extend(raw_state.cap_raw.to_bytes());
|
||||
buffer.extend(self.capitalized_cap_raw.to_bytes());
|
||||
if S::TRACK_CAPITAL {
|
||||
buffer.extend(self.capitalized_cap_raw.to_bytes());
|
||||
}
|
||||
fs::write(self.raw.path_state(height), buffer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
fs,
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::distribution::state::{WithCapital, WithoutCapital};
|
||||
|
||||
static NEXT_PATH_ID: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn test_path() -> PathBuf {
|
||||
std::env::temp_dir().join(format!(
|
||||
"brk-cost-basis-data-{}-{}",
|
||||
std::process::id(),
|
||||
NEXT_PATH_ID.fetch_add(1, Ordering::Relaxed)
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkpoint_layout_tracks_capital_capability() {
|
||||
let root = test_path();
|
||||
|
||||
let mut compact = CostBasisData::<WithoutCapital>::create(&root, "compact");
|
||||
compact.clean().unwrap();
|
||||
compact.init();
|
||||
compact.write(Height::ZERO, false).unwrap();
|
||||
|
||||
let mut full = CostBasisData::<WithCapital>::create(&root, "full");
|
||||
full.clean().unwrap();
|
||||
full.init();
|
||||
full.write(Height::ZERO, false).unwrap();
|
||||
|
||||
let compact_len = fs::metadata(compact.raw.path_state(Height::ZERO))
|
||||
.unwrap()
|
||||
.len();
|
||||
let full_len = fs::metadata(full.raw.path_state(Height::ZERO))
|
||||
.unwrap()
|
||||
.len();
|
||||
assert_eq!(full_len, compact_len + 16);
|
||||
|
||||
let mut compact_reader = CostBasisData::<WithoutCapital>::create(&root, "compact");
|
||||
assert_eq!(
|
||||
compact_reader.import_at_or_before(Height::ZERO).unwrap(),
|
||||
Height::ZERO
|
||||
);
|
||||
|
||||
let mut legacy_reader = CostBasisData::<WithoutCapital>::create(&root, "full");
|
||||
assert_eq!(
|
||||
legacy_reader.import_at_or_before(Height::ZERO).unwrap(),
|
||||
Height::ZERO
|
||||
);
|
||||
assert_eq!(legacy_reader.capitalized_cap_raw(), CentsSquaredSats::ZERO);
|
||||
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use brk_types::{Cents, CentsSats, CentsSquaredSats, Sats};
|
||||
use vecdb::unlikely;
|
||||
|
||||
/// Trait for realized state operations, implemented by Minimal, Core, and Full variants.
|
||||
pub trait RealizedOps: Default + Clone + Send + Sync + 'static {
|
||||
@@ -36,14 +37,39 @@ pub trait RealizedOps: Default + Clone + Send + Sync + 'static {
|
||||
);
|
||||
}
|
||||
|
||||
/// Minimal realized state: cap, profit, loss, value_created/destroyed.
|
||||
/// Used by MinimalCohortMetrics cohorts (amount_range, type_, address — ~135 separate cohorts).
|
||||
/// Minimal realized state: cap, profit, and loss.
|
||||
/// Used by MinimalCohortMetrics cohorts (amount_range and type) and address cohorts.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct MinimalRealizedState {
|
||||
cap_raw: u128,
|
||||
profit_raw: u128,
|
||||
loss_raw: u128,
|
||||
value_destroyed_raw: u128,
|
||||
}
|
||||
|
||||
impl MinimalRealizedState {
|
||||
#[inline]
|
||||
pub(crate) fn increment_cap(&mut self, cap: CentsSats) {
|
||||
self.cap_raw += cap.as_u128();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn decrement_cap(&mut self, cap: CentsSats) {
|
||||
self.cap_raw -= cap.as_u128();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn realize_spend(&mut self, current: CentsSats, previous: CentsSats) {
|
||||
match current.cmp(&previous) {
|
||||
Ordering::Greater => {
|
||||
self.profit_raw += (current - previous).as_u128();
|
||||
}
|
||||
Ordering::Less => {
|
||||
self.loss_raw += (previous - current).as_u128();
|
||||
}
|
||||
Ordering::Equal => {}
|
||||
}
|
||||
self.decrement_cap(previous);
|
||||
}
|
||||
}
|
||||
|
||||
impl RealizedOps for MinimalRealizedState {
|
||||
@@ -71,14 +97,6 @@ impl RealizedOps for MinimalRealizedState {
|
||||
Cents::new((self.loss_raw / Sats::ONE_BTC_U128) as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn value_destroyed(&self) -> Cents {
|
||||
if self.value_destroyed_raw == 0 {
|
||||
return Cents::ZERO;
|
||||
}
|
||||
Cents::new((self.value_destroyed_raw / Sats::ONE_BTC_U128) as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_cap_raw(&mut self, cap_raw: CentsSats) {
|
||||
self.cap_raw = cap_raw.inner();
|
||||
@@ -91,7 +109,6 @@ impl RealizedOps for MinimalRealizedState {
|
||||
fn reset_single_iteration_values(&mut self) {
|
||||
self.profit_raw = 0;
|
||||
self.loss_raw = 0;
|
||||
self.value_destroyed_raw = 0;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -99,18 +116,17 @@ impl RealizedOps for MinimalRealizedState {
|
||||
if sats.is_zero() {
|
||||
return;
|
||||
}
|
||||
let price_sats = CentsSats::from_price_sats(price, sats);
|
||||
self.cap_raw += price_sats.as_u128();
|
||||
self.increment_cap(CentsSats::from_price_sats(price, sats));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn increment_snapshot(&mut self, price_sats: CentsSats, _capitalized_cap: CentsSquaredSats) {
|
||||
self.cap_raw += price_sats.as_u128();
|
||||
self.increment_cap(price_sats);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn decrement_snapshot(&mut self, price_sats: CentsSats, _capitalized_cap: CentsSquaredSats) {
|
||||
self.cap_raw -= price_sats.as_u128();
|
||||
self.decrement_cap(price_sats);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -122,25 +138,16 @@ impl RealizedOps for MinimalRealizedState {
|
||||
_ath_ps: CentsSats,
|
||||
_prev_capitalized_cap: CentsSquaredSats,
|
||||
) {
|
||||
match current_ps.cmp(&prev_ps) {
|
||||
Ordering::Greater => {
|
||||
self.profit_raw += (current_ps - prev_ps).as_u128();
|
||||
}
|
||||
Ordering::Less => {
|
||||
self.loss_raw += (prev_ps - current_ps).as_u128();
|
||||
}
|
||||
Ordering::Equal => {}
|
||||
}
|
||||
self.cap_raw -= prev_ps.as_u128();
|
||||
self.value_destroyed_raw += prev_ps.as_u128();
|
||||
self.realize_spend(current_ps, prev_ps);
|
||||
}
|
||||
}
|
||||
|
||||
/// Core realized state: extends Minimal with sent_in_profit/loss tracking.
|
||||
/// Used by CoreCohortMetrics cohorts (epoch, class, under_age, over_age — ~59 separate cohorts).
|
||||
/// Core realized state: extends Minimal with activity tracking.
|
||||
/// Used by CoreCohortMetrics cohorts (entry, epoch, class, under_age, over_age).
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct CoreRealizedState {
|
||||
minimal: MinimalRealizedState,
|
||||
value_destroyed_raw: u128,
|
||||
sent_in_profit: Sats,
|
||||
sent_in_loss: Sats,
|
||||
}
|
||||
@@ -165,7 +172,10 @@ impl RealizedOps for CoreRealizedState {
|
||||
|
||||
#[inline]
|
||||
fn value_destroyed(&self) -> Cents {
|
||||
self.minimal.value_destroyed()
|
||||
if unlikely(self.value_destroyed_raw == 0) {
|
||||
return Cents::ZERO;
|
||||
}
|
||||
Cents::new((self.value_destroyed_raw / Sats::ONE_BTC_U128) as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -189,6 +199,7 @@ impl RealizedOps for CoreRealizedState {
|
||||
#[inline]
|
||||
fn reset_single_iteration_values(&mut self) {
|
||||
self.minimal.reset_single_iteration_values();
|
||||
self.value_destroyed_raw = 0;
|
||||
self.sent_in_profit = Sats::ZERO;
|
||||
self.sent_in_loss = Sats::ZERO;
|
||||
}
|
||||
@@ -221,6 +232,7 @@ impl RealizedOps for CoreRealizedState {
|
||||
) {
|
||||
self.minimal
|
||||
.send(sats, current_ps, prev_ps, ath_ps, prev_capitalized_cap);
|
||||
self.value_destroyed_raw += prev_ps.as_u128();
|
||||
match current_ps.cmp(&prev_ps) {
|
||||
Ordering::Greater | Ordering::Equal => {
|
||||
self.sent_in_profit += sats;
|
||||
@@ -240,7 +252,7 @@ impl CoreRealizedState {
|
||||
}
|
||||
|
||||
/// Full realized state (~160 bytes).
|
||||
/// Used by BasicCohortMetrics cohorts (age_range — 21 separate cohorts).
|
||||
/// Used by BasicCohortMetrics cohorts (age_range — 23 separate cohorts).
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct RealizedState {
|
||||
core: CoreRealizedState,
|
||||
@@ -376,3 +388,52 @@ impl RealizedState {
|
||||
self.peak_regret_raw
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn price_sats(price: u64) -> CentsSats {
|
||||
CentsSats::from_price_sats(Cents::new(price), Sats::ONE_BTC)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimal_tracks_only_cap_profit_and_loss() {
|
||||
let mut state = MinimalRealizedState::default();
|
||||
state.increment(Cents::new(10_000), Sats::ONE_BTC);
|
||||
state.send(
|
||||
Sats::ONE_BTC,
|
||||
price_sats(15_000),
|
||||
price_sats(10_000),
|
||||
CentsSats::ZERO,
|
||||
CentsSquaredSats::ZERO,
|
||||
);
|
||||
|
||||
assert_eq!(state.cap(), Cents::ZERO);
|
||||
assert_eq!(state.profit(), Cents::new(5_000));
|
||||
assert_eq!(state.loss(), Cents::ZERO);
|
||||
assert_eq!(state.value_destroyed(), Cents::ZERO);
|
||||
assert_eq!(state.sent_in_profit(), Sats::ZERO);
|
||||
assert_eq!(state.sent_in_loss(), Sats::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn core_adds_activity_to_minimal_realized_state() {
|
||||
let mut state = CoreRealizedState::default();
|
||||
state.increment(Cents::new(10_000), Sats::ONE_BTC);
|
||||
state.send(
|
||||
Sats::ONE_BTC,
|
||||
price_sats(15_000),
|
||||
price_sats(10_000),
|
||||
CentsSats::ZERO,
|
||||
CentsSquaredSats::ZERO,
|
||||
);
|
||||
|
||||
assert_eq!(state.cap(), Cents::ZERO);
|
||||
assert_eq!(state.profit(), Cents::new(5_000));
|
||||
assert_eq!(state.loss(), Cents::ZERO);
|
||||
assert_eq!(state.value_destroyed(), Cents::new(10_000));
|
||||
assert_eq!(state.sent_in_profit(), Sats::ONE_BTC);
|
||||
assert_eq!(state.sent_in_loss(), Sats::ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ fn div_btc(raw: u128) -> Cents {
|
||||
/// Trait for accumulating profit/loss across BTreeMap entries.
|
||||
/// `WithoutCapital` skips capital tracking; `WithCapital` tracks all fields.
|
||||
pub trait Accumulate: Default + Clone + Send + Sync + 'static {
|
||||
const TRACK_CAPITAL: bool;
|
||||
|
||||
fn to_output(&self) -> UnrealizedState;
|
||||
fn core(&self) -> &WithoutCapital;
|
||||
fn core_mut(&mut self) -> &mut WithoutCapital;
|
||||
@@ -78,6 +80,8 @@ pub trait Accumulate: Default + Clone + Send + Sync + 'static {
|
||||
}
|
||||
|
||||
impl Accumulate for WithoutCapital {
|
||||
const TRACK_CAPITAL: bool = false;
|
||||
|
||||
fn to_output(&self) -> UnrealizedState {
|
||||
UnrealizedState {
|
||||
supply_in_profit: self.supply_in_profit,
|
||||
@@ -114,6 +118,8 @@ impl Accumulate for WithoutCapital {
|
||||
}
|
||||
|
||||
impl Accumulate for WithCapital {
|
||||
const TRACK_CAPITAL: bool = true;
|
||||
|
||||
fn to_output(&self) -> UnrealizedState {
|
||||
UnrealizedState {
|
||||
capitalized_cap_in_profit_raw: self.capitalized_cap_in_profit,
|
||||
|
||||
@@ -40,7 +40,7 @@ use super::{
|
||||
metrics::AvgAmountMetrics,
|
||||
};
|
||||
|
||||
const VERSION: Version = Version::new(24 + brk_oracle::VERSION);
|
||||
const VERSION: Version = Version::new(30 + brk_oracle::VERSION);
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct AddrMetricsVecs<M: StorageMode = Rw> {
|
||||
@@ -170,6 +170,8 @@ struct DistributionTransientState {
|
||||
}
|
||||
|
||||
const SAVED_STAMPED_CHANGES: u16 = 10;
|
||||
/// Version of the fixed-width `FundedAddrData` record layout.
|
||||
const FUNDED_ADDR_DATA_VERSION: Version = Version::ONE;
|
||||
|
||||
impl Vecs {
|
||||
pub(crate) fn forced_import(
|
||||
@@ -177,6 +179,7 @@ impl Vecs {
|
||||
parent_version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
prices: &price::Vecs,
|
||||
) -> Result<Self> {
|
||||
let db_path = parent.join(super::DB_NAME);
|
||||
let states_path = db_path.join("states");
|
||||
@@ -185,16 +188,30 @@ impl Vecs {
|
||||
db.set_min_regions(50_000)?;
|
||||
|
||||
let version = parent_version + VERSION;
|
||||
let spot_price = prices.spot.cents.height.read_only_cached_boxed_clone();
|
||||
|
||||
let utxo_cohorts =
|
||||
UTXOCohorts::forced_import(&db, version, indexes, &states_path, cached_starts)?;
|
||||
let utxo_cohorts = UTXOCohorts::forced_import(
|
||||
&db,
|
||||
version,
|
||||
indexes,
|
||||
&states_path,
|
||||
cached_starts,
|
||||
&spot_price,
|
||||
)?;
|
||||
|
||||
let addr_cohorts =
|
||||
AddrCohorts::forced_import(&db, version, indexes, &states_path, cached_starts)?;
|
||||
let addr_cohorts = AddrCohorts::forced_import(
|
||||
&db,
|
||||
version,
|
||||
indexes,
|
||||
&states_path,
|
||||
cached_starts,
|
||||
&spot_price,
|
||||
)?;
|
||||
|
||||
// Create address data BytesVecs first so we can also use them for identity mappings
|
||||
let funded_addr_data_version = version + FUNDED_ADDR_DATA_VERSION;
|
||||
let funded_addr_index_to_funded_addr_data = BytesVec::forced_import_with(
|
||||
vecdb::ImportOptions::new(&db, "funded_addr_data", version)
|
||||
vecdb::ImportOptions::new(&db, "funded_addr_data", funded_addr_data_version)
|
||||
.with_saved_stamped_changes(SAVED_STAMPED_CHANGES),
|
||||
)?;
|
||||
let empty_addr_index_to_empty_addr_data = BytesVec::forced_import_with(
|
||||
@@ -205,7 +222,7 @@ impl Vecs {
|
||||
// Identity mappings for traversable
|
||||
let funded_addr_index = LazyVecFrom1::init(
|
||||
"funded_addr_index",
|
||||
version,
|
||||
funded_addr_data_version,
|
||||
funded_addr_index_to_funded_addr_data.read_only_boxed_clone(),
|
||||
|index, _| index,
|
||||
);
|
||||
@@ -232,19 +249,32 @@ impl Vecs {
|
||||
// `reused_*` uses the receive-side predicate (funded_txo_count > 1,
|
||||
// industry standard). `respent_*` uses the spend-side counterpart
|
||||
// (spent_txo_count > 1, strictly more restrictive).
|
||||
let reused_addr_count =
|
||||
ReusedAddrVecs::forced_import(&db, "reused", version, indexes, cached_starts)?;
|
||||
let respent_addr_count =
|
||||
ReusedAddrVecs::forced_import(&db, "respent", version, indexes, cached_starts)?;
|
||||
let reused_addr_count = ReusedAddrVecs::forced_import(
|
||||
&db,
|
||||
"reused",
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
&spot_price,
|
||||
)?;
|
||||
let respent_addr_count = ReusedAddrVecs::forced_import(
|
||||
&db,
|
||||
"respent",
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
&spot_price,
|
||||
)?;
|
||||
|
||||
// Exposed address tracking (counts + supply) - quantum / pubkey-exposure sense
|
||||
let exposed_addr_vecs = ExposedAddrVecs::forced_import(&db, version, indexes)?;
|
||||
let exposed_addr_vecs = ExposedAddrVecs::forced_import(&db, version, indexes, &spot_price)?;
|
||||
|
||||
// Growth rate: delta change + rate (global + per-type)
|
||||
let delta = DeltaVecs::new(version, &addr_count, cached_starts, indexes);
|
||||
|
||||
// Average amount (supply / utxo_count, supply / funded_addr_count) for `all` and per addr type.
|
||||
let avg_amount = WithAddrTypes::<AvgAmountMetrics>::forced_import(&db, version, indexes)?;
|
||||
let avg_amount =
|
||||
WithAddrTypes::<AvgAmountMetrics>::forced_import(&db, version, indexes, &spot_price)?;
|
||||
|
||||
let this = Self {
|
||||
supply_state: BytesVec::forced_import_with(
|
||||
@@ -585,7 +615,6 @@ impl Vecs {
|
||||
&starting_lengths,
|
||||
&outputs.by_type,
|
||||
&inputs.by_type,
|
||||
prices,
|
||||
all_supply_sats,
|
||||
&type_supply_sats,
|
||||
exit,
|
||||
@@ -594,14 +623,12 @@ impl Vecs {
|
||||
&starting_lengths,
|
||||
&outputs.by_type,
|
||||
&inputs.by_type,
|
||||
prices,
|
||||
all_supply_sats,
|
||||
&type_supply_sats,
|
||||
exit,
|
||||
)?;
|
||||
self.addrs.exposed.compute_rest(
|
||||
&starting_lengths,
|
||||
prices,
|
||||
all_supply_sats,
|
||||
&type_supply_sats,
|
||||
exit,
|
||||
@@ -610,7 +637,6 @@ impl Vecs {
|
||||
// Average amount (supply / utxo_count, supply / funded_addr_count) for `all` and per addr type.
|
||||
let all_m = &self.utxo_cohorts.all.metrics;
|
||||
self.addrs.avg_amount.all.compute(
|
||||
prices,
|
||||
&all_m.supply.total.sats.height,
|
||||
&all_m.outputs.unspent_count.height,
|
||||
&self.addrs.funded.all.height,
|
||||
@@ -626,7 +652,6 @@ impl Vecs {
|
||||
{
|
||||
let type_m = &t.get(ot).metrics;
|
||||
avg.compute(
|
||||
prices,
|
||||
&type_m.supply.total.sats.height,
|
||||
&type_m.outputs.unspent_count.height,
|
||||
&funded.height,
|
||||
@@ -672,7 +697,7 @@ impl Vecs {
|
||||
.height
|
||||
.read_only_clone();
|
||||
self.addr_cohorts
|
||||
.compute_rest_part2(prices, &starting_lengths, &all_supply_sats, exit)?;
|
||||
.compute_rest_part2(&starting_lengths, &all_supply_sats, exit)?;
|
||||
|
||||
let exit = exit.clone();
|
||||
self.db.run_bg(move |db| {
|
||||
|
||||
@@ -28,7 +28,7 @@ impl Vecs {
|
||||
&mut self,
|
||||
indexer: &Indexer,
|
||||
indexes: &indexes::Vecs,
|
||||
prices: &price::Vecs,
|
||||
_prices: &price::Vecs,
|
||||
distribution: &distribution::Vecs,
|
||||
cointime: &cointime::Vecs,
|
||||
exit: &Exit,
|
||||
@@ -67,7 +67,7 @@ impl Vecs {
|
||||
.map(|cohort| &cohort.coindays_created.cumulative.height)
|
||||
.collect();
|
||||
|
||||
let rest_start = self.compute_primary(
|
||||
self.compute_primary(
|
||||
&starting_lengths,
|
||||
&indexes.timestamp.monotonic,
|
||||
&transfer_volumes,
|
||||
@@ -77,9 +77,6 @@ impl Vecs {
|
||||
&realized_caps,
|
||||
exit,
|
||||
)?;
|
||||
let mut rest_lengths = starting_lengths.clone();
|
||||
rest_lengths.height = rest_start;
|
||||
self.compute_rest(prices, &rest_lengths, exit)?;
|
||||
|
||||
let exit = exit.clone();
|
||||
self.db.run_bg(move |db| {
|
||||
@@ -271,34 +268,6 @@ impl Vecs {
|
||||
Ok(Height::from(start))
|
||||
}
|
||||
|
||||
fn compute_rest(
|
||||
&mut self,
|
||||
prices: &price::Vecs,
|
||||
starting_lengths: &Lengths,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
for cohort in self.age_range.iter_mut() {
|
||||
cohort
|
||||
.supply
|
||||
.mobile
|
||||
.compute(prices, starting_lengths.height, exit)?;
|
||||
cohort
|
||||
.supply
|
||||
.immobile
|
||||
.compute(prices, starting_lengths.height, exit)?;
|
||||
}
|
||||
|
||||
self.supply
|
||||
.mobile
|
||||
.compute(prices, starting_lengths.height, exit)?;
|
||||
self.supply
|
||||
.immobile
|
||||
.compute(prices, starting_lengths.height, exit)?;
|
||||
self.price
|
||||
.compute_ratio(starting_lengths, &prices.spot.cents.height, exit)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn primary_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
let mut vecs = Vec::with_capacity(AGE_COHORT_COUNT * 4 + 5 + HORIZON_COUNT);
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@ use std::path::Path;
|
||||
|
||||
use brk_cohort::{AgeRange, CohortContext};
|
||||
use brk_error::Result;
|
||||
use brk_types::{StoredF64, Version};
|
||||
use vecdb::{ReadableCloneableVec, UnaryTransform};
|
||||
use brk_types::{Cents, Height, StoredF64, Version};
|
||||
use vecdb::{CachedBoxedVec, ReadableCloneableVec, UnaryTransform};
|
||||
|
||||
use super::{CohortVecs, DB_NAME, HorizonVecs, Horizons, Split, Vecs, mobility};
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
FiatPerBlock, LazyPerBlock, PerBlock, PriceWithRatioPerBlock, ValuePerBlock,
|
||||
FiatPerBlock, LazyPerBlock, PerBlock, PriceWithRatioPerBlock, SpotValuePerBlock,
|
||||
db_utils::{finalize_db, open_db},
|
||||
},
|
||||
};
|
||||
@@ -38,6 +38,7 @@ impl CohortVecs {
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let spending_rate =
|
||||
PerBlock::forced_import(db, &format!("{name}_spending_rate"), version, indexes)?;
|
||||
@@ -55,7 +56,13 @@ impl CohortVecs {
|
||||
spending_exposure,
|
||||
mobility,
|
||||
supply: import_split(|side| {
|
||||
ValuePerBlock::forced_import(db, &format!("{name}_{side}_supply"), version, indexes)
|
||||
SpotValuePerBlock::forced_import(
|
||||
db,
|
||||
&format!("{name}_{side}_supply"),
|
||||
version,
|
||||
indexes,
|
||||
spot_price,
|
||||
)
|
||||
})?,
|
||||
})
|
||||
}
|
||||
@@ -66,17 +73,25 @@ impl Vecs {
|
||||
parent_path: &Path,
|
||||
parent_version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
prices: &crate::price::Vecs,
|
||||
) -> Result<Self> {
|
||||
let db = open_db(parent_path, DB_NAME, 250_000)?;
|
||||
let version = parent_version + VERSION;
|
||||
let spot_price = prices.spot.cents.height.read_only_cached_boxed_clone();
|
||||
|
||||
let this = Self {
|
||||
age_range: AgeRange::try_new(|_, name| {
|
||||
let name = CohortContext::Utxo.prefixed(name);
|
||||
CohortVecs::forced_import(&db, &name, version, indexes)
|
||||
CohortVecs::forced_import(&db, &name, version, indexes, &spot_price)
|
||||
})?,
|
||||
supply: import_split(|side| {
|
||||
ValuePerBlock::forced_import(&db, &format!("{side}_supply"), version, indexes)
|
||||
SpotValuePerBlock::forced_import(
|
||||
&db,
|
||||
&format!("{side}_supply"),
|
||||
version,
|
||||
indexes,
|
||||
&spot_price,
|
||||
)
|
||||
})?,
|
||||
supply_in_loss_share: PerBlock::forced_import(
|
||||
&db,
|
||||
@@ -95,7 +110,13 @@ impl Vecs {
|
||||
})
|
||||
})?,
|
||||
cap: FiatPerBlock::forced_import(&db, "coinflow_cap", version, indexes)?,
|
||||
price: PriceWithRatioPerBlock::forced_import(&db, "coinflow_price", version, indexes)?,
|
||||
price: PriceWithRatioPerBlock::forced_import(
|
||||
&db,
|
||||
"coinflow_price",
|
||||
version,
|
||||
indexes,
|
||||
&spot_price,
|
||||
)?,
|
||||
db,
|
||||
};
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ mod vecs;
|
||||
|
||||
use brk_cohort::AGE_RANGE_BOUNDS;
|
||||
|
||||
pub(crate) use brk_cohort::AGE_RANGE_COUNT as AGE_COHORT_COUNT;
|
||||
pub(crate) use vecs::HORIZON_DAYS;
|
||||
pub use vecs::{CohortVecs, HorizonVecs, Horizons, Split, Vecs};
|
||||
|
||||
pub const DB_NAME: &str = "coinflow";
|
||||
|
||||
pub(crate) const AGE_COHORT_COUNT: usize = 21;
|
||||
pub(crate) const HORIZON_COUNT: usize = 7;
|
||||
pub(crate) const HOURS_PER_DAY: f64 = 24.0;
|
||||
pub(crate) const MINIMUM_DURATION_DAYS: f64 = 1.0 / HOURS_PER_DAY;
|
||||
|
||||
@@ -4,7 +4,7 @@ use brk_types::{Cents, StoredF64};
|
||||
use vecdb::{Database, Rw, StorageMode};
|
||||
|
||||
use crate::internal::{
|
||||
FiatPerBlock, LazyPerBlock, PerBlock, PriceWithRatioPerBlock, ValuePerBlock,
|
||||
FiatPerBlock, LazyPerBlock, PerBlock, PriceWithRatioPerBlock, SpotValuePerBlock,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Traversable)]
|
||||
@@ -113,7 +113,7 @@ pub struct CohortVecs<M: StorageMode = Rw> {
|
||||
pub spending_rate: PerBlock<StoredF64, M>,
|
||||
pub spending_exposure: PerBlock<StoredF64, M>,
|
||||
pub mobility: LazyPerBlock<StoredF64>,
|
||||
pub supply: Split<ValuePerBlock<M>>,
|
||||
pub supply: Split<SpotValuePerBlock<M>>,
|
||||
}
|
||||
|
||||
#[derive(Traversable)]
|
||||
@@ -122,7 +122,7 @@ pub struct Vecs<M: StorageMode = Rw> {
|
||||
pub(crate) db: Database,
|
||||
|
||||
pub age_range: AgeRange<CohortVecs<M>>,
|
||||
pub supply: Split<ValuePerBlock<M>>,
|
||||
pub supply: Split<SpotValuePerBlock<M>>,
|
||||
#[traversable(wrap = "supply/mobile/in_loss", rename = "share")]
|
||||
pub supply_in_loss_share: PerBlock<StoredF64, M>,
|
||||
pub horizon: Horizons<HorizonVecs<M>>,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use brk_cohort::AGE_RANGE_BOUNDS;
|
||||
use brk_cohort::{AGE_RANGE_BOUNDS, AGE_RANGE_COUNT};
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_types::{Bitcoin, Height, ONE_DAY_IN_SEC_F64, Sats, StoredF64, Timestamp, Version};
|
||||
@@ -6,9 +6,8 @@ use vecdb::{AnyVec, Exit, ReadableVec};
|
||||
|
||||
use super::super::activity;
|
||||
use super::{CohortVecs, Vecs};
|
||||
use crate::{distribution, indexes, price};
|
||||
use crate::{distribution, indexes};
|
||||
|
||||
const AGE_COHORT_COUNT: usize = 21;
|
||||
const HOURS_PER_DAY: f64 = 24.0;
|
||||
const WRITE_INTERVAL: usize = 10_000;
|
||||
|
||||
@@ -17,7 +16,6 @@ impl Vecs {
|
||||
&mut self,
|
||||
indexer: &Indexer,
|
||||
indexes: &indexes::Vecs,
|
||||
prices: &price::Vecs,
|
||||
distribution: &distribution::Vecs,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
@@ -48,7 +46,7 @@ impl Vecs {
|
||||
&coindays_destroyed,
|
||||
exit,
|
||||
)?;
|
||||
self.compute_rest(starting_height, prices, &supplies, exit)
|
||||
self.compute_rest(starting_height, &supplies, exit)
|
||||
}
|
||||
|
||||
fn compute_created<T, S>(
|
||||
@@ -62,7 +60,7 @@ impl Vecs {
|
||||
T: ReadableVec<Height, Timestamp>,
|
||||
S: ReadableVec<Height, Sats>,
|
||||
{
|
||||
debug_assert_eq!(supplies.len(), AGE_COHORT_COUNT);
|
||||
debug_assert_eq!(supplies.len(), AGE_RANGE_COUNT);
|
||||
|
||||
let created_version: Version = std::iter::once(timestamps.version())
|
||||
.chain(supplies.iter().map(|vec| vec.version()))
|
||||
@@ -138,8 +136,8 @@ impl Vecs {
|
||||
V: ReadableVec<Height, Sats>,
|
||||
D: ReadableVec<Height, StoredF64>,
|
||||
{
|
||||
debug_assert_eq!(transfer_volumes.len(), AGE_COHORT_COUNT);
|
||||
debug_assert_eq!(source_coindays_destroyed.len(), AGE_COHORT_COUNT);
|
||||
debug_assert_eq!(transfer_volumes.len(), AGE_RANGE_COUNT);
|
||||
debug_assert_eq!(source_coindays_destroyed.len(), AGE_RANGE_COUNT);
|
||||
|
||||
let destroyed_version: Version = transfer_volumes
|
||||
.iter()
|
||||
@@ -186,10 +184,10 @@ impl Vecs {
|
||||
.collect();
|
||||
|
||||
for offset in 0..(chunk_end - chunk_start) {
|
||||
let volumes_btc: [f64; AGE_COHORT_COUNT] = std::array::from_fn(|index| {
|
||||
let volumes_btc: [f64; AGE_RANGE_COUNT] = std::array::from_fn(|index| {
|
||||
f64::from(Bitcoin::from(transfer_batches[index][offset]))
|
||||
});
|
||||
let cdd: [f64; AGE_COHORT_COUNT] =
|
||||
let cdd: [f64; AGE_RANGE_COUNT] =
|
||||
std::array::from_fn(|index| f64::from(destroyed_batches[index][offset]));
|
||||
let consumed = allocate_consumed_coindays(volumes_btc, cdd, &bounds);
|
||||
|
||||
@@ -215,14 +213,13 @@ impl Vecs {
|
||||
fn compute_rest<S>(
|
||||
&mut self,
|
||||
starting_height: Height,
|
||||
prices: &price::Vecs,
|
||||
supplies: &[&S],
|
||||
exit: &Exit,
|
||||
) -> Result<()>
|
||||
where
|
||||
S: ReadableVec<Height, Sats>,
|
||||
{
|
||||
debug_assert_eq!(supplies.len(), AGE_COHORT_COUNT);
|
||||
debug_assert_eq!(supplies.len(), AGE_RANGE_COUNT);
|
||||
|
||||
for (cohort, &total_supply) in self.iter_mut().zip(supplies) {
|
||||
let CohortVecs {
|
||||
@@ -244,7 +241,6 @@ impl Vecs {
|
||||
|
||||
supply.compute_from(
|
||||
starting_height,
|
||||
prices,
|
||||
total_supply,
|
||||
&activity_vecs.liveliness.height,
|
||||
&activity_vecs.vaultedness.height,
|
||||
@@ -275,12 +271,12 @@ fn coindays_created(supply: Sats, interval_seconds: u32) -> StoredF64 {
|
||||
StoredF64::from(f64::from(Bitcoin::from(supply)) * interval_seconds as f64 / ONE_DAY_IN_SEC_F64)
|
||||
}
|
||||
|
||||
fn age_bounds_days() -> [(f64, f64); AGE_COHORT_COUNT] {
|
||||
fn age_bounds_days() -> [(f64, f64); AGE_RANGE_COUNT] {
|
||||
let mut bounds = AGE_RANGE_BOUNDS.iter();
|
||||
std::array::from_fn(|index| {
|
||||
let bound = bounds.next().unwrap();
|
||||
let lower = bound.start as f64 / HOURS_PER_DAY;
|
||||
let width = if index + 1 < AGE_COHORT_COUNT {
|
||||
let width = if index + 1 < AGE_RANGE_COUNT {
|
||||
(bound.end - bound.start) as f64 / HOURS_PER_DAY
|
||||
} else {
|
||||
0.0
|
||||
@@ -290,14 +286,14 @@ fn age_bounds_days() -> [(f64, f64); AGE_COHORT_COUNT] {
|
||||
}
|
||||
|
||||
fn allocate_consumed_coindays(
|
||||
transfer_volume_btc: [f64; AGE_COHORT_COUNT],
|
||||
coindays_destroyed: [f64; AGE_COHORT_COUNT],
|
||||
bounds: &[(f64, f64); AGE_COHORT_COUNT],
|
||||
) -> [f64; AGE_COHORT_COUNT] {
|
||||
let mut result = [0.0; AGE_COHORT_COUNT];
|
||||
transfer_volume_btc: [f64; AGE_RANGE_COUNT],
|
||||
coindays_destroyed: [f64; AGE_RANGE_COUNT],
|
||||
bounds: &[(f64, f64); AGE_RANGE_COUNT],
|
||||
) -> [f64; AGE_RANGE_COUNT] {
|
||||
let mut result = [0.0; AGE_RANGE_COUNT];
|
||||
let mut older_transfer_volume = 0.0;
|
||||
|
||||
for index in (0..AGE_COHORT_COUNT).rev() {
|
||||
for index in (0..AGE_RANGE_COUNT).rev() {
|
||||
let (lower_days, width_days) = bounds[index];
|
||||
let within_cohort =
|
||||
(coindays_destroyed[index] - transfer_volume_btc[index] * lower_days).max(0.0);
|
||||
@@ -335,8 +331,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn destruction_at_a_boundary_stays_in_the_ranges_already_traversed() {
|
||||
let mut volumes = [0.0; AGE_COHORT_COUNT];
|
||||
let mut cdd = [0.0; AGE_COHORT_COUNT];
|
||||
let mut volumes = [0.0; AGE_RANGE_COUNT];
|
||||
let mut cdd = [0.0; AGE_RANGE_COUNT];
|
||||
volumes[2] = 1.0;
|
||||
cdd[2] = 1.0;
|
||||
|
||||
@@ -350,8 +346,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn consumed_coindays_cover_every_traversed_cohort() {
|
||||
let mut volumes = [0.0; AGE_COHORT_COUNT];
|
||||
let mut cdd = [0.0; AGE_COHORT_COUNT];
|
||||
let mut volumes = [0.0; AGE_RANGE_COUNT];
|
||||
let mut cdd = [0.0; AGE_RANGE_COUNT];
|
||||
volumes[2] = 2.0;
|
||||
cdd[2] = 20.0;
|
||||
|
||||
@@ -366,10 +362,10 @@ mod tests {
|
||||
#[test]
|
||||
fn allocated_coindays_conserve_mixed_cohort_destruction() {
|
||||
let bounds = age_bounds_days();
|
||||
let mut volumes = [0.0; AGE_COHORT_COUNT];
|
||||
let mut cdd = [0.0; AGE_COHORT_COUNT];
|
||||
let mut volumes = [0.0; AGE_RANGE_COUNT];
|
||||
let mut cdd = [0.0; AGE_RANGE_COUNT];
|
||||
|
||||
for index in [0, 1, 2, 10, 20] {
|
||||
for index in [0, 1, 2, 10, 20, AGE_RANGE_COUNT - 2, AGE_RANGE_COUNT - 1] {
|
||||
let (lower, width) = bounds[index];
|
||||
let volume = index as f64 + 1.0;
|
||||
let age = lower + if width > 0.0 { width / 2.0 } else { 30.0 };
|
||||
@@ -381,4 +377,21 @@ mod tests {
|
||||
|
||||
assert!((allocated.iter().sum::<f64>() - cdd.iter().sum::<f64>()).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounds_and_allocation_cover_every_canonical_age_range() {
|
||||
let bounds = age_bounds_days();
|
||||
let mut volumes = [0.0; AGE_RANGE_COUNT];
|
||||
let mut cdd = [0.0; AGE_RANGE_COUNT];
|
||||
let last = AGE_RANGE_COUNT - 1;
|
||||
|
||||
volumes[last] = 1.0;
|
||||
cdd[last] = bounds[last].0 + 30.0;
|
||||
|
||||
let allocated = allocate_consumed_coindays(volumes, cdd, &bounds);
|
||||
|
||||
assert_eq!(bounds.len(), AGE_RANGE_BOUNDS.iter().count());
|
||||
assert!(allocated[last] > 0.0);
|
||||
assert!((allocated.iter().sum::<f64>() - cdd.iter().sum::<f64>()).abs() < 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use brk_cohort::{AgeRange, CohortContext};
|
||||
use brk_error::Result;
|
||||
use brk_types::Version;
|
||||
use vecdb::Database;
|
||||
use brk_types::{Cents, Height, Version};
|
||||
use vecdb::{CachedBoxedVec, Database};
|
||||
|
||||
use super::{CohortVecs, Vecs};
|
||||
use crate::{
|
||||
@@ -20,6 +20,7 @@ impl CohortVecs {
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
coindays_created: PerBlockCumulativeRolling::forced_import(
|
||||
@@ -44,7 +45,9 @@ impl CohortVecs {
|
||||
cached_starts,
|
||||
)?,
|
||||
activity: ActivityDerivedVecs::forced_import_with_prefix(db, name, version, indexes)?,
|
||||
supply: SupplyBaseVecs::forced_import_with_prefix(db, name, version, indexes)?,
|
||||
supply: SupplyBaseVecs::forced_import_with_prefix(
|
||||
db, name, version, indexes, spot_price,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -55,12 +58,13 @@ impl Vecs {
|
||||
parent_version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let version = parent_version + VERSION;
|
||||
|
||||
Ok(Self(AgeRange::try_new(|_, name| {
|
||||
let name = CohortContext::Utxo.prefixed(name);
|
||||
CohortVecs::forced_import(db, &name, version, indexes, cached_starts)
|
||||
CohortVecs::forced_import(db, &name, version, indexes, cached_starts, spot_price)
|
||||
})?))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,12 @@ use vecdb::Exit;
|
||||
|
||||
use super::super::{activity, value};
|
||||
use super::Vecs;
|
||||
use crate::{distribution, mining};
|
||||
use crate::distribution;
|
||||
|
||||
impl Vecs {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn compute(
|
||||
&mut self,
|
||||
indexer: &Indexer,
|
||||
mining: &mining::Vecs,
|
||||
distribution: &distribution::Vecs,
|
||||
activity: &activity::Vecs,
|
||||
value: &value::Vecs,
|
||||
@@ -23,13 +21,6 @@ impl Vecs {
|
||||
let realized_cap_cents = &all_metrics.realized.cap.cents.height;
|
||||
let circulating_supply = &all_metrics.supply.total.btc.height;
|
||||
|
||||
self.thermo.cents.height.compute_transform(
|
||||
starting_lengths.height,
|
||||
&mining.rewards.subsidy.cumulative.cents.height,
|
||||
|(i, v, ..)| (i, v),
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.investor.cents.height.compute_subtract(
|
||||
starting_lengths.height,
|
||||
realized_cap_cents,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::Version;
|
||||
use vecdb::Database;
|
||||
use brk_types::{Cents, Version};
|
||||
use vecdb::{Database, ReadableCloneableVec};
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{FiatPerBlock, RatioPerBlock},
|
||||
internal::{
|
||||
CentsUnsignedToDollars, FiatPerBlock, Identity, LazyFiatPerBlock, LazyPerBlock, PerBlock,
|
||||
RatioPerBlock,
|
||||
},
|
||||
};
|
||||
|
||||
impl Vecs {
|
||||
@@ -13,9 +16,24 @@ impl Vecs {
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
subsidy_cents: &PerBlock<Cents>,
|
||||
) -> Result<Self> {
|
||||
let thermo_cents = LazyPerBlock::from_computed::<Identity<Cents>>(
|
||||
"thermo_cap_cents",
|
||||
version,
|
||||
subsidy_cents.height.read_only_boxed_clone(),
|
||||
subsidy_cents,
|
||||
);
|
||||
let thermo_usd = LazyPerBlock::from_lazy::<CentsUnsignedToDollars, Cents>(
|
||||
"thermo_cap",
|
||||
version,
|
||||
&thermo_cents,
|
||||
);
|
||||
Ok(Self {
|
||||
thermo: FiatPerBlock::forced_import(db, "thermo_cap", version, indexes)?,
|
||||
thermo: LazyFiatPerBlock {
|
||||
usd: thermo_usd,
|
||||
cents: thermo_cents,
|
||||
},
|
||||
investor: FiatPerBlock::forced_import(db, "investor_cap", version, indexes)?,
|
||||
vaulted: FiatPerBlock::forced_import(db, "vaulted_cap", version, indexes)?,
|
||||
active: FiatPerBlock::forced_import(db, "active_cap", version, indexes)?,
|
||||
|
||||
@@ -2,11 +2,11 @@ use brk_traversable::Traversable;
|
||||
use brk_types::{Cents, PartsPerMillion32};
|
||||
use vecdb::{Rw, StorageMode};
|
||||
|
||||
use crate::internal::{FiatPerBlock, RatioPerBlock};
|
||||
use crate::internal::{FiatPerBlock, LazyFiatPerBlock, RatioPerBlock};
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct Vecs<M: StorageMode = Rw> {
|
||||
pub thermo: FiatPerBlock<Cents, M>,
|
||||
pub thermo: LazyFiatPerBlock<Cents>,
|
||||
pub investor: FiatPerBlock<Cents, M>,
|
||||
pub vaulted: FiatPerBlock<Cents, M>,
|
||||
pub active: FiatPerBlock<Cents, M>,
|
||||
|
||||
@@ -3,7 +3,7 @@ use brk_indexer::Indexer;
|
||||
use vecdb::Exit;
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{blocks, distribution, indexes, mining, price, supply};
|
||||
use crate::{blocks, distribution, indexes, price, supply};
|
||||
|
||||
impl Vecs {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -13,7 +13,6 @@ impl Vecs {
|
||||
indexes: &indexes::Vecs,
|
||||
prices: &price::Vecs,
|
||||
blocks: &blocks::Vecs,
|
||||
mining: &mining::Vecs,
|
||||
supply_vecs: &supply::Vecs,
|
||||
distribution: &distribution::Vecs,
|
||||
exit: &Exit,
|
||||
@@ -23,19 +22,13 @@ impl Vecs {
|
||||
// Activity computes first (liveliness, vaultedness, etc.)
|
||||
self.activity.compute(indexer, distribution, exit)?;
|
||||
self.age_range
|
||||
.compute(indexer, indexes, prices, distribution, exit)?;
|
||||
.compute(indexer, indexes, distribution, exit)?;
|
||||
|
||||
// Phase 2: supply, adjusted, and value are independent.
|
||||
let (r1, r2) = rayon::join(
|
||||
|| {
|
||||
self.supply.compute(
|
||||
indexer,
|
||||
prices,
|
||||
distribution,
|
||||
&self.activity,
|
||||
&self.age_range,
|
||||
exit,
|
||||
)
|
||||
self.supply
|
||||
.compute(indexer, distribution, &self.activity, &self.age_range, exit)
|
||||
},
|
||||
|| {
|
||||
rayon::join(
|
||||
@@ -55,14 +48,8 @@ impl Vecs {
|
||||
r2.1?;
|
||||
|
||||
// Cap depends on activity + value
|
||||
self.cap.compute(
|
||||
indexer,
|
||||
mining,
|
||||
distribution,
|
||||
&self.activity,
|
||||
&self.value,
|
||||
exit,
|
||||
)?;
|
||||
self.cap
|
||||
.compute(indexer, distribution, &self.activity, &self.value, exit)?;
|
||||
|
||||
// Phase 4: pricing and reserve_risk are independent
|
||||
let (r3, r4) = rayon::join(
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use std::path::Path;
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_types::Version;
|
||||
use brk_types::{Cents, Version};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::db_utils::{finalize_db, open_db},
|
||||
internal::{
|
||||
PerBlock,
|
||||
db_utils::{finalize_db, open_db},
|
||||
},
|
||||
price,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -21,18 +25,23 @@ impl Vecs {
|
||||
parent_version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
prices: &price::Vecs,
|
||||
subsidy_cents: &PerBlock<Cents>,
|
||||
) -> Result<Self> {
|
||||
let db = open_db(parent_path, DB_NAME, 250_000)?;
|
||||
let version = parent_version;
|
||||
let v1 = version + Version::ONE;
|
||||
let spot_price = prices.spot.cents.height.read_only_cached_boxed_clone();
|
||||
let activity = ActivityVecs::forced_import(&db, version, indexes, cached_starts)?;
|
||||
let age_range = AgeRangeVecs::forced_import(&db, version, indexes, cached_starts)?;
|
||||
let supply = SupplyVecs::forced_import(&db, v1, indexes)?;
|
||||
let age_range =
|
||||
AgeRangeVecs::forced_import(&db, version, indexes, cached_starts, &spot_price)?;
|
||||
let supply = SupplyVecs::forced_import(&db, v1, indexes, &spot_price)?;
|
||||
let value = ValueVecs::forced_import(&db, v1, indexes, cached_starts)?;
|
||||
let cap = CapVecs::forced_import(&db, version + Version::TWO, indexes)?;
|
||||
let prices = PricesVecs::forced_import(&db, version + Version::new(3), indexes)?;
|
||||
let cap = CapVecs::forced_import(&db, version + Version::TWO, indexes, subsidy_cents)?;
|
||||
let prices =
|
||||
PricesVecs::forced_import(&db, version + Version::new(3), indexes, &spot_price)?;
|
||||
let adjusted = AdjustedVecs::forced_import(&db, version, indexes)?;
|
||||
let reserve_risk = ReserveRiskVecs::forced_import(&db, v1, indexes)?;
|
||||
let reserve_risk = ReserveRiskVecs::forced_import(&db, v1, indexes, &spot_price)?;
|
||||
|
||||
let this = Self {
|
||||
db,
|
||||
|
||||
@@ -12,7 +12,7 @@ impl Vecs {
|
||||
pub(crate) fn compute(
|
||||
&mut self,
|
||||
indexer: &Indexer,
|
||||
prices: &price::Vecs,
|
||||
_prices: &price::Vecs,
|
||||
distribution: &distribution::Vecs,
|
||||
activity: &activity::Vecs,
|
||||
supply: &supply::Vecs,
|
||||
@@ -24,57 +24,43 @@ impl Vecs {
|
||||
let circulating_supply = &all_metrics.supply.total.btc.height;
|
||||
let realized_price = &all_metrics.realized.price.cents.height;
|
||||
|
||||
self.vaulted
|
||||
.compute_all(prices, &starting_lengths, exit, |v| {
|
||||
Ok(v.compute_transform2(
|
||||
starting_lengths.height,
|
||||
realized_price,
|
||||
&activity.vaultedness.height,
|
||||
|(i, price, vaultedness, ..)| {
|
||||
(i, Cents::from(f64::from(price) / f64::from(vaultedness)))
|
||||
},
|
||||
exit,
|
||||
)?)
|
||||
})?;
|
||||
self.vaulted.cents.height.compute_transform2(
|
||||
starting_lengths.height,
|
||||
realized_price,
|
||||
&activity.vaultedness.height,
|
||||
|(i, price, vaultedness, ..)| {
|
||||
(i, Cents::from(f64::from(price) / f64::from(vaultedness)))
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.active
|
||||
.compute_all(prices, &starting_lengths, exit, |v| {
|
||||
Ok(v.compute_transform2(
|
||||
starting_lengths.height,
|
||||
realized_price,
|
||||
&activity.liveliness.height,
|
||||
|(i, price, liveliness, ..)| {
|
||||
(i, Cents::from(f64::from(price) / f64::from(liveliness)))
|
||||
},
|
||||
exit,
|
||||
)?)
|
||||
})?;
|
||||
self.active.cents.height.compute_transform2(
|
||||
starting_lengths.height,
|
||||
realized_price,
|
||||
&activity.liveliness.height,
|
||||
|(i, price, liveliness, ..)| (i, Cents::from(f64::from(price) / f64::from(liveliness))),
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.true_market_mean
|
||||
.compute_all(prices, &starting_lengths, exit, |v| {
|
||||
Ok(v.compute_transform2(
|
||||
starting_lengths.height,
|
||||
&cap.investor.cents.height,
|
||||
&supply.active.btc.height,
|
||||
|(i, cap_cents, supply_btc, ..)| {
|
||||
(i, Cents::from(f64::from(cap_cents) / f64::from(supply_btc)))
|
||||
},
|
||||
exit,
|
||||
)?)
|
||||
})?;
|
||||
self.true_market_mean.cents.height.compute_transform2(
|
||||
starting_lengths.height,
|
||||
&cap.investor.cents.height,
|
||||
&supply.active.btc.height,
|
||||
|(i, cap_cents, supply_btc, ..)| {
|
||||
(i, Cents::from(f64::from(cap_cents) / f64::from(supply_btc)))
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.cointime
|
||||
.compute_all(prices, &starting_lengths, exit, |v| {
|
||||
Ok(v.compute_transform2(
|
||||
starting_lengths.height,
|
||||
&cap.cointime.cents.height,
|
||||
circulating_supply,
|
||||
|(i, cap_cents, supply_btc, ..)| {
|
||||
(i, Cents::from(f64::from(cap_cents) / f64::from(supply_btc)))
|
||||
},
|
||||
exit,
|
||||
)?)
|
||||
})?;
|
||||
self.cointime.cents.height.compute_transform2(
|
||||
starting_lengths.height,
|
||||
&cap.cointime.cents.height,
|
||||
circulating_supply,
|
||||
|(i, cap_cents, supply_btc, ..)| {
|
||||
(i, Cents::from(f64::from(cap_cents) / f64::from(supply_btc)))
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::Version;
|
||||
use vecdb::Database;
|
||||
use brk_types::{Cents, Height, Version};
|
||||
use vecdb::{CachedBoxedVec, Database};
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{indexes, internal::PriceWithRatioPerBlock};
|
||||
@@ -10,10 +10,11 @@ impl Vecs {
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
macro_rules! import {
|
||||
($name:expr) => {
|
||||
PriceWithRatioPerBlock::forced_import(db, $name, version, indexes)?
|
||||
PriceWithRatioPerBlock::forced_import(db, $name, version, indexes, spot_price)?
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -32,13 +32,6 @@ impl Vecs {
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.value.height.compute_divide(
|
||||
starting_height,
|
||||
&prices.spot.usd.height,
|
||||
&self.hodl_bank,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,38 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::Version;
|
||||
use vecdb::{Database, EagerVec, ImportableVec};
|
||||
use brk_types::{Cents, Dollars, Height, StoredF64, Version};
|
||||
use vecdb::{CachedBoxedVec, Database, EagerVec, ImportableVec, ReadableCloneableVec};
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{indexes, internal::PerBlock};
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{Identity, LazyIndexedVec, LazyPerBlock},
|
||||
};
|
||||
|
||||
impl Vecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let v1 = version + Version::ONE;
|
||||
let hodl_bank = EagerVec::forced_import(db, "hodl_bank", v1)?;
|
||||
let value_source = LazyIndexedVec::new(
|
||||
"reserve_risk_source",
|
||||
v1,
|
||||
hodl_bank.read_only_boxed_clone(),
|
||||
spot_price.clone(),
|
||||
|_, hodl_bank, spot| StoredF64::from(Dollars::from(spot)) / hodl_bank,
|
||||
);
|
||||
Ok(Self {
|
||||
vocdd_median_1y: EagerVec::forced_import(db, "vocdd_median_1y", v1)?,
|
||||
hodl_bank: EagerVec::forced_import(db, "hodl_bank", v1)?,
|
||||
value: PerBlock::forced_import(db, "reserve_risk", v1, indexes)?,
|
||||
hodl_bank,
|
||||
value: LazyPerBlock::from_height_source::<Identity<StoredF64>, _>(
|
||||
"reserve_risk",
|
||||
v1,
|
||||
value_source,
|
||||
indexes,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ use brk_traversable::Traversable;
|
||||
use brk_types::{Height, StoredF64};
|
||||
use vecdb::{EagerVec, PcoVec, Rw, StorageMode};
|
||||
|
||||
use crate::internal::PerBlock;
|
||||
use crate::internal::LazyPerBlock;
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct Vecs<M: StorageMode = Rw> {
|
||||
pub value: PerBlock<StoredF64, M>,
|
||||
pub value: LazyPerBlock<StoredF64>,
|
||||
pub vocdd_median_1y: M::Stored<EagerVec<PcoVec<Height, StoredF64>>>,
|
||||
pub hodl_bank: M::Stored<EagerVec<PcoVec<Height, StoredF64>>>,
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, WritableVec};
|
||||
|
||||
use super::super::{activity, age_range};
|
||||
use super::{BaseVecs, Vecs};
|
||||
use crate::{distribution, frameworks::WeightedRatio, price};
|
||||
use crate::{distribution, frameworks::WeightedRatio};
|
||||
|
||||
const WRITE_INTERVAL: usize = 10_000;
|
||||
|
||||
@@ -14,7 +14,6 @@ impl BaseVecs {
|
||||
pub(crate) fn compute_from(
|
||||
&mut self,
|
||||
starting_height: Height,
|
||||
prices: &price::Vecs,
|
||||
total_supply: &impl ReadableVec<Height, Sats>,
|
||||
liveliness: &impl ReadableVec<Height, StoredF64>,
|
||||
vaultedness: &impl ReadableVec<Height, StoredF64>,
|
||||
@@ -34,9 +33,6 @@ impl BaseVecs {
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.vaulted.compute(prices, starting_height, exit)?;
|
||||
self.active.compute(prices, starting_height, exit)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -45,7 +41,6 @@ impl Vecs {
|
||||
pub(crate) fn compute(
|
||||
&mut self,
|
||||
indexer: &Indexer,
|
||||
prices: &price::Vecs,
|
||||
distribution: &distribution::Vecs,
|
||||
activity: &activity::Vecs,
|
||||
age_range: &age_range::Vecs,
|
||||
@@ -63,7 +58,6 @@ impl Vecs {
|
||||
|
||||
self.base.compute_from(
|
||||
starting_height,
|
||||
prices,
|
||||
circulating_supply,
|
||||
&activity.liveliness.height,
|
||||
&activity.vaultedness.height,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::Version;
|
||||
use vecdb::Database;
|
||||
use brk_types::{Cents, Height, Version};
|
||||
use vecdb::{CachedBoxedVec, Database};
|
||||
|
||||
use super::{BaseVecs, Vecs};
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{PerBlock, ValuePerBlock},
|
||||
internal::{PerBlock, SpotValuePerBlock},
|
||||
};
|
||||
|
||||
impl BaseVecs {
|
||||
@@ -14,6 +14,7 @@ impl BaseVecs {
|
||||
prefix: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let name = |metric: &str| {
|
||||
if prefix.is_empty() {
|
||||
@@ -24,8 +25,20 @@ impl BaseVecs {
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
vaulted: ValuePerBlock::forced_import(db, &name("vaulted_supply"), version, indexes)?,
|
||||
active: ValuePerBlock::forced_import(db, &name("active_supply"), version, indexes)?,
|
||||
vaulted: SpotValuePerBlock::forced_import(
|
||||
db,
|
||||
&name("vaulted_supply"),
|
||||
version,
|
||||
indexes,
|
||||
spot_price,
|
||||
)?,
|
||||
active: SpotValuePerBlock::forced_import(
|
||||
db,
|
||||
&name("active_supply"),
|
||||
version,
|
||||
indexes,
|
||||
spot_price,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -35,9 +48,10 @@ impl Vecs {
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
base: BaseVecs::forced_import_with_prefix(db, "", version, indexes)?,
|
||||
base: BaseVecs::forced_import_with_prefix(db, "", version, indexes, spot_price)?,
|
||||
active_supply_in_loss_share: PerBlock::forced_import(
|
||||
db,
|
||||
"cointime_supply_in_loss_share",
|
||||
|
||||
@@ -3,12 +3,12 @@ use brk_types::StoredF64;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{Rw, StorageMode};
|
||||
|
||||
use crate::internal::{PerBlock, ValuePerBlock};
|
||||
use crate::internal::{PerBlock, SpotValuePerBlock};
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct BaseVecs<M: StorageMode = Rw> {
|
||||
pub vaulted: ValuePerBlock<M>,
|
||||
pub active: ValuePerBlock<M>,
|
||||
pub vaulted: SpotValuePerBlock<M>,
|
||||
pub active: SpotValuePerBlock<M>,
|
||||
}
|
||||
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
|
||||
@@ -35,30 +35,49 @@ impl Vecs {
|
||||
gini::compute(&mut self.gini, distribution, indexer, exit)?;
|
||||
|
||||
// RHODL Ratio: 1d-1w realized cap / 1y-2y realized cap
|
||||
self.rhodl_ratio
|
||||
.ppm
|
||||
.compute_binary::<Dollars, Dollars, RatioDollars<PartsPerMillion64>>(
|
||||
starting_lengths.height,
|
||||
&distribution
|
||||
.utxo_cohorts
|
||||
.age_range
|
||||
._1d_to_1w
|
||||
.metrics
|
||||
.realized
|
||||
.cap
|
||||
.usd
|
||||
.height,
|
||||
&distribution
|
||||
.utxo_cohorts
|
||||
.age_range
|
||||
._1y_to_2y
|
||||
.metrics
|
||||
.realized
|
||||
.cap
|
||||
.usd
|
||||
.height,
|
||||
exit,
|
||||
)?;
|
||||
self.rhodl_ratio.ppm.height.compute_transform3(
|
||||
starting_lengths.height,
|
||||
&distribution
|
||||
.utxo_cohorts
|
||||
.age_range
|
||||
._1d_to_1w
|
||||
.metrics
|
||||
.realized
|
||||
.cap
|
||||
.usd
|
||||
.height,
|
||||
&distribution
|
||||
.utxo_cohorts
|
||||
.age_range
|
||||
._1y_to_18m
|
||||
.metrics
|
||||
.realized
|
||||
.cap
|
||||
.usd
|
||||
.height,
|
||||
&distribution
|
||||
.utxo_cohorts
|
||||
.age_range
|
||||
._18m_to_2y
|
||||
.metrics
|
||||
.realized
|
||||
.cap
|
||||
.usd
|
||||
.height,
|
||||
|(i, young_cap, year1_cap, month18_cap, ..)| {
|
||||
let denominator = year1_cap + month18_cap;
|
||||
let ratio = f64::from(young_cap) / f64::from(denominator);
|
||||
(
|
||||
i,
|
||||
if ratio.is_finite() {
|
||||
PartsPerMillion64::from(ratio)
|
||||
} else {
|
||||
PartsPerMillion64::default()
|
||||
},
|
||||
)
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
|
||||
// NVT: market_cap / tx_volume_24h
|
||||
let market_cap = &distribution
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Dollars, Version};
|
||||
use vecdb::ReadableCloneableVec;
|
||||
|
||||
use crate::internal::{FiatType, Identity, LazyPerBlock, NumericValue, PerBlock};
|
||||
use crate::internal::{FiatType, Identity, LazyPerBlock, NumericValue};
|
||||
|
||||
/// Lazy fiat: both cents and usd are lazy views of a stored source.
|
||||
/// Zero extra stored vecs.
|
||||
@@ -13,22 +12,13 @@ pub struct LazyFiatPerBlock<C: FiatType> {
|
||||
}
|
||||
|
||||
impl<C: FiatType> LazyFiatPerBlock<C> {
|
||||
pub(crate) fn from_computed(name: &str, version: Version, source: &PerBlock<C>) -> Self
|
||||
pub(crate) fn from_lazy(name: &str, version: Version, source: &LazyPerBlock<C>) -> Self
|
||||
where
|
||||
C: NumericValue,
|
||||
{
|
||||
let cents = LazyPerBlock::from_computed::<Identity<C>>(
|
||||
&format!("{name}_cents"),
|
||||
version,
|
||||
source.height.read_only_boxed_clone(),
|
||||
source,
|
||||
);
|
||||
let usd = LazyPerBlock::from_computed::<C::ToDollars>(
|
||||
name,
|
||||
version,
|
||||
source.height.read_only_boxed_clone(),
|
||||
source,
|
||||
);
|
||||
let cents =
|
||||
LazyPerBlock::from_lazy::<Identity<C>, C>(&format!("{name}_cents"), version, source);
|
||||
let usd = LazyPerBlock::from_lazy::<C::ToDollars, C>(name, version, source);
|
||||
Self { usd, cents }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,24 +119,6 @@ impl<B: FixedRatio> LazyPercentPerBlock<B> {
|
||||
}
|
||||
|
||||
impl LazyPercentPerBlock<brk_types::PartsPerMillionSigned64> {
|
||||
pub(crate) fn from_cagr(
|
||||
name: &str,
|
||||
version: Version,
|
||||
years: u8,
|
||||
source: &PercentPerBlock<brk_types::PartsPerMillionSigned64>,
|
||||
) -> Self {
|
||||
match years {
|
||||
2 => Self::from_percent::<Cagr<2>>(name, version, source),
|
||||
3 => Self::from_percent::<Cagr<3>>(name, version, source),
|
||||
4 => Self::from_percent::<Cagr<4>>(name, version, source),
|
||||
5 => Self::from_percent::<Cagr<5>>(name, version, source),
|
||||
6 => Self::from_percent::<Cagr<6>>(name, version, source),
|
||||
8 => Self::from_percent::<Cagr<8>>(name, version, source),
|
||||
10 => Self::from_percent::<Cagr<10>>(name, version, source),
|
||||
_ => unreachable!("unsupported DCA CAGR period: {years} years"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_lazy_cagr(name: &str, version: Version, years: u8, source: &Self) -> Self {
|
||||
match years {
|
||||
2 => Self::from_lazy_percent::<Cagr<2>>(name, version, source),
|
||||
|
||||
@@ -8,9 +8,7 @@ use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Cents, Dollars, SatsFract, Version};
|
||||
use schemars::JsonSchema;
|
||||
use vecdb::{
|
||||
Database, ReadableCloneableVec, ReadableVec, TypedVec, UnaryTransform,
|
||||
};
|
||||
use vecdb::{Database, ReadableCloneableVec, ReadableVec, TypedVec, UnaryTransform};
|
||||
|
||||
use super::{LazyPerBlock, PerBlock};
|
||||
use crate::{
|
||||
@@ -95,8 +93,7 @@ impl Price<LazyPerBlock<Cents>> {
|
||||
source,
|
||||
indexes,
|
||||
);
|
||||
let usd =
|
||||
LazyPerBlock::from_lazy::<CentsUnsignedToDollars, Cents>(name, version, ¢s);
|
||||
let usd = LazyPerBlock::from_lazy::<CentsUnsignedToDollars, Cents>(name, version, ¢s);
|
||||
let sats = LazyPerBlock::from_lazy::<DollarsToSatsFract, Cents>(
|
||||
&format!("{name}_sats"),
|
||||
version,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{StoredF32, Version};
|
||||
use brk_types::{Height, StoredF32, Version};
|
||||
use schemars::JsonSchema;
|
||||
use vecdb::{ReadableCloneableVec, UnaryTransform};
|
||||
use vecdb::{ReadableVec, TypedVec, UnaryTransform};
|
||||
|
||||
use crate::internal::{FixedRatio, LazyPerBlock, NumericValue, PerBlock};
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{ComputedVecValue, FixedRatio, Identity, LazyPerBlock, NumericValue},
|
||||
};
|
||||
|
||||
/// Fully lazy variant of `RatioPerBlock` derived from one per-block source.
|
||||
#[derive(Clone, Traversable)]
|
||||
@@ -21,18 +24,44 @@ where
|
||||
R: FixedRatio,
|
||||
S: NumericValue + JsonSchema,
|
||||
{
|
||||
pub(crate) fn from_source<F>(name: &str, version: Version, source: &PerBlock<S>) -> Self
|
||||
pub(crate) fn from_lazy_source<F, S2T>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source: &LazyPerBlock<S, S2T>,
|
||||
) -> Self
|
||||
where
|
||||
F: UnaryTransform<S, R>,
|
||||
S2T: ComputedVecValue + JsonSchema,
|
||||
{
|
||||
let ppm = LazyPerBlock::from_computed::<F>(
|
||||
&format!("{name}_{}", R::SUFFIX),
|
||||
version,
|
||||
source.height.read_only_boxed_clone(),
|
||||
source,
|
||||
);
|
||||
let ppm =
|
||||
LazyPerBlock::from_lazy::<F, S2T>(&format!("{name}_{}", R::SUFFIX), version, source);
|
||||
let ratio = LazyPerBlock::from_lazy::<R::ToRatio, S>(name, version, &ppm);
|
||||
|
||||
Self { ppm, ratio }
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> LazyRatioPerBlock<R>
|
||||
where
|
||||
R: FixedRatio,
|
||||
{
|
||||
pub(crate) fn from_height_source<V>(
|
||||
name: &str,
|
||||
version: Version,
|
||||
source: V,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Self
|
||||
where
|
||||
V: TypedVec<I = Height, T = R> + ReadableVec<Height, R> + Clone + 'static,
|
||||
{
|
||||
let ppm = LazyPerBlock::from_height_source::<Identity<R>, _>(
|
||||
&format!("{name}_{}", R::SUFFIX),
|
||||
version,
|
||||
source,
|
||||
indexes,
|
||||
);
|
||||
let ratio = LazyPerBlock::from_lazy::<R::ToRatio, R>(name, version, &ppm);
|
||||
|
||||
Self { ppm, ratio }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
mod base;
|
||||
mod lazy;
|
||||
mod price;
|
||||
mod windows;
|
||||
|
||||
pub use base::*;
|
||||
pub use lazy::*;
|
||||
pub use price::*;
|
||||
pub use windows::*;
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Lengths;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Cents, Dollars, Height, PartsPerMillion64, SatsFract, StoredF32, Version};
|
||||
use vecdb::{Database, EagerVec, Exit, PcoVec, ReadableVec, Rw, StorageMode, unlikely};
|
||||
use vecdb::{CachedBoxedVec, Database, ReadableCloneableVec, Rw, StorageMode, unlikely};
|
||||
|
||||
use crate::internal::{LazyPerBlock, PerBlock, Price};
|
||||
use crate::{indexes, price};
|
||||
|
||||
use super::RatioPerBlock;
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{LazyIndexedVec, LazyPerBlock, LazyRatioPerBlock, PerBlock, Price},
|
||||
};
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct PriceWithRatioPerBlock<M: StorageMode = Rw> {
|
||||
pub usd: LazyPerBlock<Dollars, Cents>,
|
||||
pub cents: PerBlock<Cents, M>,
|
||||
pub sats: LazyPerBlock<SatsFract, Dollars>,
|
||||
pub ppm: PerBlock<PartsPerMillion64, M>,
|
||||
pub ppm: LazyPerBlock<PartsPerMillion64>,
|
||||
pub ratio: LazyPerBlock<StoredF32, PartsPerMillion64>,
|
||||
}
|
||||
|
||||
@@ -24,9 +23,23 @@ impl PriceWithRatioPerBlock {
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let price = Price::forced_import(db, name, version, indexes)?;
|
||||
let ratio = RatioPerBlock::forced_import(db, name, version + Version::ONE, indexes)?;
|
||||
let ratio_version = version + Version::new(4);
|
||||
let ppm_source = LazyIndexedVec::new(
|
||||
&format!("{name}_ratio_ppm_source"),
|
||||
ratio_version,
|
||||
price.cents.height.read_only_boxed_clone(),
|
||||
spot_price.clone(),
|
||||
|_, price, spot| price_ratio(spot, price),
|
||||
);
|
||||
let ratio = LazyRatioPerBlock::from_height_source(
|
||||
&format!("{name}_ratio"),
|
||||
ratio_version,
|
||||
ppm_source,
|
||||
indexes,
|
||||
);
|
||||
Ok(Self {
|
||||
usd: price.usd,
|
||||
cents: price.cents,
|
||||
@@ -35,38 +48,6 @@ impl PriceWithRatioPerBlock {
|
||||
ratio: ratio.ratio,
|
||||
})
|
||||
}
|
||||
|
||||
/// Compute ratio from close price and this metric's price.
|
||||
pub(crate) fn compute_ratio(
|
||||
&mut self,
|
||||
starting_lengths: &Lengths,
|
||||
close_price: &impl ReadableVec<Height, Cents>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.ppm.height.compute_transform2(
|
||||
starting_lengths.height,
|
||||
close_price,
|
||||
&self.cents.height,
|
||||
|(i, close, price, ..)| (i, price_ratio(close, price)),
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute price via closure (in cents), then compute ratio.
|
||||
pub(crate) fn compute_all<F>(
|
||||
&mut self,
|
||||
prices: &price::Vecs,
|
||||
starting_lengths: &Lengths,
|
||||
exit: &Exit,
|
||||
mut compute_price: F,
|
||||
) -> Result<()>
|
||||
where
|
||||
F: FnMut(&mut EagerVec<PcoVec<Height, Cents>>) -> Result<()>,
|
||||
{
|
||||
compute_price(&mut self.cents.height)?;
|
||||
self.compute_ratio(starting_lengths, &prices.spot.cents.height, exit)
|
||||
}
|
||||
}
|
||||
|
||||
fn price_ratio(close: Cents, price: Cents) -> PartsPerMillion64 {
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{BinaryTransform, Database, Exit, ReadableVec, Rw, StorageMode, VecValue};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{FixedRatio, RatioPerBlock, Windows},
|
||||
};
|
||||
|
||||
/// Four rolling-window ratios with fixed-point storage and lazy float views.
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
#[traversable(transparent)]
|
||||
pub struct RatioRollingWindows<B: FixedRatio, M: StorageMode = Rw>(
|
||||
pub Windows<RatioPerBlock<B, M>>,
|
||||
);
|
||||
|
||||
impl<B: FixedRatio> RatioRollingWindows<B> {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(Windows::try_from_fn(|suffix| {
|
||||
RatioPerBlock::forced_import_ppm(db, &format!("{name}_{suffix}"), version, indexes)
|
||||
})?))
|
||||
}
|
||||
|
||||
pub(crate) fn compute_binary<S1T, S2T, F, R1, R2>(
|
||||
&mut self,
|
||||
max_from: Height,
|
||||
sources1: [&R1; 4],
|
||||
sources2: [&R2; 4],
|
||||
exit: &Exit,
|
||||
) -> Result<()>
|
||||
where
|
||||
S1T: VecValue,
|
||||
S2T: VecValue,
|
||||
R1: ReadableVec<Height, S1T>,
|
||||
R2: ReadableVec<Height, S2T>,
|
||||
F: BinaryTransform<S1T, S2T, B>,
|
||||
{
|
||||
for ((target, s1), s2) in self
|
||||
.0
|
||||
.as_mut_array()
|
||||
.into_iter()
|
||||
.zip(sources1)
|
||||
.zip(sources2)
|
||||
{
|
||||
target
|
||||
.ppm
|
||||
.compute_binary::<S1T, S2T, F>(max_from, s1, s2, exit)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Bitcoin, Cents, Dollars, Height, Sats, SatsSigned, Version};
|
||||
use brk_types::{Bitcoin, Cents, Dollars, Sats, SatsSigned, Version};
|
||||
use schemars::JsonSchema;
|
||||
use vecdb::{Database, Exit, ReadableCloneableVec, Rw, StorageMode, UnaryTransform};
|
||||
use vecdb::{Database, ReadableCloneableVec, Rw, StorageMode, UnaryTransform};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
CentsUnsignedToDollars, LazyPerBlock, NumericValue, PerBlock, SatsSignedToBitcoin,
|
||||
SatsToBitcoin, SatsToCents,
|
||||
SatsToBitcoin,
|
||||
},
|
||||
price,
|
||||
};
|
||||
|
||||
/// Trait that associates a sats type with its transform to Bitcoin.
|
||||
@@ -66,19 +65,4 @@ impl ValuePerBlock {
|
||||
cents,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn compute(
|
||||
&mut self,
|
||||
prices: &price::Vecs,
|
||||
max_from: Height,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.cents.compute_binary::<Sats, Cents, SatsToCents>(
|
||||
max_from,
|
||||
&self.sats.height,
|
||||
&prices.spot.cents.height,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Lazy value wrapper for ValuePerBlock - all transforms are lazy.
|
||||
//! Lazy value wrapper for point-in-time value sources.
|
||||
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Bitcoin, Cents, Dollars, Height, Sats, Version};
|
||||
@@ -6,10 +6,10 @@ use derive_more::{Deref, DerefMut};
|
||||
use vecdb::UnaryTransform;
|
||||
|
||||
use crate::internal::{
|
||||
Identity, LazyValue, LazyValueDerivedResolutions, SatsToBitcoin, ValuePerBlock,
|
||||
Identity, LazyValue, LazyValueDerivedResolutions, SatsToBitcoin, SpotValuePerBlock,
|
||||
};
|
||||
|
||||
/// Lazy value wrapper with height + all derived last transforms from ValuePerBlock.
|
||||
/// Lazy value wrapper with height + all derived last transforms.
|
||||
#[derive(Clone, Deref, DerefMut, Traversable)]
|
||||
#[traversable(merge)]
|
||||
pub struct LazyValuePerBlock {
|
||||
@@ -22,14 +22,23 @@ pub struct LazyValuePerBlock {
|
||||
}
|
||||
|
||||
impl LazyValuePerBlock {
|
||||
pub(crate) fn from_block_source<
|
||||
pub(crate) fn spot_identity(name: &str, source: &SpotValuePerBlock, version: Version) -> Self {
|
||||
Self::from_spot_block_source::<
|
||||
Identity<Sats>,
|
||||
SatsToBitcoin,
|
||||
Identity<Cents>,
|
||||
Identity<Dollars>,
|
||||
>(name, source, version)
|
||||
}
|
||||
|
||||
pub(crate) fn from_spot_block_source<
|
||||
SatsTransform,
|
||||
BitcoinTransform,
|
||||
CentsTransform,
|
||||
DollarsTransform,
|
||||
>(
|
||||
name: &str,
|
||||
source: &ValuePerBlock,
|
||||
source: &SpotValuePerBlock,
|
||||
version: Version,
|
||||
) -> Self
|
||||
where
|
||||
@@ -38,14 +47,14 @@ impl LazyValuePerBlock {
|
||||
CentsTransform: UnaryTransform<Cents, Cents>,
|
||||
DollarsTransform: UnaryTransform<Dollars, Dollars>,
|
||||
{
|
||||
let height = LazyValue::from_block_source::<
|
||||
let height = LazyValue::from_spot_block_source::<
|
||||
SatsTransform,
|
||||
BitcoinTransform,
|
||||
CentsTransform,
|
||||
DollarsTransform,
|
||||
>(name, source, version);
|
||||
|
||||
let resolutions = LazyValueDerivedResolutions::from_block_source::<
|
||||
let resolutions = LazyValueDerivedResolutions::from_spot_block_source::<
|
||||
SatsTransform,
|
||||
BitcoinTransform,
|
||||
CentsTransform,
|
||||
@@ -57,10 +66,4 @@ impl LazyValuePerBlock {
|
||||
resolutions: Box::new(resolutions),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn identity(name: &str, source: &ValuePerBlock, version: Version) -> Self {
|
||||
Self::from_block_source::<Identity<Sats>, SatsToBitcoin, Identity<Cents>, Identity<Dollars>>(
|
||||
name, source, version,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use brk_traversable::Traversable;
|
||||
use brk_types::{Bitcoin, Cents, Dollars, Sats, Version};
|
||||
use vecdb::UnaryTransform;
|
||||
|
||||
use crate::internal::{DerivedResolutions, ValuePerBlock};
|
||||
use crate::internal::{DerivedResolutions, SpotValuePerBlock};
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct LazyValueDerivedResolutions {
|
||||
@@ -13,14 +13,14 @@ pub struct LazyValueDerivedResolutions {
|
||||
}
|
||||
|
||||
impl LazyValueDerivedResolutions {
|
||||
pub(crate) fn from_block_source<
|
||||
pub(crate) fn from_spot_block_source<
|
||||
SatsTransform,
|
||||
BitcoinTransform,
|
||||
CentsTransform,
|
||||
DollarsTransform,
|
||||
>(
|
||||
name: &str,
|
||||
source: &ValuePerBlock,
|
||||
source: &SpotValuePerBlock,
|
||||
version: Version,
|
||||
) -> Self
|
||||
where
|
||||
@@ -41,7 +41,7 @@ impl LazyValueDerivedResolutions {
|
||||
&source.sats.resolutions,
|
||||
);
|
||||
|
||||
let cents = DerivedResolutions::from_derived_computed::<CentsTransform>(
|
||||
let cents = DerivedResolutions::from_lazy::<CentsTransform, Cents>(
|
||||
&format!("{name}_cents"),
|
||||
version,
|
||||
&source.cents.resolutions,
|
||||
|
||||
@@ -8,7 +8,8 @@ mod lazy_derived_resolutions;
|
||||
mod lazy_rolling_avg;
|
||||
mod lazy_rolling_sum;
|
||||
mod rolling_distribution;
|
||||
mod with_deltas;
|
||||
mod spot;
|
||||
mod spot_with_deltas;
|
||||
|
||||
pub use base::*;
|
||||
pub use cumulative::*;
|
||||
@@ -20,4 +21,5 @@ pub use lazy_derived_resolutions::*;
|
||||
pub use lazy_rolling_avg::*;
|
||||
pub use lazy_rolling_sum::*;
|
||||
pub use rolling_distribution::*;
|
||||
pub use with_deltas::*;
|
||||
pub use spot::*;
|
||||
pub use spot_with_deltas::*;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Bitcoin, Cents, Dollars, Height, Sats, Version};
|
||||
use vecdb::{BinaryTransform, CachedBoxedVec, Database, ReadableCloneableVec, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
CentsUnsignedToDollars, Identity, LazyIndexedVec, LazyPerBlock, PerBlock, SatsToBitcoin,
|
||||
SatsToCents,
|
||||
},
|
||||
};
|
||||
|
||||
/// A point-in-time value whose fiat amount is derived from sats and same-height spot price.
|
||||
#[derive(Traversable)]
|
||||
pub struct SpotValuePerBlock<M: StorageMode = Rw> {
|
||||
pub btc: LazyPerBlock<Bitcoin, Sats>,
|
||||
pub sats: PerBlock<Sats, M>,
|
||||
pub usd: LazyPerBlock<Dollars, Cents>,
|
||||
pub cents: LazyPerBlock<Cents>,
|
||||
}
|
||||
|
||||
impl SpotValuePerBlock {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let sats = PerBlock::forced_import(db, &format!("{name}_sats"), version, indexes)?;
|
||||
|
||||
let btc = LazyPerBlock::from_computed::<SatsToBitcoin>(
|
||||
name,
|
||||
version,
|
||||
sats.height.read_only_boxed_clone(),
|
||||
&sats,
|
||||
);
|
||||
|
||||
let cents_source = LazyIndexedVec::new(
|
||||
&format!("{name}_cents_source"),
|
||||
version,
|
||||
sats.height.read_only_boxed_clone(),
|
||||
spot_price.clone(),
|
||||
|_, sats, spot| SatsToCents::apply(sats, spot),
|
||||
);
|
||||
let cents = LazyPerBlock::from_height_source::<Identity<Cents>, _>(
|
||||
&format!("{name}_cents"),
|
||||
version,
|
||||
cents_source,
|
||||
indexes,
|
||||
);
|
||||
|
||||
let usd = LazyPerBlock::from_lazy::<CentsUnsignedToDollars, Cents>(
|
||||
&format!("{name}_usd"),
|
||||
version,
|
||||
¢s,
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
btc,
|
||||
sats,
|
||||
usd,
|
||||
cents,
|
||||
})
|
||||
}
|
||||
}
|
||||
+8
-7
@@ -1,32 +1,33 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{PartsPerMillionSigned64, Sats, SatsSigned, Version};
|
||||
use brk_types::{Cents, Height, PartsPerMillionSigned64, Sats, SatsSigned, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{Database, Rw, StorageMode};
|
||||
use vecdb::{CachedBoxedVec, Database, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{LazyRollingDeltasAmountFromHeight, ValuePerBlock, WindowStartVec, Windows},
|
||||
internal::{LazyRollingDeltasAmountFromHeight, SpotValuePerBlock, WindowStartVec, Windows},
|
||||
};
|
||||
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct ValuePerBlockWithDeltas<M: StorageMode = Rw> {
|
||||
pub struct SpotValuePerBlockWithDeltas<M: StorageMode = Rw> {
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
#[traversable(flatten)]
|
||||
pub inner: ValuePerBlock<M>,
|
||||
pub inner: SpotValuePerBlock<M>,
|
||||
pub delta: LazyRollingDeltasAmountFromHeight<Sats, SatsSigned, PartsPerMillionSigned64>,
|
||||
}
|
||||
|
||||
impl ValuePerBlockWithDeltas {
|
||||
impl SpotValuePerBlockWithDeltas {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let inner = ValuePerBlock::forced_import(db, name, version, indexes)?;
|
||||
let inner = SpotValuePerBlock::forced_import(db, name, version, indexes, spot_price)?;
|
||||
|
||||
let delta = LazyRollingDeltasAmountFromHeight::new(
|
||||
&format!("{name}_delta"),
|
||||
@@ -2,7 +2,7 @@ use brk_traversable::Traversable;
|
||||
use brk_types::{Bitcoin, Cents, Dollars, Height, Sats, Version};
|
||||
use vecdb::{LazyVecFrom1, ReadableCloneableVec, UnaryTransform, VecIndex};
|
||||
|
||||
use crate::internal::ValuePerBlock;
|
||||
use crate::internal::SpotValuePerBlock;
|
||||
|
||||
/// Fully lazy value type at height level.
|
||||
///
|
||||
@@ -16,14 +16,14 @@ pub struct LazyValue<I: VecIndex> {
|
||||
}
|
||||
|
||||
impl LazyValue<Height> {
|
||||
pub(crate) fn from_block_source<
|
||||
pub(crate) fn from_spot_block_source<
|
||||
SatsTransform,
|
||||
BitcoinTransform,
|
||||
CentsTransform,
|
||||
DollarsTransform,
|
||||
>(
|
||||
name: &str,
|
||||
source: &ValuePerBlock,
|
||||
source: &SpotValuePerBlock,
|
||||
version: Version,
|
||||
) -> Self
|
||||
where
|
||||
|
||||
@@ -6,16 +6,16 @@ use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Lengths;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Sats, Version};
|
||||
use brk_types::{Cents, Height, Sats, Version};
|
||||
use rayon::prelude::*;
|
||||
use schemars::JsonSchema;
|
||||
use vecdb::{AnyStoredVec, AnyVec, Database, EagerVec, Exit, PcoVec, WritableVec};
|
||||
use vecdb::{AnyStoredVec, AnyVec, CachedBoxedVec, Database, EagerVec, Exit, PcoVec, WritableVec};
|
||||
|
||||
use crate::{indexes, price};
|
||||
use crate::indexes;
|
||||
|
||||
use super::{
|
||||
FixedRatio, NumericValue, PerBlock, PerBlockCumulativeRolling, PercentPerBlock, ValuePerBlock,
|
||||
WindowStartVec, Windows,
|
||||
FixedRatio, NumericValue, PerBlock, PerBlockCumulativeRolling, PercentPerBlock,
|
||||
SpotValuePerBlock, WindowStartVec, Windows,
|
||||
};
|
||||
|
||||
use crate::distribution::metrics::AvgAmountMetrics;
|
||||
@@ -158,16 +158,23 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl WithAddrTypes<ValuePerBlock> {
|
||||
impl WithAddrTypes<SpotValuePerBlock> {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let all = ValuePerBlock::forced_import(db, name, version, indexes)?;
|
||||
let all = SpotValuePerBlock::forced_import(db, name, version, indexes, spot_price)?;
|
||||
let by_addr_type = ByAddrType::new_with_name(|type_name| {
|
||||
ValuePerBlock::forced_import(db, &format!("{type_name}_{name}"), version, indexes)
|
||||
SpotValuePerBlock::forced_import(
|
||||
db,
|
||||
&format!("{type_name}_{name}"),
|
||||
version,
|
||||
indexes,
|
||||
spot_price,
|
||||
)
|
||||
})?;
|
||||
Ok(Self { all, by_addr_type })
|
||||
}
|
||||
@@ -193,16 +200,13 @@ impl WithAddrTypes<ValuePerBlock> {
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.all.sats.height.reset()?;
|
||||
self.all.cents.height.reset()?;
|
||||
for v in self.by_addr_type.values_mut() {
|
||||
v.sats.height.reset()?;
|
||||
v.cents.height.reset()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Push the stateful sats value for `all` and each per-type. Cents are
|
||||
/// derived post-hoc from sats × price in [`Self::compute_rest`].
|
||||
/// Push the stateful sats value for `all` and each per-type.
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_height<U>(&mut self, total: U, per_type: impl IntoIterator<Item = U>)
|
||||
where
|
||||
@@ -213,21 +217,6 @@ impl WithAddrTypes<ValuePerBlock> {
|
||||
v.sats.height.push(value.into());
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive cents (and thus lazy btc/usd) for `all` and every per-type vec
|
||||
/// from the stateful sats values × spot price.
|
||||
pub(crate) fn compute_rest(
|
||||
&mut self,
|
||||
max_from: Height,
|
||||
prices: &price::Vecs,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.all.compute(prices, max_from, exit)?;
|
||||
for v in self.by_addr_type.values_mut() {
|
||||
v.compute(prices, max_from, exit)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl WithAddrTypes<AvgAmountMetrics> {
|
||||
@@ -235,10 +224,11 @@ impl WithAddrTypes<AvgAmountMetrics> {
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let all = AvgAmountMetrics::forced_import(db, "", version, indexes)?;
|
||||
let all = AvgAmountMetrics::forced_import(db, "", version, indexes, spot_price)?;
|
||||
let by_addr_type = ByAddrType::new_with_name(|type_name| {
|
||||
AvgAmountMetrics::forced_import(db, type_name, version, indexes)
|
||||
AvgAmountMetrics::forced_import(db, type_name, version, indexes, spot_price)
|
||||
})?;
|
||||
Ok(Self { all, by_addr_type })
|
||||
}
|
||||
|
||||
@@ -73,10 +73,7 @@ impl<T> ByDcaClass<T> {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn try_from_class<U, F, E>(
|
||||
class: &ByDcaClass<U>,
|
||||
mut create: F,
|
||||
) -> Result<Self, E>
|
||||
pub(crate) fn try_from_class<U, F, E>(class: &ByDcaClass<U>, mut create: F) -> Result<Self, E>
|
||||
where
|
||||
F: FnMut(&'static str, u16, Day1, &U) -> Result<T, E>,
|
||||
{
|
||||
@@ -161,41 +158,4 @@ impl<T> ByDcaClass<T> {
|
||||
pub(crate) fn day1(year: u16) -> Day1 {
|
||||
Day1::try_from(Date::new(year, 1, 1)).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
[
|
||||
&self.from_2015,
|
||||
&self.from_2016,
|
||||
&self.from_2017,
|
||||
&self.from_2018,
|
||||
&self.from_2019,
|
||||
&self.from_2020,
|
||||
&self.from_2021,
|
||||
&self.from_2022,
|
||||
&self.from_2023,
|
||||
&self.from_2024,
|
||||
&self.from_2025,
|
||||
&self.from_2026,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub(crate) fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
[
|
||||
&mut self.from_2015,
|
||||
&mut self.from_2016,
|
||||
&mut self.from_2017,
|
||||
&mut self.from_2018,
|
||||
&mut self.from_2019,
|
||||
&mut self.from_2020,
|
||||
&mut self.from_2021,
|
||||
&mut self.from_2022,
|
||||
&mut self.from_2023,
|
||||
&mut self.from_2024,
|
||||
&mut self.from_2025,
|
||||
&mut self.from_2026,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -120,44 +120,6 @@ impl<T> ByDcaPeriod<T> {
|
||||
_10y: create(n._10y, d._10y, &period._10y)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
[
|
||||
&mut self._1w,
|
||||
&mut self._1m,
|
||||
&mut self._3m,
|
||||
&mut self._6m,
|
||||
&mut self._1y,
|
||||
&mut self._2y,
|
||||
&mut self._3y,
|
||||
&mut self._4y,
|
||||
&mut self._5y,
|
||||
&mut self._6y,
|
||||
&mut self._8y,
|
||||
&mut self._10y,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub(crate) fn iter_with_days(&self) -> impl Iterator<Item = (&T, u32)> {
|
||||
let d = DCA_PERIOD_DAYS;
|
||||
[
|
||||
(&self._1w, d._1w),
|
||||
(&self._1m, d._1m),
|
||||
(&self._3m, d._3m),
|
||||
(&self._6m, d._6m),
|
||||
(&self._1y, d._1y),
|
||||
(&self._2y, d._2y),
|
||||
(&self._3y, d._3y),
|
||||
(&self._4y, d._4y),
|
||||
(&self._5y, d._5y),
|
||||
(&self._6y, d._6y),
|
||||
(&self._8y, d._8y),
|
||||
(&self._10y, d._10y),
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl<T> ByDcaPeriod<&T> {
|
||||
@@ -193,10 +155,7 @@ pub struct ByDcaCagr<T> {
|
||||
}
|
||||
|
||||
impl<T> ByDcaCagr<T> {
|
||||
pub(crate) fn try_new<U, F, E>(
|
||||
period: &ByDcaPeriod<U>,
|
||||
mut create: F,
|
||||
) -> Result<Self, E>
|
||||
pub(crate) fn try_new<U, F, E>(period: &ByDcaPeriod<U>, mut create: F) -> Result<Self, E>
|
||||
where
|
||||
F: FnMut(&'static str, u32, &U) -> Result<T, E>,
|
||||
{
|
||||
@@ -212,5 +171,4 @@ impl<T> ByDcaCagr<T> {
|
||||
_10y: create(n._10y, d._10y, &period._10y)?,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_types::{Bitcoin, Cents, PartsPerMillionSigned64, Sats};
|
||||
use brk_types::{Bitcoin, Sats};
|
||||
use vecdb::{AnyVec, Exit, ReadableOptionVec, ReadableVec, VecIndex};
|
||||
|
||||
use super::vecs::DcaStack;
|
||||
use super::{DCA_AMOUNT, Vecs};
|
||||
use crate::{indexes, internal::{RatioDiffCents, SatsToCents}, price};
|
||||
use crate::{indexes, price};
|
||||
|
||||
impl Vecs {
|
||||
pub(crate) fn compute(
|
||||
@@ -21,10 +20,7 @@ impl Vecs {
|
||||
let h2d = &indexes.height.day1;
|
||||
let close = &prices.split.close.usd.day1;
|
||||
|
||||
let start = self
|
||||
.sats_cumulative
|
||||
.len()
|
||||
.min(starting_height.to_usize());
|
||||
let start = self.sats_cumulative.len().min(starting_height.to_usize());
|
||||
let mut cumulative = start
|
||||
.checked_sub(1)
|
||||
.and_then(|height| self.sats_cumulative.collect_one_at(height))
|
||||
@@ -49,41 +45,6 @@ impl Vecs {
|
||||
exit,
|
||||
)?;
|
||||
|
||||
for stack in self.period.dca_stack.iter_mut() {
|
||||
stack.compute_cents(prices, starting_height, exit)?;
|
||||
}
|
||||
for stack in self.class.dca_stack.iter_mut() {
|
||||
stack.compute_cents(prices, starting_height, exit)?;
|
||||
}
|
||||
|
||||
for (returns, (cost_basis, _)) in self
|
||||
.period
|
||||
.dca_return
|
||||
.iter_mut()
|
||||
.zip(self.period.dca_cost_basis.iter_with_days())
|
||||
{
|
||||
returns.compute_binary::<Cents, Cents, RatioDiffCents<PartsPerMillionSigned64>>(
|
||||
starting_height,
|
||||
&prices.spot.cents.height,
|
||||
&cost_basis.cents.height,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
|
||||
for (returns, cost_basis) in self
|
||||
.class
|
||||
.dca_return
|
||||
.iter_mut()
|
||||
.zip(self.class.dca_cost_basis.iter())
|
||||
{
|
||||
returns.compute_binary::<Cents, Cents, RatioDiffCents<PartsPerMillionSigned64>>(
|
||||
starting_height,
|
||||
&prices.spot.cents.height,
|
||||
&cost_basis.cents.height,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
|
||||
let exit = exit.clone();
|
||||
self.db.run_bg(move |db| {
|
||||
let _lock = exit.lock();
|
||||
@@ -93,22 +54,6 @@ impl Vecs {
|
||||
}
|
||||
}
|
||||
|
||||
impl DcaStack {
|
||||
fn compute_cents(
|
||||
&mut self,
|
||||
prices: &price::Vecs,
|
||||
starting_height: brk_types::Height,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.cents.compute_binary::<Sats, Cents, SatsToCents>(
|
||||
starting_height,
|
||||
&self.sats.height,
|
||||
&prices.spot.cents.height,
|
||||
exit,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn sats_from_dca(price: brk_types::Dollars) -> Sats {
|
||||
if price == brk_types::Dollars::ZERO {
|
||||
Sats::ZERO
|
||||
|
||||
@@ -15,8 +15,8 @@ use crate::{
|
||||
blocks, indexes,
|
||||
internal::{
|
||||
CentsUnsignedToDollars, Identity, LazyIndexedVec, LazyPerBlock, LazyPercentPerBlock,
|
||||
LazyPreviousDeltaVec, LazySinceDayVec, LazyWindowVec, PerBlock, PercentPerBlock, Price,
|
||||
RatioDiffCents, SatsToBitcoin, SatsToCents,
|
||||
LazyPreviousDeltaVec, LazySinceDayVec, LazyWindowVec, Price, RatioDiffCents, SatsToBitcoin,
|
||||
SatsToCents,
|
||||
db_utils::{finalize_db, open_db},
|
||||
},
|
||||
price,
|
||||
@@ -50,39 +50,60 @@ impl Vecs {
|
||||
)
|
||||
})?;
|
||||
let cached_days = indexes.height.day1.read_only_cached_boxed_clone();
|
||||
let spot_price = prices.spot.cents.height.read_only_cached_boxed_clone();
|
||||
|
||||
let dca_stack = ByDcaPeriod::try_from_period(&cached_starts, |name, _days, window_starts| {
|
||||
let metric_name = format!("dca_stack_{name}");
|
||||
let source = LazyWindowVec::<Height, Sats, Sats>::new(
|
||||
&format!("{metric_name}_sats_source"),
|
||||
version,
|
||||
sats_cumulative.read_only_boxed_clone(),
|
||||
window_starts.clone(),
|
||||
true,
|
||||
|current, before, _| current.checked_sub(before).unwrap_or_default(),
|
||||
);
|
||||
dca_stack_from_source(&db, &metric_name, version, indexes, source)
|
||||
})?;
|
||||
let dca_stack =
|
||||
ByDcaPeriod::try_from_period(&cached_starts, |name, _days, window_starts| {
|
||||
let metric_name = format!("dca_stack_{name}");
|
||||
let source = LazyWindowVec::<Height, Sats, Sats>::new(
|
||||
&format!("{metric_name}_sats_source"),
|
||||
version,
|
||||
sats_cumulative.read_only_boxed_clone(),
|
||||
window_starts.clone(),
|
||||
true,
|
||||
|current, before, _| current.checked_sub(before).unwrap_or_default(),
|
||||
);
|
||||
dca_stack_from_source(&metric_name, version, indexes, source, &spot_price)
|
||||
})?;
|
||||
|
||||
let first_price_day = Day1::try_from(Date::new(2010, 7, 12)).unwrap();
|
||||
let dca_cost_basis =
|
||||
ByDcaPeriod::try_from_period(&dca_stack, |name, days, stack| {
|
||||
let metric_name = format!("dca_cost_basis_{name}");
|
||||
let dca_cost_basis = ByDcaPeriod::try_from_period(&dca_stack, |name, days, stack| {
|
||||
let metric_name = format!("dca_cost_basis_{name}");
|
||||
let source = LazyIndexedVec::new(
|
||||
&format!("{metric_name}_cents_source"),
|
||||
version,
|
||||
stack.sats.height.read_only_boxed_clone(),
|
||||
cached_days.clone(),
|
||||
move |_, stack_sats, day| {
|
||||
if day <= first_price_day {
|
||||
return Cents::ZERO;
|
||||
}
|
||||
let num_days =
|
||||
(days as usize).min(day.to_usize() + 1 - first_price_day.to_usize());
|
||||
Cents::from(DCA_AMOUNT * num_days / Bitcoin::from(stack_sats))
|
||||
},
|
||||
);
|
||||
Ok::<_, Error>(Price::from_height_source(
|
||||
&metric_name,
|
||||
version,
|
||||
source,
|
||||
indexes,
|
||||
))
|
||||
})?;
|
||||
|
||||
let dca_return =
|
||||
ByDcaPeriod::try_from_period(&dca_cost_basis, |name, _days, cost_basis| {
|
||||
let metric_name = format!("dca_return_{name}");
|
||||
let source = LazyIndexedVec::new(
|
||||
&format!("{metric_name}_cents_source"),
|
||||
&format!("{metric_name}_ppm_source"),
|
||||
version,
|
||||
stack.sats.height.read_only_boxed_clone(),
|
||||
cached_days.clone(),
|
||||
move |_, stack_sats, day| {
|
||||
if day <= first_price_day {
|
||||
return Cents::ZERO;
|
||||
}
|
||||
let num_days =
|
||||
(days as usize).min(day.to_usize() + 1 - first_price_day.to_usize());
|
||||
Cents::from(DCA_AMOUNT * num_days / Bitcoin::from(stack_sats))
|
||||
cost_basis.cents.height.read_only_boxed_clone(),
|
||||
spot_price.clone(),
|
||||
|_, cost_basis, spot| {
|
||||
RatioDiffCents::<PartsPerMillionSigned64>::apply(spot, cost_basis)
|
||||
},
|
||||
);
|
||||
Ok::<_, Error>(Price::from_height_source(
|
||||
Ok::<_, Error>(LazyPercentPerBlock::from_height_source(
|
||||
&metric_name,
|
||||
version,
|
||||
source,
|
||||
@@ -90,57 +111,47 @@ impl Vecs {
|
||||
))
|
||||
})?;
|
||||
|
||||
let dca_return = ByDcaPeriod::try_new(|name, _days| {
|
||||
PercentPerBlock::forced_import(
|
||||
&db,
|
||||
&format!("dca_return_{name}"),
|
||||
let dca_cagr = ByDcaCagr::try_new(&dca_return, |name, days, source| {
|
||||
Ok::<_, Error>(LazyPercentPerBlock::from_lazy_cagr(
|
||||
&format!("dca_cagr_{name}"),
|
||||
version,
|
||||
indexes,
|
||||
)
|
||||
(days / 365) as u8,
|
||||
source,
|
||||
))
|
||||
})?;
|
||||
|
||||
let dca_cagr =
|
||||
ByDcaCagr::try_new(&dca_return, |name, days, source| {
|
||||
Ok::<_, Error>(LazyPercentPerBlock::from_cagr(
|
||||
&format!("dca_cagr_{name}"),
|
||||
version,
|
||||
(days / 365) as u8,
|
||||
source,
|
||||
))
|
||||
})?;
|
||||
|
||||
let lump_sum_stack =
|
||||
ByDcaPeriod::try_from_period(&cached_starts, |name, days, window_starts| {
|
||||
lump_sum_stack(
|
||||
&format!("lump_sum_stack_{name}"),
|
||||
days,
|
||||
version,
|
||||
indexes,
|
||||
window_starts,
|
||||
prices,
|
||||
)
|
||||
})?;
|
||||
lump_sum_stack(
|
||||
&format!("lump_sum_stack_{name}"),
|
||||
days,
|
||||
version,
|
||||
indexes,
|
||||
window_starts,
|
||||
prices,
|
||||
)
|
||||
})?;
|
||||
|
||||
let lump_sum_return =
|
||||
ByDcaPeriod::try_from_period(&cached_starts, |name, _days, window_starts| {
|
||||
let metric_name = format!("lump_sum_return_{name}");
|
||||
let source = LazyWindowVec::<Height, Cents, PartsPerMillionSigned64>::new(
|
||||
&format!("{metric_name}_ppm_source"),
|
||||
version,
|
||||
prices.spot.cents.height.read_only_boxed_clone(),
|
||||
window_starts.clone(),
|
||||
false,
|
||||
|current, past, _| {
|
||||
RatioDiffCents::<PartsPerMillionSigned64>::apply(current, past)
|
||||
},
|
||||
);
|
||||
Ok::<_, Error>(LazyPercentPerBlock::from_height_source(
|
||||
&metric_name,
|
||||
version,
|
||||
source,
|
||||
indexes,
|
||||
))
|
||||
})?;
|
||||
let metric_name = format!("lump_sum_return_{name}");
|
||||
let source = LazyWindowVec::<Height, Cents, PartsPerMillionSigned64>::new(
|
||||
&format!("{metric_name}_ppm_source"),
|
||||
version,
|
||||
prices.spot.cents.height.read_only_boxed_clone(),
|
||||
window_starts.clone(),
|
||||
false,
|
||||
|current, past, _| {
|
||||
RatioDiffCents::<PartsPerMillionSigned64>::apply(current, past)
|
||||
},
|
||||
);
|
||||
Ok::<_, Error>(LazyPercentPerBlock::from_height_source(
|
||||
&metric_name,
|
||||
version,
|
||||
source,
|
||||
indexes,
|
||||
))
|
||||
})?;
|
||||
|
||||
let class_stack = ByDcaClass::try_new(|name, _year, day| {
|
||||
let metric_name = format!("dca_stack_{name}");
|
||||
@@ -152,7 +163,7 @@ impl Vecs {
|
||||
day,
|
||||
|current, before| current.checked_sub(before).unwrap_or_default(),
|
||||
);
|
||||
dca_stack_from_source(&db, &metric_name, version, indexes, source)
|
||||
dca_stack_from_source(&metric_name, version, indexes, source, &spot_price)
|
||||
})?;
|
||||
|
||||
let class_cost_basis =
|
||||
@@ -179,14 +190,25 @@ impl Vecs {
|
||||
))
|
||||
})?;
|
||||
|
||||
let class_return = ByDcaClass::try_new(|name, _year, _day| {
|
||||
PercentPerBlock::forced_import(
|
||||
&db,
|
||||
&format!("dca_return_{name}"),
|
||||
version,
|
||||
indexes,
|
||||
)
|
||||
})?;
|
||||
let class_return =
|
||||
ByDcaClass::try_from_class(&class_cost_basis, |name, _year, _from, cost_basis| {
|
||||
let metric_name = format!("dca_return_{name}");
|
||||
let source = LazyIndexedVec::new(
|
||||
&format!("{metric_name}_ppm_source"),
|
||||
version,
|
||||
cost_basis.cents.height.read_only_boxed_clone(),
|
||||
spot_price.clone(),
|
||||
|_, cost_basis, spot| {
|
||||
RatioDiffCents::<PartsPerMillionSigned64>::apply(spot, cost_basis)
|
||||
},
|
||||
);
|
||||
Ok::<_, Error>(LazyPercentPerBlock::from_height_source(
|
||||
&metric_name,
|
||||
version,
|
||||
source,
|
||||
indexes,
|
||||
))
|
||||
})?;
|
||||
|
||||
let this = Self {
|
||||
sats_per_day,
|
||||
@@ -212,23 +234,38 @@ impl Vecs {
|
||||
}
|
||||
|
||||
fn dca_stack_from_source<V>(
|
||||
db: &vecdb::Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
source: V,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<DcaStack>
|
||||
where
|
||||
V: TypedVec<I = Height, T = Sats> + ReadableVec<Height, Sats> + Clone + 'static,
|
||||
{
|
||||
let sats =
|
||||
LazyPerBlock::from_height_source::<Identity<Sats>, _>(&format!("{name}_sats"), version, source, indexes);
|
||||
let sats = LazyPerBlock::from_height_source::<Identity<Sats>, _>(
|
||||
&format!("{name}_sats"),
|
||||
version,
|
||||
source,
|
||||
indexes,
|
||||
);
|
||||
let btc = LazyPerBlock::from_lazy::<SatsToBitcoin, Sats>(name, version, &sats);
|
||||
let cents = PerBlock::forced_import(db, &format!("{name}_cents"), version, indexes)?;
|
||||
let usd = LazyPerBlock::from_computed::<CentsUnsignedToDollars>(
|
||||
let cents_source = LazyIndexedVec::new(
|
||||
&format!("{name}_cents_source"),
|
||||
version,
|
||||
sats.height.read_only_boxed_clone(),
|
||||
spot_price.clone(),
|
||||
|_, sats, spot| SatsToCents::apply(sats, spot),
|
||||
);
|
||||
let cents = LazyPerBlock::from_height_source::<Identity<Cents>, _>(
|
||||
&format!("{name}_cents"),
|
||||
version,
|
||||
cents_source,
|
||||
indexes,
|
||||
);
|
||||
let usd = LazyPerBlock::from_lazy::<CentsUnsignedToDollars, Cents>(
|
||||
&format!("{name}_usd"),
|
||||
version,
|
||||
cents.height.read_only_boxed_clone(),
|
||||
¢s,
|
||||
);
|
||||
Ok(DcaStack {
|
||||
@@ -271,9 +308,7 @@ fn lump_sum_stack(
|
||||
prices.spot.cents.height.read_only_boxed_clone(),
|
||||
window_starts.clone(),
|
||||
false,
|
||||
move |current, past, _| {
|
||||
SatsToCents::apply(lump_sum_sats(total_invested, past), current)
|
||||
},
|
||||
move |current, past, _| SatsToCents::apply(lump_sum_sats(total_invested, past), current),
|
||||
);
|
||||
let cents = LazyPerBlock::from_height_source::<Identity<Cents>, _>(
|
||||
&format!("{name}_cents"),
|
||||
@@ -299,8 +334,6 @@ fn lump_sum_sats(total_invested: Dollars, past_price: Cents) -> Sats {
|
||||
if past_price == Cents::ZERO {
|
||||
Sats::ZERO
|
||||
} else {
|
||||
Sats::from(Bitcoin::from(
|
||||
total_invested / Dollars::from(past_price),
|
||||
))
|
||||
Sats::from(Bitcoin::from(total_invested / Dollars::from(past_price)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,14 @@ use brk_types::{Bitcoin, Cents, Dollars, Height, PartsPerMillionSigned64, Sats};
|
||||
use vecdb::{Database, EagerVec, PcoVec, Rw, StorageMode};
|
||||
|
||||
use super::{ByDcaCagr, ByDcaClass, ByDcaPeriod};
|
||||
use crate::internal::{
|
||||
LazyPerBlock, LazyPercentPerBlock, LazyPreviousDeltaVec, PerBlock, PercentPerBlock, Price,
|
||||
};
|
||||
use crate::internal::{LazyPerBlock, LazyPercentPerBlock, LazyPreviousDeltaVec, Price};
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct DcaStack<M: StorageMode = Rw> {
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct DcaStack {
|
||||
pub btc: LazyPerBlock<Bitcoin, Sats>,
|
||||
pub sats: LazyPerBlock<Sats>,
|
||||
pub usd: LazyPerBlock<Dollars, Cents>,
|
||||
pub cents: PerBlock<Cents, M>,
|
||||
pub cents: LazyPerBlock<Cents>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
@@ -23,21 +21,21 @@ pub struct LumpSumStack {
|
||||
pub cents: LazyPerBlock<Cents>,
|
||||
}
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct PeriodVecs<M: StorageMode = Rw> {
|
||||
pub dca_stack: ByDcaPeriod<DcaStack<M>>,
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct PeriodVecs {
|
||||
pub dca_stack: ByDcaPeriod<DcaStack>,
|
||||
pub dca_cost_basis: ByDcaPeriod<Price<LazyPerBlock<Cents>>>,
|
||||
pub dca_return: ByDcaPeriod<PercentPerBlock<PartsPerMillionSigned64, M>>,
|
||||
pub dca_return: ByDcaPeriod<LazyPercentPerBlock<PartsPerMillionSigned64>>,
|
||||
pub dca_cagr: ByDcaCagr<LazyPercentPerBlock<PartsPerMillionSigned64>>,
|
||||
pub lump_sum_stack: ByDcaPeriod<LumpSumStack>,
|
||||
pub lump_sum_return: ByDcaPeriod<LazyPercentPerBlock<PartsPerMillionSigned64>>,
|
||||
}
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct ClassVecs<M: StorageMode = Rw> {
|
||||
pub dca_stack: ByDcaClass<DcaStack<M>>,
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct ClassVecs {
|
||||
pub dca_stack: ByDcaClass<DcaStack>,
|
||||
pub dca_cost_basis: ByDcaClass<Price<LazyPerBlock<Cents>>>,
|
||||
pub dca_return: ByDcaClass<PercentPerBlock<PartsPerMillionSigned64, M>>,
|
||||
pub dca_return: ByDcaClass<LazyPercentPerBlock<PartsPerMillionSigned64>>,
|
||||
}
|
||||
|
||||
#[derive(Traversable)]
|
||||
@@ -47,6 +45,6 @@ pub struct Vecs<M: StorageMode = Rw> {
|
||||
pub sats_per_day: LazyPreviousDeltaVec<Height, Sats>,
|
||||
#[traversable(hidden)]
|
||||
pub sats_cumulative: M::Stored<EagerVec<PcoVec<Height, Sats>>>,
|
||||
pub period: PeriodVecs<M>,
|
||||
pub class: ClassVecs<M>,
|
||||
pub period: PeriodVecs,
|
||||
pub class: ClassVecs,
|
||||
}
|
||||
|
||||
@@ -118,12 +118,21 @@ impl Computer {
|
||||
})?;
|
||||
|
||||
let mining_handle = big_thread().spawn_scoped(s, || -> Result<_> {
|
||||
Ok(Box::new(mining::Vecs::forced_import(
|
||||
let mining = Box::new(mining::Vecs::forced_import(
|
||||
&computed_path,
|
||||
VERSION,
|
||||
&indexes,
|
||||
&cached_starts,
|
||||
)?))
|
||||
)?);
|
||||
let cointime = Box::new(cointime::Vecs::forced_import(
|
||||
&computed_path,
|
||||
VERSION,
|
||||
&indexes,
|
||||
&cached_starts,
|
||||
&price,
|
||||
&mining.rewards.subsidy.cumulative.cents,
|
||||
)?);
|
||||
Ok((mining, cointime))
|
||||
})?;
|
||||
|
||||
let transactions_handle = big_thread().spawn_scoped(s, || -> Result<_> {
|
||||
@@ -145,17 +154,11 @@ impl Computer {
|
||||
)?))
|
||||
})?;
|
||||
|
||||
let cointime = Box::new(cointime::Vecs::forced_import(
|
||||
&computed_path,
|
||||
VERSION,
|
||||
&indexes,
|
||||
&cached_starts,
|
||||
)?);
|
||||
|
||||
let coinflow = Box::new(coinflow::Vecs::forced_import(
|
||||
&computed_path,
|
||||
VERSION,
|
||||
&indexes,
|
||||
&price,
|
||||
)?);
|
||||
|
||||
let op_return_handle = big_thread().spawn_scoped(s, || -> Result<_> {
|
||||
@@ -169,7 +172,7 @@ impl Computer {
|
||||
|
||||
let inputs = inputs_handle.join().unwrap()?;
|
||||
let outputs = outputs_handle.join().unwrap()?;
|
||||
let mining = mining_handle.join().unwrap()?;
|
||||
let (mining, cointime) = mining_handle.join().unwrap()?;
|
||||
let transactions = transactions_handle.join().unwrap()?;
|
||||
let pools = pools_handle.join().unwrap()?;
|
||||
let op_return = op_return_handle.join().unwrap()?;
|
||||
@@ -226,6 +229,7 @@ impl Computer {
|
||||
VERSION,
|
||||
&indexes,
|
||||
&cached_starts,
|
||||
&price,
|
||||
)?);
|
||||
|
||||
let market = market_handle.join().unwrap()?;
|
||||
@@ -433,12 +437,8 @@ impl Computer {
|
||||
|
||||
let investing = scope.spawn(|| {
|
||||
timed("Computed investing", || {
|
||||
self.investing.compute(
|
||||
indexer,
|
||||
&self.indexes,
|
||||
&self.price,
|
||||
exit,
|
||||
)
|
||||
self.investing
|
||||
.compute(indexer, &self.indexes, &self.price, exit)
|
||||
})
|
||||
});
|
||||
|
||||
@@ -494,7 +494,6 @@ impl Computer {
|
||||
&self.indexes,
|
||||
&self.price,
|
||||
&self.blocks,
|
||||
&self.mining,
|
||||
&self.supply,
|
||||
&self.distribution,
|
||||
exit,
|
||||
|
||||
@@ -69,13 +69,6 @@ impl Vecs {
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.drawdown.compute_drawdown(
|
||||
starting_height,
|
||||
&prices.spot.cents.height,
|
||||
&self.high.cents.height,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::Version;
|
||||
use vecdb::{Database, ReadableCloneableVec};
|
||||
use brk_types::{Cents, Height, PartsPerMillionSigned32, Version};
|
||||
use vecdb::{BinaryTransform, CachedBoxedVec, Database, ReadableCloneableVec};
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{DaysToYears, LazyPerBlock, PerBlock, PercentPerBlock, Price},
|
||||
internal::{
|
||||
DaysToYears, LazyIndexedVec, LazyPerBlock, LazyPercentPerBlock, PerBlock, Price,
|
||||
RatioDiffCents,
|
||||
},
|
||||
};
|
||||
|
||||
const VERSION: Version = Version::ONE;
|
||||
@@ -15,6 +18,7 @@ impl Vecs {
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
let v = version + VERSION;
|
||||
|
||||
@@ -39,7 +43,15 @@ impl Vecs {
|
||||
&days_since,
|
||||
);
|
||||
|
||||
let drawdown = PercentPerBlock::forced_import(db, "price_drawdown", v, indexes)?;
|
||||
let drawdown_source = LazyIndexedVec::new(
|
||||
"price_drawdown_ppm_source",
|
||||
v,
|
||||
high.cents.height.read_only_boxed_clone(),
|
||||
spot_price.clone(),
|
||||
|_, high, spot| RatioDiffCents::<PartsPerMillionSigned32>::apply(spot, high),
|
||||
);
|
||||
let drawdown =
|
||||
LazyPercentPerBlock::from_height_source("price_drawdown", v, drawdown_source, indexes);
|
||||
|
||||
Ok(Self {
|
||||
high,
|
||||
|
||||
@@ -2,12 +2,12 @@ use brk_traversable::Traversable;
|
||||
use brk_types::{Cents, PartsPerMillionSigned32, StoredF32};
|
||||
use vecdb::{Rw, StorageMode};
|
||||
|
||||
use crate::internal::{LazyPerBlock, PerBlock, PercentPerBlock, Price};
|
||||
use crate::internal::{LazyPerBlock, LazyPercentPerBlock, PerBlock, Price};
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct Vecs<M: StorageMode = Rw> {
|
||||
pub high: Price<PerBlock<Cents, M>>,
|
||||
pub drawdown: PercentPerBlock<PartsPerMillionSigned32, M>,
|
||||
pub drawdown: LazyPercentPerBlock<PartsPerMillionSigned32>,
|
||||
pub days_since: PerBlock<StoredF32, M>,
|
||||
pub years_since: LazyPerBlock<StoredF32>,
|
||||
pub max_days_between: PerBlock<StoredF32, M>,
|
||||
|
||||
@@ -34,15 +34,9 @@ impl Vecs {
|
||||
// Phase 2: Stored volatility inputs derived from lazy 24h returns.
|
||||
self.returns.compute(indexer, blocks, exit)?;
|
||||
|
||||
// Phase 3: Depends on returns, moving_average
|
||||
self.technical.compute(
|
||||
indexer,
|
||||
&self.returns,
|
||||
prices,
|
||||
blocks,
|
||||
&self.moving_average,
|
||||
exit,
|
||||
)?;
|
||||
// Phase 3: Depends on moving_average
|
||||
self.technical
|
||||
.compute(indexer, prices, blocks, &self.moving_average, exit)?;
|
||||
|
||||
let exit = exit.clone();
|
||||
self.db.run_bg(move |db| {
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::path::Path;
|
||||
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::Version;
|
||||
use vecdb::ReadOnlyClone;
|
||||
|
||||
use crate::{
|
||||
blocks, indexes,
|
||||
@@ -25,7 +26,8 @@ impl Vecs {
|
||||
let db = open_db(parent_path, super::DB_NAME, 250_000)?;
|
||||
let version = parent_version;
|
||||
|
||||
let ath = AthVecs::forced_import(&db, version, indexes)?;
|
||||
let spot_price = prices.spot.cents.height.read_only_cached_boxed_clone();
|
||||
let ath = AthVecs::forced_import(&db, version, indexes, &spot_price)?;
|
||||
let cached_starts = ByLookbackPeriod::try_new(|_, days| {
|
||||
Ok::<_, Error>(
|
||||
blocks
|
||||
@@ -35,12 +37,13 @@ impl Vecs {
|
||||
)
|
||||
})?;
|
||||
let lookback = LookbackVecs::forced_import(version, indexes, &cached_starts, prices)?;
|
||||
let returns =
|
||||
ReturnsVecs::forced_import(&db, version, indexes, &cached_starts, prices)?;
|
||||
let returns = ReturnsVecs::forced_import(&db, version, indexes, &cached_starts, prices)?;
|
||||
let volatility = VolatilityVecs::forced_import(version, &returns)?;
|
||||
let range = RangeVecs::forced_import(&db, version, indexes)?;
|
||||
let moving_average = MovingAverageVecs::forced_import(&db, version, indexes)?;
|
||||
let technical = TechnicalVecs::forced_import(&db, version, indexes)?;
|
||||
let cached_spot_price = prices.spot.cents.height.read_only_clone();
|
||||
let range = RangeVecs::forced_import(&db, version, indexes, &cached_spot_price)?;
|
||||
let moving_average = MovingAverageVecs::forced_import(&db, version, indexes, &spot_price)?;
|
||||
let technical =
|
||||
TechnicalVecs::forced_import(&db, version, indexes, &returns.periods._24h.ratio)?;
|
||||
|
||||
let this = Self {
|
||||
db,
|
||||
|
||||
@@ -101,5 +101,4 @@ impl<T> ByLookbackPeriod<T> {
|
||||
_10y: create(n._10y, d._10y, &period._10y)?,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::{Cents, Height};
|
||||
use brk_types::Version;
|
||||
use brk_types::{Cents, Height};
|
||||
use vecdb::CachedBoxedVec;
|
||||
|
||||
use super::{ByLookbackPeriod, Vecs};
|
||||
use crate::{indexes, internal::{LazyWindowVec, Price}, price};
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{LazyWindowVec, Price},
|
||||
price,
|
||||
};
|
||||
|
||||
impl Vecs {
|
||||
pub(crate) fn forced_import(
|
||||
@@ -13,25 +17,24 @@ impl Vecs {
|
||||
cached_starts: &ByLookbackPeriod<CachedBoxedVec<Height, Height>>,
|
||||
prices: &price::Vecs,
|
||||
) -> Result<Self> {
|
||||
let price_past = ByLookbackPeriod::try_from_period(
|
||||
cached_starts,
|
||||
|name, _days, window_starts| {
|
||||
let metric_name = format!("price_past_{name}");
|
||||
let source = LazyWindowVec::<Height, Cents, Cents>::new(
|
||||
&format!("{metric_name}_cents_source"),
|
||||
version,
|
||||
prices.spot.cents.height.read_only_boxed_clone(),
|
||||
window_starts.clone(),
|
||||
false,
|
||||
|_, past, _| past,
|
||||
);
|
||||
Ok::<_, Error>(Price::from_height_source(
|
||||
&metric_name,
|
||||
version,
|
||||
source,
|
||||
indexes,
|
||||
))
|
||||
})?;
|
||||
let price_past =
|
||||
ByLookbackPeriod::try_from_period(cached_starts, |name, _days, window_starts| {
|
||||
let metric_name = format!("price_past_{name}");
|
||||
let source = LazyWindowVec::<Height, Cents, Cents>::new(
|
||||
&format!("{metric_name}_cents_source"),
|
||||
version,
|
||||
prices.spot.cents.height.read_only_boxed_clone(),
|
||||
window_starts.clone(),
|
||||
false,
|
||||
|_, past, _| past,
|
||||
);
|
||||
Ok::<_, Error>(Price::from_height_source(
|
||||
&metric_name,
|
||||
version,
|
||||
source,
|
||||
indexes,
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(Self { price_past })
|
||||
}
|
||||
|
||||
@@ -35,10 +35,12 @@ impl Vecs {
|
||||
(&mut self.sma._4y, 4 * 365),
|
||||
] {
|
||||
let window_starts = blocks.lookback.start_vec(period);
|
||||
sma.compute_all(prices, &starting_lengths, exit, |v| {
|
||||
v.compute_rolling_average(starting_lengths.height, window_starts, close, exit)?;
|
||||
Ok(())
|
||||
})?;
|
||||
sma.cents.height.compute_rolling_average(
|
||||
starting_lengths.height,
|
||||
window_starts,
|
||||
close,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
|
||||
for (ema, period) in [
|
||||
@@ -60,10 +62,12 @@ impl Vecs {
|
||||
(&mut self.ema._4y, 4 * 365),
|
||||
] {
|
||||
let window_starts = blocks.lookback.start_vec(period);
|
||||
ema.compute_all(prices, &starting_lengths, exit, |v| {
|
||||
v.compute_rolling_ema(starting_lengths.height, window_starts, close, exit)?;
|
||||
Ok(())
|
||||
})?;
|
||||
ema.cents.height.compute_rolling_ema(
|
||||
starting_lengths.height,
|
||||
window_starts,
|
||||
close,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::Version;
|
||||
use vecdb::Database;
|
||||
use brk_types::{Cents, Height, Version};
|
||||
use vecdb::{CachedBoxedVec, Database};
|
||||
|
||||
use super::{
|
||||
Vecs,
|
||||
@@ -16,10 +16,11 @@ impl Vecs {
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
spot_price: &CachedBoxedVec<Height, Cents>,
|
||||
) -> Result<Self> {
|
||||
macro_rules! import {
|
||||
($name:expr) => {
|
||||
PriceWithRatioPerBlock::forced_import(db, $name, version, indexes)?
|
||||
PriceWithRatioPerBlock::forced_import(db, $name, version, indexes, spot_price)?
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user