Files
brk/crates/brk_computer/src/investing/compute.rs
2026-04-10 11:30:29 +02:00

291 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use brk_error::Result;
use brk_types::{BasisPointsSigned32, Bitcoin, Cents, Date, Day1, Dollars, Indexes, Sats};
use vecdb::{AnyVec, Exit, ReadableOptionVec, ReadableVec, VecIndex};
use super::{ByDcaPeriod, Vecs};
use crate::{blocks, indexes, internal::RatioDiffCentsBps32, market, prices};
const DCA_AMOUNT: Dollars = Dollars::mint(100.0);
impl Vecs {
pub(crate) fn compute(
&mut self,
indexes: &indexes::Vecs,
prices: &prices::Vecs,
blocks: &blocks::Vecs,
lookback: &market::lookback::Vecs,
starting_indexes: &Indexes,
exit: &Exit,
) -> Result<()> {
self.db.sync_bg_tasks()?;
let h2d = &indexes.height.day1;
let close = &prices.split.close.usd.day1;
let first_price_di = Day1::try_from(Date::new(2010, 7, 12)).unwrap().to_usize();
// Compute per-height DCA sats contribution once (reused by all periods).
// Value = sats_from_dca(close_price) on day-boundary blocks, Sats::ZERO otherwise.
{
let mut last_di: Option<Day1> = None;
self.sats_per_day.compute_transform(
starting_indexes.height,
h2d,
|(h, di, _)| {
if last_di.is_none() && h.to_usize() > 0 {
last_di = Some(h2d.collect_one_at(h.to_usize() - 1).unwrap());
}
let same_day = last_di.is_some_and(|prev| prev == di);
last_di = Some(di);
if same_day {
(h, Sats::ZERO)
} else {
let s = close
.collect_one_flat(di)
.map(sats_from_dca)
.unwrap_or(Sats::ZERO);
(h, s)
}
},
exit,
)?;
}
// DCA by period - stack (rolling sum via _start vecs)
for (stack, days) in self.period.dca_stack.iter_mut_with_days() {
let window_starts = blocks.lookback.start_vec(days as usize);
stack.sats.height.compute_rolling_sum(
starting_indexes.height,
window_starts,
&self.sats_per_day,
exit,
)?;
}
// DCA by period - stack cents (sats × price)
for stack in self.period.dca_stack.iter_mut() {
stack.compute(prices, starting_indexes.height, exit)?;
}
// DCA by period - average price (derived from stack)
let starting_height = starting_indexes.height.to_usize();
for (average_price, stack, days) in self
.period
.dca_cost_basis
.zip_mut_with_days(&self.period.dca_stack)
{
let days = days as usize;
average_price.cents.height.compute_transform2(
starting_indexes.height,
h2d,
&stack.sats.height,
|(h, di, stack_sats, ..)| {
let di_usize = di.to_usize();
let avg = if di_usize > first_price_di {
let num_days = days.min(di_usize + 1 - first_price_di);
Cents::from(DCA_AMOUNT * num_days / Bitcoin::from(stack_sats))
} else {
Cents::ZERO
};
(h, avg)
},
exit,
)?;
}
// DCA by period - returns (compute from average price)
for (returns, (average_price, _)) in self
.period
.dca_return
.iter_mut()
.zip(self.period.dca_cost_basis.iter_with_days())
{
returns.compute_binary::<Cents, Cents, RatioDiffCentsBps32>(
starting_indexes.height,
&prices.spot.cents.height,
&average_price.cents.height,
exit,
)?;
}
// DCA by period - CAGR (computed from returns at height level)
for (cagr, returns, days) in self
.period
.dca_cagr
.zip_mut_with_period(&self.period.dca_return)
{
let years = days as f64 / 365.0;
cagr.bps.height.compute_transform(
starting_indexes.height,
&returns.bps.height,
|(h, r, ..)| {
let ratio = f64::from(r);
let v = (ratio + 1.0).powf(1.0 / years) - 1.0;
(h, BasisPointsSigned32::from(v))
},
exit,
)?;
}
// Lump sum by period - stack
let lookback_dca = ByDcaPeriod::from_lookback(&lookback.price_past);
for (stack, lookback_price, days) in
self.period.lump_sum_stack.zip_mut_with_days(&lookback_dca)
{
let total_invested = DCA_AMOUNT * days as usize;
stack.sats.height.compute_transform2(
starting_indexes.height,
h2d,
&lookback_price.cents.height,
|(h, _di, lp, ..)| {
let sats = if lp == Cents::ZERO {
Sats::ZERO
} else {
Sats::from(Bitcoin::from(total_invested / Dollars::from(lp)))
};
(h, sats)
},
exit,
)?;
}
// Lump sum by period - stack cents (sats × price)
for stack in self.period.lump_sum_stack.iter_mut() {
stack.compute(prices, starting_indexes.height, exit)?;
}
// Lump sum by period - returns (compute from lookback price)
for (returns, (lookback_price, _)) in self
.period
.lump_sum_return
.iter_mut()
.zip(lookback_dca.iter_with_days())
{
returns.compute_binary::<Cents, Cents, RatioDiffCentsBps32>(
starting_indexes.height,
&prices.spot.cents.height,
&lookback_price.cents.height,
exit,
)?;
}
// DCA by year class - stack (cumulative sum from class start date)
let start_days = super::ByDcaClass::<()>::start_days();
for (stack, day1) in self.class.dca_stack.iter_mut().zip(start_days) {
let mut last_di: Option<Day1> = None;
let cls_start = stack.sats.height.len().min(starting_height);
let mut prev_value = if cls_start > 0 {
stack
.sats
.height
.collect_one_at(cls_start - 1)
.unwrap_or_default()
} else {
Sats::ZERO
};
stack.sats.height.compute_transform(
starting_indexes.height,
h2d,
|(h, di, _)| {
let hi = h.to_usize();
if last_di.is_none() && hi > 0 {
last_di = Some(h2d.collect_one_at(hi - 1).unwrap());
}
if di < day1 {
last_di = Some(di);
prev_value = Sats::ZERO;
return (h, Sats::ZERO);
}
let prev_di = last_di;
last_di = Some(di);
let same_day = prev_di.is_some_and(|prev| prev == di);
let result = if same_day {
prev_value
} else {
let prev = if hi > 0 && prev_di.is_some_and(|pd| pd >= day1) {
prev_value
} else {
Sats::ZERO
};
let s = close
.collect_one_flat(di)
.map(sats_from_dca)
.unwrap_or(Sats::ZERO);
prev + s
};
prev_value = result;
(h, result)
},
exit,
)?;
}
// DCA by year class - stack cents (sats × price)
for stack in self.class.dca_stack.iter_mut() {
stack.compute(prices, starting_indexes.height, exit)?;
}
// DCA by year class - average price (derived from stack)
let start_days = super::ByDcaClass::<()>::start_days();
for ((average_price, stack), from) in self
.class
.dca_cost_basis
.iter_mut()
.zip(self.class.dca_stack.iter())
.zip(start_days)
{
let from_usize = from.to_usize();
average_price.cents.height.compute_transform2(
starting_indexes.height,
h2d,
&stack.sats.height,
|(h, di, stack_sats, ..)| {
let di_usize = di.to_usize();
if di_usize < from_usize {
return (h, Cents::ZERO);
}
let num_days = di_usize + 1 - from_usize;
let avg = Cents::from(DCA_AMOUNT * num_days / Bitcoin::from(stack_sats));
(h, avg)
},
exit,
)?;
}
// DCA by year class - returns (compute from average price)
for (returns, average_price) in self
.class
.dca_return
.iter_mut()
.zip(self.class.dca_cost_basis.iter())
{
returns.compute_binary::<Cents, Cents, RatioDiffCentsBps32>(
starting_indexes.height,
&prices.spot.cents.height,
&average_price.cents.height,
exit,
)?;
}
let exit = exit.clone();
self.db.run_bg(move |db| {
let _lock = exit.lock();
db.compact_deferred_default()
});
Ok(())
}
}
fn sats_from_dca(price: Dollars) -> Sats {
if price == Dollars::ZERO {
Sats::ZERO
} else {
Sats::from(Bitcoin::from(DCA_AMOUNT / price))
}
}