diff --git a/crates/brk_computer/src/models/bedrock/urpd_metric.rs b/crates/brk_computer/src/internal/daily_metric.rs similarity index 86% rename from crates/brk_computer/src/models/bedrock/urpd_metric.rs rename to crates/brk_computer/src/internal/daily_metric.rs index 21f92bc8b..db7d9cd9b 100644 --- a/crates/brk_computer/src/models/bedrock/urpd_metric.rs +++ b/crates/brk_computer/src/internal/daily_metric.rs @@ -7,24 +7,29 @@ use brk_types::{ Month3, Month6, Timestamp, Version, Week1, Year1, Year10, }; use schemars::JsonSchema; +use serde::Serialize; use vecdb::{ - AnyExportableVec, AnyVec, Database, EagerVec, ImportableVec, LazyVecFrom1, PcoVec, - ReadableBoxedVec, ReadableCloneableVec, ReadableVec, Rw, StorageMode, TypedVec, VecIndex, - VecValue, short_type_name, + AnyExportableVec, AnyVec, Database, EagerVec, Formattable, ImportableVec, LazyVecFrom1, + PcoVec, PcoVecValue, ReadableBoxedVec, ReadableCloneableVec, ReadableVec, Rw, StorageMode, + TypedVec, UnaryTransform, VecIndex, VecValue, short_type_name, }; -use crate::{indexes, internal::NumericValue}; +use crate::indexes; type StoredDay = ::Stored>>; type DayMapping = LazyVecFrom1; -type Repeated = UrpdView; -type Last = UrpdView; +type Repeated = DailyView; +type Last = DailyView; + +pub trait DailyValue: VecValue + Formattable + JsonSchema + Serialize {} + +impl DailyValue for T where T: VecValue + Formattable + JsonSchema + Serialize {} pub struct RepeatDay; pub struct LastDay; #[derive(Clone)] -pub(crate) struct UrpdMappings { +pub(crate) struct DailyMappings { height: DayMapping, minute10: DayMapping, minute30: DayMapping, @@ -42,7 +47,7 @@ pub(crate) struct UrpdMappings { epoch: DayMapping, } -impl UrpdMappings { +impl DailyMappings { pub(crate) fn new(indexes: &indexes::Vecs) -> Self { let height = LazyVecFrom1::init( "day1", @@ -87,9 +92,9 @@ fn date_mapping(source: ReadableBoxedVec) -> DayMapping +pub struct DailyViews where - T: NumericValue + JsonSchema, + T: DailyValue, { pub height: Repeated, pub minute10: Repeated, @@ -108,15 +113,15 @@ where pub epoch: Last, } -impl UrpdViews +impl DailyViews where - T: NumericValue + JsonSchema, + T: DailyValue, { - pub(super) fn new( + pub(crate) fn new( name: &str, source: ReadableBoxedVec, version: Version, - mappings: &UrpdMappings, + mappings: &DailyMappings, ) -> Self { Self { height: repeated(name, source.clone(), version, &mappings.height), @@ -140,33 +145,73 @@ where #[derive(Traversable)] #[traversable(merge)] -pub struct UrpdMetric +pub struct DailyMetric where - T: NumericValue + JsonSchema, + T: DailyValue + PcoVecValue, { pub day1: StoredDay, #[traversable(flatten)] - pub views: Box>, + pub views: Box>, } -impl UrpdMetric +impl DailyMetric where - T: NumericValue + JsonSchema, + T: DailyValue + PcoVecValue, { pub(crate) fn forced_import( db: &Database, name: &str, version: Version, - mappings: &UrpdMappings, + mappings: &DailyMappings, ) -> Result { let day1 = EagerVec::forced_import(db, name, version)?; let source = day1.read_only_boxed_clone(); - let views = Box::new(UrpdViews::new(name, source, version, mappings)); + let views = Box::new(DailyViews::new(name, source, version, mappings)); Ok(Self { day1, views }) } } +type LazyDay = LazyVecFrom1; + +#[derive(Clone, Traversable)] +#[traversable(merge)] +pub struct LazyDailyMetric +where + T: DailyValue, + S: VecValue, +{ + pub day1: LazyDay, + #[traversable(flatten)] + pub views: Box>, +} + +impl LazyDailyMetric +where + T: DailyValue, + S: VecValue, +{ + pub(crate) fn from_source( + name: &str, + version: Version, + source: ReadableBoxedVec, + mappings: &DailyMappings, + ) -> Self + where + F: UnaryTransform, + { + let day1 = LazyVecFrom1::transformed::(name, version, source); + let views = Box::new(DailyViews::new( + name, + day1.read_only_boxed_clone(), + version, + mappings, + )); + + Self { day1, views } + } +} + fn repeated( name: &str, source: ReadableBoxedVec, @@ -178,7 +223,7 @@ where T: VecValue, V: ReadableCloneableVec + ?Sized, { - UrpdView::new(name, version, source, mapping.read_only_boxed_clone()) + DailyView::new(name, version, source, mapping.read_only_boxed_clone()) } fn last( @@ -192,7 +237,7 @@ where T: VecValue, V: ReadableCloneableVec + ?Sized, { - UrpdView::new(name, version, source, mapping.read_only_boxed_clone()) + DailyView::new(name, version, source, mapping.read_only_boxed_clone()) } pub trait DayStrategy: Send + Sync + 'static { @@ -220,7 +265,7 @@ impl DayStrategy for LastDay { } } -pub struct UrpdView +pub struct DailyView where I: VecIndex, T: VecValue, @@ -232,7 +277,7 @@ where _phantom: PhantomData S>, } -impl Clone for UrpdView +impl Clone for DailyView where I: VecIndex, T: VecValue, @@ -248,7 +293,7 @@ where } } -impl UrpdView +impl DailyView where I: VecIndex, T: VecValue, @@ -312,7 +357,7 @@ where } } -impl AnyVec for UrpdView +impl AnyVec for DailyView where I: VecIndex, T: VecValue, @@ -347,7 +392,7 @@ where } } -impl TypedVec for UrpdView +impl TypedVec for DailyView where I: VecIndex, T: VecValue, @@ -357,7 +402,7 @@ where type T = Option; } -impl ReadableVec> for UrpdView +impl ReadableVec> for DailyView where I: VecIndex, T: VecValue, @@ -407,10 +452,10 @@ where } } -impl Traversable for UrpdView +impl Traversable for DailyView where I: VecIndex, - T: NumericValue + JsonSchema, + T: DailyValue, S: DayStrategy, { fn to_tree_node(&self) -> TreeNode { @@ -526,7 +571,7 @@ mod tests { .unwrap() .as_nanos(); let path = - std::env::temp_dir().join(format!("brk-urpd-view-{}-{suffix}", std::process::id())); + std::env::temp_dir().join(format!("brk-daily-view-{}-{suffix}", std::process::id())); let db = Database::open(&path).unwrap(); let mut source: EagerVec> = @@ -542,7 +587,7 @@ mod tests { source.write().unwrap(); mapping.write().unwrap(); - let view = UrpdView::::new( + let view = DailyView::::new( "test", Version::ONE, source.read_only_boxed_clone(), diff --git a/crates/brk_computer/src/internal/mod.rs b/crates/brk_computer/src/internal/mod.rs index bb429f1ae..c1ef25f73 100644 --- a/crates/brk_computer/src/internal/mod.rs +++ b/crates/brk_computer/src/internal/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod algo; mod block_walker; mod cache_budget; mod containers; +mod daily_metric; pub(crate) mod db_utils; mod indexes; mod per_block; @@ -14,6 +15,7 @@ mod with_addr_types; pub(crate) use block_walker::*; pub(crate) use cache_budget::*; pub(crate) use containers::*; +pub(crate) use daily_metric::*; pub(crate) use indexes::*; pub(crate) use per_block::*; pub(crate) use per_tx::*; diff --git a/crates/brk_computer/src/lib.rs b/crates/brk_computer/src/lib.rs index 546d6fd25..47776bd4d 100644 --- a/crates/brk_computer/src/lib.rs +++ b/crates/brk_computer/src/lib.rs @@ -468,7 +468,6 @@ impl Computer { &self.indexes, &self.price, &self.distribution, - &self.market, &self.frameworks, exit, ) diff --git a/crates/brk_computer/src/models/bedrock/import.rs b/crates/brk_computer/src/models/bedrock/import.rs index fc3367af0..05d080151 100644 --- a/crates/brk_computer/src/models/bedrock/import.rs +++ b/crates/brk_computer/src/models/bedrock/import.rs @@ -6,10 +6,12 @@ use vecdb::Database; use super::{ price::Price, - urpd_metric::{UrpdMappings, UrpdMetric}, vecs::{Levels, ModeVecs, Modes, Percentiles, Vecs}, }; -use crate::indexes; +use crate::{ + indexes, + internal::{DailyMappings, DailyMetric}, +}; const VERSION: Version = Version::new(4); @@ -41,16 +43,16 @@ fn import_ratio( db: &Database, name: &str, version: Version, - mappings: &UrpdMappings, -) -> Result> { - UrpdMetric::forced_import(db, name, version, mappings) + mappings: &DailyMappings, +) -> Result> { + DailyMetric::forced_import(db, name, version, mappings) } fn import_price( db: &Database, name: &str, version: Version, - mappings: &UrpdMappings, + mappings: &DailyMappings, ) -> Result { Price::forced_import(db, name, version, mappings) } @@ -59,7 +61,7 @@ fn import_mode( db: &Database, name: &str, version: Version, - mappings: &UrpdMappings, + mappings: &DailyMappings, ) -> Result { Ok(ModeVecs { loss_threshold: import_percentiles(|percentile| { @@ -87,7 +89,7 @@ impl Vecs { states_path: PathBuf, ) -> Result { let version = parent_version + VERSION; - let mappings = UrpdMappings::new(indexes); + let mappings = DailyMappings::new(indexes); Ok(Self { states_path, diff --git a/crates/brk_computer/src/models/bedrock/mod.rs b/crates/brk_computer/src/models/bedrock/mod.rs index dd8b91108..ad4eab946 100644 --- a/crates/brk_computer/src/models/bedrock/mod.rs +++ b/crates/brk_computer/src/models/bedrock/mod.rs @@ -1,7 +1,6 @@ mod compute; mod import; mod price; -mod urpd_metric; mod vecs; use std::path::PathBuf; diff --git a/crates/brk_computer/src/models/bedrock/price.rs b/crates/brk_computer/src/models/bedrock/price.rs index 313500959..87cf3372d 100644 --- a/crates/brk_computer/src/models/bedrock/price.rs +++ b/crates/brk_computer/src/models/bedrock/price.rs @@ -4,61 +4,19 @@ //! custom daily repeat/last-day views: cents are stored, USD is derived from cents, //! and sats are derived from USD. -use brk_traversable::Traversable; use brk_types::{Cents, Day1, Dollars, SatsFract, Version}; -use schemars::JsonSchema; -use vecdb::{ - LazyVecFrom1, ReadableBoxedVec, ReadableCloneableVec, Rw, StorageMode, UnaryTransform, +use brk_traversable::Traversable; +use vecdb::{ReadableCloneableVec, Rw, StorageMode}; + +use crate::internal::{ + CentsUnsignedToDollars, DailyMappings, DailyMetric, DollarsToSatsFract, LazyDailyMetric, }; -use super::urpd_metric::{UrpdMappings, UrpdMetric, UrpdViews}; -use crate::internal::{CentsUnsignedToDollars, DollarsToSatsFract, NumericValue}; - -type LazyDay = LazyVecFrom1; - -#[derive(Clone, Traversable)] -#[traversable(merge)] -pub struct LazyUrpdMetric -where - T: NumericValue + JsonSchema, - S: NumericValue, -{ - pub day1: LazyDay, - #[traversable(flatten)] - pub views: Box>, -} - -impl LazyUrpdMetric -where - T: NumericValue + JsonSchema + 'static, - S: NumericValue + JsonSchema, -{ - fn from_source( - name: &str, - version: Version, - source: ReadableBoxedVec, - mappings: &UrpdMappings, - ) -> Self - where - F: UnaryTransform, - { - let day1 = LazyVecFrom1::transformed::(name, version, source); - let views = Box::new(UrpdViews::new( - name, - day1.read_only_boxed_clone(), - version, - mappings, - )); - - Self { day1, views } - } -} - #[derive(Traversable)] pub struct Price { - pub usd: LazyUrpdMetric, - pub cents: UrpdMetric, - pub sats: LazyUrpdMetric, + pub usd: LazyDailyMetric, + pub cents: DailyMetric, + pub sats: LazyDailyMetric, } impl Price { @@ -66,16 +24,16 @@ impl Price { db: &vecdb::Database, name: &str, version: Version, - mappings: &UrpdMappings, + mappings: &DailyMappings, ) -> brk_error::Result { - let cents = UrpdMetric::forced_import(db, &format!("{name}_cents"), version, mappings)?; - let usd = LazyUrpdMetric::from_source::( + let cents = DailyMetric::forced_import(db, &format!("{name}_cents"), version, mappings)?; + let usd = LazyDailyMetric::from_source::( name, version, cents.day1.read_only_boxed_clone(), mappings, ); - let sats = LazyUrpdMetric::from_source::( + let sats = LazyDailyMetric::from_source::( &format!("{name}_sats"), version, usd.day1.read_only_boxed_clone(), diff --git a/crates/brk_computer/src/models/bedrock/vecs.rs b/crates/brk_computer/src/models/bedrock/vecs.rs index 7007f6b87..ec1acea0e 100644 --- a/crates/brk_computer/src/models/bedrock/vecs.rs +++ b/crates/brk_computer/src/models/bedrock/vecs.rs @@ -5,7 +5,8 @@ use brk_types::StoredF64; use derive_more::{Deref, DerefMut}; use vecdb::{Rw, StorageMode}; -use super::{price::Price, urpd_metric::UrpdMetric}; +use super::price::Price; +use crate::internal::DailyMetric; pub(crate) const MODE_COUNT: usize = 10; pub(crate) const MODE_NAMES: [&str; MODE_COUNT] = [ @@ -45,7 +46,7 @@ pub struct Levels { #[derive(Traversable)] pub struct ModeVecs { - pub loss_threshold: Percentiles>, + pub loss_threshold: Percentiles>, pub floor: Percentiles>, pub level: Levels>, } diff --git a/crates/brk_computer/src/models/capital_sentiment/compute.rs b/crates/brk_computer/src/models/capital_sentiment/compute.rs index 98c505efb..f6c89f3ec 100644 --- a/crates/brk_computer/src/models/capital_sentiment/compute.rs +++ b/crates/brk_computer/src/models/capital_sentiment/compute.rs @@ -1,25 +1,28 @@ -use brk_error::{OptionData, Result}; +use std::collections::VecDeque; + +use brk_error::Result; use brk_indexer::Indexer; -use brk_types::{CapitalSentimentPhase, Cents, Height, StoredBool, StoredU8, Version}; +use brk_types::{CapitalSentimentPhase, Cents, Day1, StoredBool, StoredU8, Version}; use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, VecIndex, WritableVec}; use super::Vecs; use crate::{ - distribution, internal::db_utils::validate_any_computed_version_or_reset, market, price, + distribution, indexes, internal::db_utils::validate_any_computed_version_or_reset, price, }; -const WRITE_INTERVAL: usize = 10_000; +const PRICE_SMA_DAYS: usize = 365; +const WRITE_INTERVAL_DAYS: usize = 1_000; impl Vecs { pub(crate) fn compute( &mut self, indexer: &Indexer, + indexes: &indexes::Vecs, prices: &price::Vecs, distribution: &distribution::Vecs, - market: &market::Vecs, exit: &Exit, ) -> Result<()> { - let spot = &prices.spot.cents.height; + let close = &prices.split.close.cents.day1; let all = &distribution .utxo_cohorts .all @@ -28,7 +31,7 @@ impl Vecs { .capitalized .price .cents - .height; + .day1; let sth = &distribution .utxo_cohorts .sth @@ -37,7 +40,7 @@ impl Vecs { .capitalized .price .cents - .height; + .day1; let lth = &distribution .utxo_cohorts .lth @@ -46,74 +49,77 @@ impl Vecs { .capitalized .price .cents - .height; - let sma_1y = &market.moving_average.sma._1y.cents.height; + .day1; - let source_version: Version = [ - spot.version(), - all.version(), - sth.version(), - lth.version(), - sma_1y.version(), - ] + let source_version: Version = [close.version(), all.version(), sth.version(), lth.version()] .into_iter() .sum(); - validate_any_computed_version_or_reset(&mut self.phase_code.height, source_version)?; - validate_any_computed_version_or_reset(&mut self.is_long.height, source_version)?; + validate_any_computed_version_or_reset(&mut self.phase_code.day1, source_version)?; + validate_any_computed_version_or_reset(&mut self.is_long.day1, source_version)?; - let source_end = [spot.len(), all.len(), sth.len(), lth.len(), sma_1y.len()] + let source_end = [ + indexes.day1.date.len(), + close.len(), + all.len(), + sth.len(), + lth.len(), + ] .into_iter() .min() .unwrap_or_default(); + let recompute_from = recompute_day(indexer, indexes) + .map(usize::from) + .unwrap_or_default(); let start = self .phase_code - .height + .day1 .len() - .min(self.is_long.height.len()) - .min(indexer.safe_lengths().height.to_usize()) + .min(self.is_long.day1.len()) + .min(recompute_from) .min(source_end); - self.phase_code.height.any_truncate_if_needed_at(start)?; - self.is_long.height.any_truncate_if_needed_at(start)?; + self.phase_code.day1.any_truncate_if_needed_at(start)?; + self.is_long.day1.any_truncate_if_needed_at(start)?; let mut is_long = start .checked_sub(1) - .map(Height::from) - .map(|height| self.is_long.height.collect_one(height).data()) - .transpose()? + .map(Day1::from) + .and_then(|day| self.is_long.day1.collect_one(day)) .is_some_and(|value| value.is_true()); - let mut previous_price = start + let mut previous_over_sth = start .checked_sub(1) - .map(Height::from) - .map(|height| spot.collect_one(height).data()) - .transpose()?; - let mut previous_sth = start - .checked_sub(1) - .map(Height::from) - .map(|height| sth.collect_one(height).data()) - .transpose()?; + .map(Day1::from) + .map(|day| { + is_over_sth( + close.collect_one(day).flatten(), + sth.collect_one(day).flatten(), + ) + }); + let mut sma = RollingSma::from_history(close, start); - for height_index in start..source_end { - let height = Height::from(height_index); - let price = spot.collect_one(height).data()?; - let sth = sth.collect_one(height).data()?; + for day_index in start..source_end { + let day = Day1::from(day_index); + let price = close.collect_one(day).flatten(); + let sth = sth.collect_one(day).flatten(); + let over_sth = is_over_sth(price, sth); let code = classify_phase_code( price, - all.collect_one(height).data()?, + all.collect_one(day).flatten(), sth, - lth.collect_one(height).data()?, - sma_1y.collect_one(height).data()?, + lth.collect_one(day).flatten(), + sma.observe(price), ); - is_long = next_is_long(is_long, previous_price.zip(previous_sth), price, sth, code); + is_long = next_is_long(is_long, previous_over_sth, over_sth, code); - self.phase_code.height.push(code); - self.is_long.height.push(StoredBool::from(is_long)); - previous_price = Some(price); - previous_sth = Some(sth); + self.phase_code.day1.push(code); + self.is_long.day1.push(StoredBool::from(is_long)); + previous_over_sth = Some(over_sth); - if (height_index + 1).is_multiple_of(WRITE_INTERVAL) || height_index + 1 == source_end { + if (day_index + 1).is_multiple_of(WRITE_INTERVAL_DAYS) + || day_index + 1 == source_end + { let _lock = exit.lock(); - self.phase_code.height.write()?; - self.is_long.height.write()?; + self.phase_code.day1.write()?; + self.is_long.day1.write()?; } } @@ -121,17 +127,47 @@ impl Vecs { } } -/// Advance the stateful cash/long strategy used by BRK Signal. +#[derive(Default)] +struct RollingSma { + values: VecDeque, + sum: u128, +} + +impl RollingSma { + fn from_history( + source: &impl ReadableVec>, + end: usize, + ) -> Self { + let mut sma = Self::default(); + source.for_each_range_at(0, end, |price| { + let _ = sma.observe(price); + }); + sma + } + + /// Observe one daily close and return the sum of the latest 365 valid closes. + fn observe(&mut self, price: Option) -> Option { + if let Some(price) = price.filter(|price| is_finite_positive(*price)) { + let price = price.inner(); + self.values.push_back(price); + self.sum += u128::from(price); + if self.values.len() > PRICE_SMA_DAYS { + self.sum -= u128::from(self.values.pop_front().unwrap()); + } + } + (self.values.len() == PRICE_SMA_DAYS).then_some(self.sum) + } +} + +/// Advance the stateful short/long strategy used by BRK Signal. fn next_is_long( is_long: bool, - previous: Option<(Cents, Cents)>, - price: Cents, - sth: Cents, + previous_over_sth: Option, + over_sth: bool, phase_code: StoredU8, ) -> bool { - let crossed_above_sth = previous.is_some_and(|(previous_price, previous_sth)| { - !is_over_sth(previous_price, previous_sth) && is_over_sth(price, sth) - }); + let crossed_above_sth = + previous_over_sth.is_some_and(|previous| !previous && over_sth); if !is_long && crossed_above_sth { return true; @@ -144,42 +180,71 @@ fn next_is_long( } #[inline] -fn is_over_sth(price: Cents, sth: Cents) -> bool { - !price.is_nan() && !sth.is_nan() && price > Cents::ZERO && sth > Cents::ZERO && price >= sth +fn is_finite_positive(value: Cents) -> bool { + !value.is_nan() && value > Cents::ZERO } -/// Code `0` means the model's references are not all available yet. -fn classify_phase_code(price: Cents, all: Cents, sth: Cents, lth: Cents, sma: Cents) -> StoredU8 { - if [price, all, sth, lth, sma].into_iter().any(Cents::is_nan) { - StoredU8::ZERO - } else { - StoredU8::new(classify_phase(price, all, sth, lth, sma).code()) - } +#[inline] +fn is_over_sth(price: Option, sth: Option) -> bool { + price + .zip(sth) + .is_some_and(|(price, sth)| { + is_finite_positive(price) && is_finite_positive(sth) && price >= sth + }) +} + +/// Code `0` means the capitalized-price references are not all available yet. +fn classify_phase_code( + price: Option, + all: Option, + sth: Option, + lth: Option, + sma_sum: Option, +) -> StoredU8 { + let Some((price, all, sth, lth)) = price + .zip(all) + .zip(sth) + .zip(lth) + .map(|(((price, all), sth), lth)| (price, all, sth, lth)) + .filter(|values| { + [values.0, values.1, values.2, values.3] + .into_iter() + .all(is_finite_positive) + }) + else { + return StoredU8::ZERO; + }; + + StoredU8::new(classify_phase(price, all, sth, lth, sma_sum).code()) } /// Classify investor sentiment from the three capitalized-price references, -/// using the 1-year SMA only as confirmation and disambiguation. +/// using the 365-daily-close SMA only as confirmation and disambiguation. fn classify_phase( price: Cents, all: Cents, sth: Cents, lth: Cents, - sma: Cents, + sma_sum: Option, ) -> CapitalSentimentPhase { use CapitalSentimentPhase as Phase; + let price_x_days = price.as_u128() * PRICE_SMA_DAYS as u128; + let all_x_days = all.as_u128() * PRICE_SMA_DAYS as u128; let above_all = price >= all; let above_sth = price >= sth; let above_lth = price >= lth; - let above_sma = price >= sma; - let bull_structure = sth >= lth; + let above_sma = sma_sum.is_some_and(|sma| price_x_days >= sma); + let bull_structure = sth > lth; let above_slow_refs = above_all && above_lth; - let references_above_price = [all, sth, lth, sma] + let above_any_slow_ref = above_all || above_lth; + let references_above_price = [all, sth, lth] .into_iter() .filter(|reference| *reference > price) - .count(); + .count() + + usize::from(sma_sum.is_some_and(|sma| sma > price_x_days)); let price_in_middle = references_above_price == 2; - let core_bull_phase = if all > sma { + let core_bull_phase = if sma_sum.is_some_and(|sma| all_x_days > sma) { Phase::RagingBull } else { Phase::Bull @@ -190,6 +255,38 @@ fn classify_phase( Phase::Bear }; + if sma_sum.is_none() { + if bull_structure { + if above_sth { + return if above_slow_refs { + core_bull_phase + } else { + Phase::EarlyBull + }; + } + if above_slow_refs { + return Phase::WeakBull; + } + return if above_any_slow_ref { + Phase::EarlyBear + } else { + core_bear_phase + }; + } + + if !above_sth { + return core_bear_phase; + } + if above_slow_refs { + return core_bull_phase; + } + return if above_any_slow_ref { + Phase::EarlyBull + } else { + Phase::CautiousBull + }; + } + if !above_all && !above_sth && !above_lth && !above_sma { return core_bear_phase; } @@ -218,6 +315,19 @@ fn classify_phase( Phase::EarlyBear } +fn recompute_day(indexer: &Indexer, indexes: &indexes::Vecs) -> Option { + let starting_height = indexer.safe_lengths().height; + indexes + .height + .day1 + .collect_one(starting_height) + .or_else(|| { + starting_height + .decremented() + .and_then(|height| indexes.height.day1.collect_one(height)) + }) +} + #[cfg(test)] mod tests { use std::{cmp::Reverse, collections::BTreeSet}; @@ -229,7 +339,13 @@ mod tests { } fn classify(price: u64, all: u64, sth: u64, lth: u64, sma: u64) -> CapitalSentimentPhase { - classify_phase(cents(price), cents(all), cents(sth), cents(lth), cents(sma)) + classify_phase( + cents(price), + cents(all), + cents(sth), + cents(lth), + Some(u128::from(sma) * PRICE_SMA_DAYS as u128), + ) } #[test] @@ -276,21 +392,35 @@ mod tests { } #[test] - fn capitalized_crossover_uses_sth_led_tie_break() { + fn equal_sth_and_lth_is_not_a_bull_structure() { assert_eq!( classify(70, 50, 50, 50, 100), - CapitalSentimentPhase::EarlyBear + CapitalSentimentPhase::CautiousBull ); } #[test] fn missing_reference_has_no_phase() { assert_eq!( - classify_phase_code(cents(100), cents(70), cents(80), Cents::NAN, cents(50)), + classify_phase_code( + Some(cents(100)), + Some(cents(70)), + Some(cents(80)), + None, + Some(u128::from(50_u64) * PRICE_SMA_DAYS as u128), + ), StoredU8::ZERO ); } + #[test] + fn phase_is_available_before_the_sma_window_is_full() { + assert_eq!( + classify_phase(cents(100), cents(70), cents(80), cents(60), None), + CapitalSentimentPhase::Bull + ); + } + #[test] fn signal_enters_only_on_an_sth_cross_and_exits_on_a_sell_phase() { use CapitalSentimentPhase as Phase; @@ -298,34 +428,10 @@ mod tests { let bull = StoredU8::new(Phase::Bull.code()); let bear = StoredU8::new(Phase::Bear.code()); - assert!(!next_is_long(false, None, cents(100), cents(80), bull)); - assert!(next_is_long( - false, - Some((cents(70), cents(80))), - cents(80), - cents(80), - bull, - )); - assert!(!next_is_long( - false, - Some((cents(90), cents(80))), - cents(100), - cents(80), - bull, - )); - assert!(next_is_long( - true, - Some((cents(90), cents(80))), - cents(100), - cents(80), - bull, - )); - assert!(!next_is_long( - true, - Some((cents(90), cents(80))), - cents(100), - cents(80), - bear, - )); + assert!(!next_is_long(false, None, true, bull)); + assert!(next_is_long(false, Some(false), true, bull)); + assert!(!next_is_long(false, Some(true), true, bull)); + assert!(next_is_long(true, Some(true), true, bull)); + assert!(!next_is_long(true, Some(true), true, bear)); } } diff --git a/crates/brk_computer/src/models/capital_sentiment/import.rs b/crates/brk_computer/src/models/capital_sentiment/import.rs index 949fc3a0c..3b790ed67 100644 --- a/crates/brk_computer/src/models/capital_sentiment/import.rs +++ b/crates/brk_computer/src/models/capital_sentiment/import.rs @@ -5,10 +5,10 @@ use vecdb::{Database, ReadableCloneableVec, UnaryTransform}; use super::Vecs; use crate::{ indexes, - internal::{LazyPerBlock, PerBlock}, + internal::{DailyMappings, DailyMetric, LazyDailyMetric}, }; -const VERSION: Version = Version::new(3); +const VERSION: Version = Version::new(4); struct CodeToPhase; @@ -51,31 +51,37 @@ impl Vecs { indexes: &indexes::Vecs, ) -> Result { let version = parent_version + VERSION; + let mappings = DailyMappings::new(indexes); - let phase_code = - PerBlock::forced_import(db, "capital_sentiment_phase_code", version, indexes)?; - let is_long = PerBlock::::forced_import( + let phase_code = DailyMetric::forced_import( + db, + "capital_sentiment_phase_code", + version, + &mappings, + )?; + let is_long = DailyMetric::::forced_import( db, "capital_sentiment_is_long", version, - indexes, + &mappings, )?; - let is_short = LazyPerBlock::from_computed::( + let is_short = LazyDailyMetric::from_source::( "capital_sentiment_is_short", version, - is_long.height.read_only_boxed_clone(), - &is_long, + is_long.day1.read_only_boxed_clone(), + &mappings, ); - let phase = LazyPerBlock::from_computed::( + let phase = LazyDailyMetric::from_source::( "capital_sentiment_phase", version, - phase_code.height.read_only_boxed_clone(), - &phase_code, + phase_code.day1.read_only_boxed_clone(), + &mappings, ); - let score = LazyPerBlock::from_lazy::( + let score = LazyDailyMetric::from_source::( "capital_sentiment_score", version, - &phase, + phase.day1.read_only_boxed_clone(), + &mappings, ); Ok(Self { diff --git a/crates/brk_computer/src/models/capital_sentiment/vecs.rs b/crates/brk_computer/src/models/capital_sentiment/vecs.rs index fa3717c58..54cc3cce4 100644 --- a/crates/brk_computer/src/models/capital_sentiment/vecs.rs +++ b/crates/brk_computer/src/models/capital_sentiment/vecs.rs @@ -2,18 +2,18 @@ use brk_traversable::Traversable; use brk_types::{CapitalSentimentPhase, StoredBool, StoredI8, StoredU8}; use vecdb::{Rw, StorageMode}; -use crate::internal::{LazyPerBlock, PerBlock}; +use crate::internal::{DailyMetric, LazyDailyMetric}; #[derive(Traversable)] pub struct Vecs { - /// Compact, per-block source of truth. + /// Compact daily source of truth. #[traversable(hidden)] - pub(super) phase_code: PerBlock, + pub(super) phase_code: DailyMetric, - /// BRK Signal position: `true` is long and `false` is cash. - pub is_long: PerBlock, + /// BRK Signal position: `true` is long and `false` is short. + pub is_long: DailyMetric, /// Lazy complement of `is_long`. - pub is_short: LazyPerBlock, - pub phase: LazyPerBlock, StoredU8>, - pub score: LazyPerBlock, Option>, + pub is_short: LazyDailyMetric, + pub phase: LazyDailyMetric, StoredU8>, + pub score: LazyDailyMetric, Option>, } diff --git a/crates/brk_computer/src/models/compute.rs b/crates/brk_computer/src/models/compute.rs index cd277a79c..9a359e4ba 100644 --- a/crates/brk_computer/src/models/compute.rs +++ b/crates/brk_computer/src/models/compute.rs @@ -3,7 +3,7 @@ use brk_indexer::Indexer; use vecdb::Exit; use super::Vecs; -use crate::{distribution, frameworks, indexes, market, price}; +use crate::{distribution, frameworks, indexes, price}; impl Vecs { #[allow(clippy::too_many_arguments)] @@ -13,7 +13,6 @@ impl Vecs { indexes: &indexes::Vecs, prices: &price::Vecs, distribution: &distribution::Vecs, - market: &market::Vecs, frameworks: &frameworks::Vecs, exit: &Exit, ) -> Result<()> { @@ -28,7 +27,7 @@ impl Vecs { exit, )?; self.capital_sentiment - .compute(indexer, prices, distribution, market, exit)?; + .compute(indexer, indexes, prices, distribution, exit)?; self.rarity_meter.compute( indexer, distribution, diff --git a/crates/vecdb/src/variants/lazy/agg/sparse.rs b/crates/vecdb/src/variants/lazy/agg/sparse.rs index 1f0c5c536..345679cf7 100644 --- a/crates/vecdb/src/variants/lazy/agg/sparse.rs +++ b/crates/vecdb/src/variants/lazy/agg/sparse.rs @@ -26,10 +26,9 @@ impl AggFold, SI, SI, T> for Sparse { let next_first = mapping .get(idx + 1) .map(|h| h.to_usize()) - .unwrap_or(source_len) - .min(source_len); + .unwrap_or(source_len); - if current_first >= next_first { + if next_first == 0 || current_first >= next_first { slot_map.push(None); } else { slot_map.push(Some(indices.len() as u32)); @@ -56,111 +55,11 @@ impl AggFold, SI, SI, T> for Sparse { let next_first = mapping .get(index + 1) .map(|h| h.to_usize()) - .unwrap_or(source_len) - .min(source_len); + .unwrap_or(source_len); - if current_first >= next_first { + if next_first == 0 || current_first >= next_first { return Some(None); } Some(source.collect_one_at(next_first - 1)) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::{AnyVec, Version}; - - struct TestVec(Vec); - - impl AnyVec for TestVec { - fn version(&self) -> Version { - Version::ZERO - } - - fn name(&self) -> &str { - "test" - } - - fn len(&self) -> usize { - self.0.len() - } - - fn index_type_to_string(&self) -> &'static str { - "usize" - } - - fn region_names(&self) -> Vec { - Vec::new() - } - - fn value_type_to_size_of(&self) -> usize { - size_of::() - } - - fn value_type_to_string(&self) -> &'static str { - "u8" - } - } - - impl ReadableVec for TestVec { - fn read_into_at(&self, from: usize, to: usize, buf: &mut Vec) { - buf.extend_from_slice(&self.0[from.min(self.len())..to.min(self.len())]); - } - - fn for_each_range_dyn_at(&self, from: usize, to: usize, f: &mut dyn FnMut(u8)) { - self.0[from.min(self.len())..to.min(self.len())] - .iter() - .copied() - .for_each(f); - } - - fn fold_range_at B>( - &self, - from: usize, - to: usize, - init: B, - f: F, - ) -> B { - self.0[from.min(self.len())..to.min(self.len())] - .iter() - .copied() - .fold(init, f) - } - - fn try_fold_range_at Result>( - &self, - from: usize, - to: usize, - init: B, - f: F, - ) -> Result { - self.0[from.min(self.len())..to.min(self.len())] - .iter() - .copied() - .try_fold(init, f) - } - } - - #[test] - fn partial_source_returns_none_after_its_last_period() { - let source = TestVec(vec![10, 20, 30]); - let mapping = [0_usize, 2, 4, 6]; - - let values = Sparse::fold( - &source, - &mapping, - 0, - mapping.len(), - Vec::new(), - |mut v, x| { - v.push(x); - v - }, - ); - - assert_eq!(values, [Some(20), Some(30), None, None]); - assert_eq!(Sparse::collect_one(&source, &mapping, 1), Some(Some(30))); - assert_eq!(Sparse::collect_one(&source, &mapping, 2), Some(None)); - } -}