mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-08-04 22:53:05 -07:00
global: part X + 2
This commit is contained in:
+78
-33
@@ -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,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::*;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>>,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user