global: part X + 2

This commit is contained in:
nym21
2026-08-04 22:43:07 +02:00
parent c9ca972657
commit 68e0eea88f
12 changed files with 353 additions and 337 deletions
@@ -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<T, M> = <M as StorageMode>::Stored<EagerVec<PcoVec<Day1, T>>>;
type DayMapping<I, T> = LazyVecFrom1<I, Day1, I, T>;
type Repeated<I, T> = UrpdView<I, T, RepeatDay>;
type Last<I, T> = UrpdView<I, T, LastDay>;
type Repeated<I, T> = DailyView<I, T, RepeatDay>;
type Last<I, T> = DailyView<I, T, LastDay>;
pub trait DailyValue: VecValue + Formattable + JsonSchema + Serialize {}
impl<T> 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<Height, Day1>,
minute10: DayMapping<Minute10, Timestamp>,
minute30: DayMapping<Minute30, Timestamp>,
@@ -42,7 +47,7 @@ pub(crate) struct UrpdMappings {
epoch: DayMapping<Epoch, Timestamp>,
}
impl UrpdMappings {
impl DailyMappings {
pub(crate) fn new(indexes: &indexes::Vecs) -> Self {
let height = LazyVecFrom1::init(
"day1",
@@ -87,9 +92,9 @@ fn date_mapping<I: VecIndex>(source: ReadableBoxedVec<I, Date>) -> DayMapping<I,
#[derive(Clone, Traversable)]
#[traversable(merge)]
pub struct UrpdViews<T>
pub struct DailyViews<T>
where
T: NumericValue + JsonSchema,
T: DailyValue,
{
pub height: Repeated<Height, T>,
pub minute10: Repeated<Minute10, T>,
@@ -108,15 +113,15 @@ where
pub epoch: Last<Epoch, T>,
}
impl<T> UrpdViews<T>
impl<T> DailyViews<T>
where
T: NumericValue + JsonSchema,
T: DailyValue,
{
pub(super) fn new(
pub(crate) fn new(
name: &str,
source: ReadableBoxedVec<Day1, T>,
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<T, M: StorageMode = Rw>
pub struct DailyMetric<T, M: StorageMode = Rw>
where
T: NumericValue + JsonSchema,
T: DailyValue + PcoVecValue,
{
pub day1: StoredDay<T, M>,
#[traversable(flatten)]
pub views: Box<UrpdViews<T>>,
pub views: Box<DailyViews<T>>,
}
impl<T> UrpdMetric<T>
impl<T> DailyMetric<T>
where
T: NumericValue + JsonSchema,
T: DailyValue + PcoVecValue,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
mappings: &UrpdMappings,
mappings: &DailyMappings,
) -> Result<Self> {
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<T, S> = LazyVecFrom1<Day1, T, Day1, S>;
#[derive(Clone, Traversable)]
#[traversable(merge)]
pub struct LazyDailyMetric<T, S>
where
T: DailyValue,
S: VecValue,
{
pub day1: LazyDay<T, S>,
#[traversable(flatten)]
pub views: Box<DailyViews<T>>,
}
impl<T, S> LazyDailyMetric<T, S>
where
T: DailyValue,
S: VecValue,
{
pub(crate) fn from_source<F>(
name: &str,
version: Version,
source: ReadableBoxedVec<Day1, S>,
mappings: &DailyMappings,
) -> Self
where
F: UnaryTransform<S, T>,
{
let day1 = LazyVecFrom1::transformed::<F>(name, version, source);
let views = Box::new(DailyViews::new(
name,
day1.read_only_boxed_clone(),
version,
mappings,
));
Self { day1, views }
}
}
fn repeated<I, T, V>(
name: &str,
source: ReadableBoxedVec<Day1, T>,
@@ -178,7 +223,7 @@ where
T: VecValue,
V: ReadableCloneableVec<I, Day1> + ?Sized,
{
UrpdView::new(name, version, source, mapping.read_only_boxed_clone())
DailyView::new(name, version, source, mapping.read_only_boxed_clone())
}
fn last<I, T, V>(
@@ -192,7 +237,7 @@ where
T: VecValue,
V: ReadableCloneableVec<I, Day1> + ?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<I, T, S>
pub struct DailyView<I, T, S>
where
I: VecIndex,
T: VecValue,
@@ -232,7 +277,7 @@ where
_phantom: PhantomData<fn() -> S>,
}
impl<I, T, S> Clone for UrpdView<I, T, S>
impl<I, T, S> Clone for DailyView<I, T, S>
where
I: VecIndex,
T: VecValue,
@@ -248,7 +293,7 @@ where
}
}
impl<I, T, S> UrpdView<I, T, S>
impl<I, T, S> DailyView<I, T, S>
where
I: VecIndex,
T: VecValue,
@@ -312,7 +357,7 @@ where
}
}
impl<I, T, S> AnyVec for UrpdView<I, T, S>
impl<I, T, S> AnyVec for DailyView<I, T, S>
where
I: VecIndex,
T: VecValue,
@@ -347,7 +392,7 @@ where
}
}
impl<I, T, S> TypedVec for UrpdView<I, T, S>
impl<I, T, S> TypedVec for DailyView<I, T, S>
where
I: VecIndex,
T: VecValue,
@@ -357,7 +402,7 @@ where
type T = Option<T>;
}
impl<I, T, S> ReadableVec<I, Option<T>> for UrpdView<I, T, S>
impl<I, T, S> ReadableVec<I, Option<T>> for DailyView<I, T, S>
where
I: VecIndex,
T: VecValue,
@@ -407,10 +452,10 @@ where
}
}
impl<I, T, S> Traversable for UrpdView<I, T, S>
impl<I, T, S> Traversable for DailyView<I, T, S>
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<PcoVec<Day1, StoredF64>> =
@@ -542,7 +587,7 @@ mod tests {
source.write().unwrap();
mapping.write().unwrap();
let view = UrpdView::<Height, StoredF64, RepeatDay>::new(
let view = DailyView::<Height, StoredF64, RepeatDay>::new(
"test",
Version::ONE,
source.read_only_boxed_clone(),
+2
View File
@@ -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::*;
-1
View File
@@ -468,7 +468,6 @@ impl Computer {
&self.indexes,
&self.price,
&self.distribution,
&self.market,
&self.frameworks,
exit,
)
@@ -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<StoredF64>> {
UrpdMetric::forced_import(db, name, version, mappings)
mappings: &DailyMappings,
) -> Result<DailyMetric<StoredF64>> {
DailyMetric::forced_import(db, name, version, mappings)
}
fn import_price(
db: &Database,
name: &str,
version: Version,
mappings: &UrpdMappings,
mappings: &DailyMappings,
) -> Result<Price> {
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<ModeVecs> {
Ok(ModeVecs {
loss_threshold: import_percentiles(|percentile| {
@@ -87,7 +89,7 @@ impl Vecs {
states_path: PathBuf,
) -> Result<Self> {
let version = parent_version + VERSION;
let mappings = UrpdMappings::new(indexes);
let mappings = DailyMappings::new(indexes);
Ok(Self {
states_path,
@@ -1,7 +1,6 @@
mod compute;
mod import;
mod price;
mod urpd_metric;
mod vecs;
use std::path::PathBuf;
+12 -54
View File
@@ -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<T, S> = LazyVecFrom1<Day1, T, Day1, S>;
#[derive(Clone, Traversable)]
#[traversable(merge)]
pub struct LazyUrpdMetric<T, S>
where
T: NumericValue + JsonSchema,
S: NumericValue,
{
pub day1: LazyDay<T, S>,
#[traversable(flatten)]
pub views: Box<UrpdViews<T>>,
}
impl<T, S> LazyUrpdMetric<T, S>
where
T: NumericValue + JsonSchema + 'static,
S: NumericValue + JsonSchema,
{
fn from_source<F>(
name: &str,
version: Version,
source: ReadableBoxedVec<Day1, S>,
mappings: &UrpdMappings,
) -> Self
where
F: UnaryTransform<S, T>,
{
let day1 = LazyVecFrom1::transformed::<F>(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<M: StorageMode = Rw> {
pub usd: LazyUrpdMetric<Dollars, Cents>,
pub cents: UrpdMetric<Cents, M>,
pub sats: LazyUrpdMetric<SatsFract, Dollars>,
pub usd: LazyDailyMetric<Dollars, Cents>,
pub cents: DailyMetric<Cents, M>,
pub sats: LazyDailyMetric<SatsFract, Dollars>,
}
impl Price {
@@ -66,16 +24,16 @@ impl Price {
db: &vecdb::Database,
name: &str,
version: Version,
mappings: &UrpdMappings,
mappings: &DailyMappings,
) -> brk_error::Result<Self> {
let cents = UrpdMetric::forced_import(db, &format!("{name}_cents"), version, mappings)?;
let usd = LazyUrpdMetric::from_source::<CentsUnsignedToDollars>(
let cents = DailyMetric::forced_import(db, &format!("{name}_cents"), version, mappings)?;
let usd = LazyDailyMetric::from_source::<CentsUnsignedToDollars>(
name,
version,
cents.day1.read_only_boxed_clone(),
mappings,
);
let sats = LazyUrpdMetric::from_source::<DollarsToSatsFract>(
let sats = LazyDailyMetric::from_source::<DollarsToSatsFract>(
&format!("{name}_sats"),
version,
usd.day1.read_only_boxed_clone(),
@@ -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<T> {
#[derive(Traversable)]
pub struct ModeVecs<M: StorageMode = Rw> {
pub loss_threshold: Percentiles<UrpdMetric<StoredF64, M>>,
pub loss_threshold: Percentiles<DailyMetric<StoredF64, M>>,
pub floor: Percentiles<Price<M>>,
pub level: Levels<Price<M>>,
}
@@ -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<u64>,
sum: u128,
}
impl RollingSma {
fn from_history(
source: &impl ReadableVec<Day1, Option<Cents>>,
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<Cents>) -> Option<u128> {
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<bool>,
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<Cents>, sth: Option<Cents>) -> 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<Cents>,
all: Option<Cents>,
sth: Option<Cents>,
lth: Option<Cents>,
sma_sum: Option<u128>,
) -> 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<u128>,
) -> 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<Day1> {
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));
}
}
@@ -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<Self> {
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::<StoredBool>::forced_import(
let phase_code = DailyMetric::forced_import(
db,
"capital_sentiment_phase_code",
version,
&mappings,
)?;
let is_long = DailyMetric::<StoredBool>::forced_import(
db,
"capital_sentiment_is_long",
version,
indexes,
&mappings,
)?;
let is_short = LazyPerBlock::from_computed::<IsLongToIsShort>(
let is_short = LazyDailyMetric::from_source::<IsLongToIsShort>(
"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::<CodeToPhase>(
let phase = LazyDailyMetric::from_source::<CodeToPhase>(
"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::<PhaseToScore, StoredU8>(
let score = LazyDailyMetric::from_source::<PhaseToScore>(
"capital_sentiment_score",
version,
&phase,
phase.day1.read_only_boxed_clone(),
&mappings,
);
Ok(Self {
@@ -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<M: StorageMode = Rw> {
/// Compact, per-block source of truth.
/// Compact daily source of truth.
#[traversable(hidden)]
pub(super) phase_code: PerBlock<StoredU8, M>,
pub(super) phase_code: DailyMetric<StoredU8, M>,
/// BRK Signal position: `true` is long and `false` is cash.
pub is_long: PerBlock<StoredBool, M>,
/// BRK Signal position: `true` is long and `false` is short.
pub is_long: DailyMetric<StoredBool, M>,
/// Lazy complement of `is_long`.
pub is_short: LazyPerBlock<StoredBool>,
pub phase: LazyPerBlock<Option<CapitalSentimentPhase>, StoredU8>,
pub score: LazyPerBlock<Option<StoredI8>, Option<CapitalSentimentPhase>>,
pub is_short: LazyDailyMetric<StoredBool, StoredBool>,
pub phase: LazyDailyMetric<Option<CapitalSentimentPhase>, StoredU8>,
pub score: LazyDailyMetric<Option<StoredI8>, Option<CapitalSentimentPhase>>,
}
+2 -3
View File
@@ -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,
+4 -105
View File
@@ -26,10 +26,9 @@ impl<T: VecValue, SI: VecIndex> AggFold<Option<T>, 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<T: VecValue, SI: VecIndex> AggFold<Option<T>, 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<u8>);
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<String> {
Vec::new()
}
fn value_type_to_size_of(&self) -> usize {
size_of::<u8>()
}
fn value_type_to_string(&self) -> &'static str {
"u8"
}
}
impl ReadableVec<usize, u8> for TestVec {
fn read_into_at(&self, from: usize, to: usize, buf: &mut Vec<u8>) {
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, F: FnMut(B, u8) -> 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<B, E, F: FnMut(B, u8) -> Result<B, E>>(
&self,
from: usize,
to: usize,
init: B,
f: F,
) -> Result<B, E> {
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));
}
}