global: snapshot

This commit is contained in:
k
2024-10-26 16:41:38 +02:00
parent 7114c3bdf9
commit f5754780a8
30 changed files with 888 additions and 541 deletions

View File

@@ -24,6 +24,7 @@
- Added `Realized Profit To Loss Ratio` to all entities
- Added `Hash Price Min`
- Added `Hash Price Rebound`
- Removed all year datasets (25) in favor for epoch datasets (5), the former was too granular to be really useful
- Removed datasets split by liquidity for all datasets already split by any address kind, while fun to have, they took time to compute, ram, and space to store and no one was actually checking them
- Fixed a lot of values in split by liquidity datasets
@@ -43,7 +44,7 @@
- Added handling of SIGINT and SIGTERM terminal signals which menas you can now safely CTRL+C or kill the parser while it's exporting
- Added config print at the start of the program
- Compressed `empty_address_data` struct to save space (should shave of between up to 50% of the `address_index_to_empty_address_data` database)
- ~Doubled the number of `txid_to_tx_data` databases from 4096 to 8192~ If you ran with this you need to delete that database
- Doubled the number of `txid_to_tx_data` databases from 4096 to 8192
- Added `--recompute_computed true` argument, to allow recomputation of computed datasets in case of a bug
- Fixed not saved arguments, not being processed properly
- Fixed bug in `generic_map.multi_insert_simple_average`

View File

@@ -53,6 +53,8 @@ Please open an issue if you want to add another instance
## Structure
- `parser`: The backbone of the project, it does most of the work by parsing and then computing datasets from the timechain
- Takes 2 to 4 days to parse the whole chain and create all datasets
- If up to date wil take 1-3mn to compute the last 100 blocks
- `website`: A web app which displays the generated datasets in various charts
- `server`: A small server which will serve both the website and the computed datasets via an API
@@ -70,9 +72,12 @@ Please open an issue if you want to add another instance
### Requirements
- At least 16 GB of RAM
- At least 8 GB of RAM
- 1 TB of free space (will use 60-80% of that)
- A running instance of bitcoin-core (>= 28.0 **NOT** supported) with txindex=1 and rpc credentials
- A running instance of bitcoin-core with:
- `-txindex=1`
- `-blocksxor=0`
- RPC credentials
- Git
### Docker

View File

@@ -67,7 +67,7 @@ pub fn find_first_inserted_unsafe_height(
let inserted_last_date_is_older_than_saved_state = min_datasets_inserted_last_date.map_or(true, |min_datasets_last_date| min_datasets_last_date < last_safe_date);
if inserted_last_date_is_older_than_saved_state {
dbg!(min_datasets_inserted_last_date , *last_safe_date);
// dbg!(min_datasets_inserted_last_date , *last_safe_date);
return None;
}

View File

@@ -130,8 +130,9 @@ impl AddressIndexToAddressData {
impl AnyDatabaseGroup for AddressIndexToAddressData {
fn import() -> Self {
Self {
metadata: Metadata::import(&Self::full_path(), 1),
map: BTreeMap::default(),
metadata: Metadata::import(&Self::full_path()),
}
}

View File

@@ -90,8 +90,9 @@ impl AddressIndexToEmptyAddressData {
impl AnyDatabaseGroup for AddressIndexToEmptyAddressData {
fn import() -> Self {
Self {
metadata: Metadata::import(&Self::full_path(), 1),
map: BTreeMap::default(),
metadata: Metadata::import(&Self::full_path()),
}
}

View File

@@ -235,6 +235,8 @@ impl AddressToAddressIndex {
impl AnyDatabaseGroup for AddressToAddressIndex {
fn import() -> Self {
Self {
metadata: Metadata::import(&Self::full_path(), 1),
p2pk: BTreeMap::default(),
p2pkh: BTreeMap::default(),
p2sh: BTreeMap::default(),
@@ -246,7 +248,6 @@ impl AnyDatabaseGroup for AddressToAddressIndex {
unknown: None,
empty: None,
multisig: None,
metadata: Metadata::import(&Self::full_path()),
}
}

View File

@@ -1,5 +1,6 @@
use allocative::Allocative;
use bincode::{Decode, Encode};
use color_eyre::eyre::eyre;
use serde::{Deserialize, Serialize};
use std::{
fmt::Debug,
@@ -34,10 +35,10 @@ impl DerefMut for Metadata {
}
impl Metadata {
pub fn import(path: &str) -> Self {
pub fn import(path: &str, version: u16) -> Self {
Self {
path: path.to_owned(),
data: MetadataData::import(path).unwrap_or_default(),
data: MetadataData::import(path, version).unwrap_or_default(),
}
}
@@ -77,6 +78,7 @@ impl Metadata {
#[derive(Default, Debug, Encode, Decode, Serialize, Deserialize, Allocative)]
pub struct MetadataData {
version: u16,
pub serial: usize,
pub len: Counter,
pub last_height: Option<Height>,
@@ -93,10 +95,16 @@ impl MetadataData {
format!("{folder_path}/{name}")
}
pub fn import(path: &str) -> color_eyre::Result<Self> {
pub fn import(path: &str, version: u16) -> color_eyre::Result<Self> {
fs::create_dir_all(path)?;
Serialization::Binary.import(Path::new(&Self::full_path(path)))
let s: MetadataData = Serialization::Binary.import(Path::new(&Self::full_path(path)))?;
if s.version != version {
return Err(eyre!("Bad version"));
}
Ok(s)
}
pub fn export(&self, path: &str) -> color_eyre::Result<()> {

View File

@@ -114,15 +114,16 @@ impl TxidToTxData {
}
fn db_index(txid: &Txid) -> u16 {
((txid[0] as u16) << 4) + ((txid[1] as u16) >> 4)
((txid[0] as u16) << 5) + ((txid[1] as u16) >> 3)
}
}
impl AnyDatabaseGroup for TxidToTxData {
fn import() -> Self {
Self {
metadata: Metadata::import(&Self::full_path(), 2),
map: BTreeMap::default(),
metadata: Metadata::import(&Self::full_path()),
}
}

View File

@@ -89,8 +89,9 @@ impl TxoutIndexToAddressIndex {
impl AnyDatabaseGroup for TxoutIndexToAddressIndex {
fn import() -> Self {
Self {
metadata: Metadata::import(&Self::full_path(), 1),
map: BTreeMap::default(),
metadata: Metadata::import(&Self::full_path()),
}
}

View File

@@ -89,8 +89,9 @@ impl TxoutIndexToAmount {
impl AnyDatabaseGroup for TxoutIndexToAmount {
fn import() -> Self {
Self {
metadata: Metadata::import(&Self::full_path(), 1),
map: BTreeMap::default(),
metadata: Metadata::import(&Self::full_path()),
}
}

View File

@@ -149,7 +149,9 @@ impl AllDatasets {
}
pub fn insert(&mut self, insert_data: InsertData) {
self.address.insert(&insert_data);
if insert_data.compute_addresses {
self.address.insert(&insert_data);
}
self.utxo.insert(&insert_data);

View File

@@ -354,14 +354,14 @@ impl PriceDatasets {
dates,
&mut self.all_time_high.date,
|(value, date, _, map)| {
let high = self.high.date.get(date).unwrap();
let high = self.high.date.get_or_import(date).unwrap();
let is_ath = high == value;
if is_ath {
*date
} else {
let previous_date = date.checked_sub(1).unwrap();
*map.get(&previous_date).as_ref().unwrap_or(date)
*map.get_or_import(&previous_date).as_ref().unwrap_or(date)
}
},
);
@@ -399,7 +399,7 @@ impl PriceDatasets {
pub fn get_date_ohlc(&mut self, date: Date) -> color_eyre::Result<OHLC> {
if self.ohlc.date.is_key_safe(date) {
Ok(self.ohlc.date.get(&date).unwrap().to_owned())
Ok(self.ohlc.date.get_or_import(&date).unwrap().to_owned())
} else {
let ohlc = self
.get_from_daily_kraken(&date)
@@ -488,7 +488,7 @@ impl PriceDatasets {
timestamp: Timestamp,
previous_timestamp: Option<Timestamp>,
) -> color_eyre::Result<OHLC> {
if let Some(ohlc) = self.ohlc.height.get(&height) {
if let Some(ohlc) = self.ohlc.height.get_or_import(&height) {
return Ok(ohlc);
}

View File

@@ -60,7 +60,7 @@ impl Binary {
let config = config::standard();
let decoded = decode_from_slice::<T, _>(&decompressed, config).unwrap().0;
let decoded = decode_from_slice::<T, _>(&decompressed, config)?.0;
Ok(decoded)
}

View File

@@ -1,17 +1,19 @@
use crate::structs::{Epoch, Height};
pub enum UTXOFilter {
To(u32),
FromTo { from: u32, to: u32 },
From(u32),
Year(u32),
Epoch(Epoch),
}
impl UTXOCheck for UTXOFilter {
fn check(&self, days_old: &u32, year: &u32) -> bool {
fn check(&self, days_old: &u32, height: &Height) -> bool {
match self {
UTXOFilter::From(from) => from <= days_old,
UTXOFilter::To(to) => to > days_old,
UTXOFilter::FromTo { from, to } => from <= days_old && to > days_old,
UTXOFilter::Year(_year) => _year == year,
UTXOFilter::Epoch(epoch) => *epoch == height.into(),
}
}
@@ -20,13 +22,13 @@ impl UTXOCheck for UTXOFilter {
UTXOFilter::From(from) => from <= days_old,
UTXOFilter::To(to) => to > days_old,
UTXOFilter::FromTo { from, to } => from <= days_old && to > days_old,
UTXOFilter::Year(_) => unreachable!(),
UTXOFilter::Epoch(_) => unreachable!(),
}
}
}
pub trait UTXOCheck {
fn check(&self, days_old: &u32, year: &u32) -> bool;
fn check(&self, days_old: &u32, height: &Height) -> bool;
fn check_days_old(&self, days_old: &u32) -> bool;
}

View File

@@ -1,3 +1,5 @@
use crate::structs::Epoch;
use super::{SplitByUTXOCohort, UTXOFilter};
pub const UTXO_FILTERS: SplitByUTXOCohort<UTXOFilter> = SplitByUTXOCohort {
@@ -62,22 +64,11 @@ pub const UTXO_FILTERS: SplitByUTXOCohort<UTXOFilter> = SplitByUTXOCohort {
from_10y: UTXOFilter::From(10 * 365),
from_15y: UTXOFilter::From(15 * 365),
year_2009: UTXOFilter::Year(2009),
year_2010: UTXOFilter::Year(2010),
year_2011: UTXOFilter::Year(2011),
year_2012: UTXOFilter::Year(2012),
year_2013: UTXOFilter::Year(2013),
year_2014: UTXOFilter::Year(2014),
year_2015: UTXOFilter::Year(2015),
year_2016: UTXOFilter::Year(2016),
year_2017: UTXOFilter::Year(2017),
year_2018: UTXOFilter::Year(2018),
year_2019: UTXOFilter::Year(2019),
year_2020: UTXOFilter::Year(2020),
year_2021: UTXOFilter::Year(2021),
year_2022: UTXOFilter::Year(2022),
year_2023: UTXOFilter::Year(2023),
year_2024: UTXOFilter::Year(2024),
epoch_1: UTXOFilter::Epoch(Epoch(1)),
epoch_2: UTXOFilter::Epoch(Epoch(2)),
epoch_3: UTXOFilter::Epoch(Epoch(3)),
epoch_4: UTXOFilter::Epoch(Epoch(4)),
epoch_5: UTXOFilter::Epoch(Epoch(5)),
sth: UTXOFilter::To(155),
lth: UTXOFilter::From(155),

View File

@@ -37,22 +37,11 @@ pub enum UTXOCohortId {
From10y,
From15y,
Year2009,
Year2010,
Year2011,
Year2012,
Year2013,
Year2014,
Year2015,
Year2016,
Year2017,
Year2018,
Year2019,
Year2020,
Year2021,
Year2022,
Year2023,
Year2024,
Epoch1,
Epoch2,
Epoch3,
Epoch4,
Epoch5,
ShortTermHolders,
LongTermHolders,
@@ -95,22 +84,11 @@ impl UTXOCohortId {
Self::From10y => "from_10y",
Self::From15y => "from_15y",
Self::Year2009 => "year_2009",
Self::Year2010 => "year_2010",
Self::Year2011 => "year_2011",
Self::Year2012 => "year_2012",
Self::Year2013 => "year_2013",
Self::Year2014 => "year_2014",
Self::Year2015 => "year_2015",
Self::Year2016 => "year_2016",
Self::Year2017 => "year_2017",
Self::Year2018 => "year_2018",
Self::Year2019 => "year_2019",
Self::Year2020 => "year_2020",
Self::Year2021 => "year_2021",
Self::Year2022 => "year_2022",
Self::Year2023 => "year_2023",
Self::Year2024 => "year_2024",
Self::Epoch1 => "epoch_1",
Self::Epoch2 => "epoch_2",
Self::Epoch3 => "epoch_3",
Self::Epoch4 => "epoch_4",
Self::Epoch5 => "epoch_5",
Self::ShortTermHolders => "sth",
Self::LongTermHolders => "lth",

View File

@@ -1,5 +1,4 @@
use allocative::Allocative;
use chrono::Datelike;
use derive_deref::{Deref, DerefMut};
use rayon::prelude::*;
@@ -19,8 +18,6 @@ impl UTXOCohortsDurableStates {
if let Some(last_block_data) = date_data_vec.last_block() {
date_data_vec.iter().for_each(|date_data| {
let year = date_data.date.year() as u32;
date_data.blocks.iter().for_each(|block_data| {
let amount = block_data.amount;
let utxo_count = block_data.utxos as f64;
@@ -35,7 +32,7 @@ impl UTXOCohortsDurableStates {
last_block_data.timestamp,
);
s.initial_filtered_apply(&increment_days_old, &year, |state| {
s.initial_filtered_apply(&increment_days_old, &block_data.height, |state| {
state
.increment(amount, utxo_count, block_data.price)
.unwrap();
@@ -53,6 +50,7 @@ impl UTXOCohortsDurableStates {
last_block_data: &BlockData,
previous_last_block_data: Option<&BlockData>,
) {
let height = block_data.height;
let amount = block_data.amount;
let utxo_count = block_data.utxos as f64;
let price = block_data.price;
@@ -63,9 +61,7 @@ impl UTXOCohortsDurableStates {
}
if block_data.height == last_block_data.height {
let year = block_data.timestamp.to_year();
self.initial_filtered_apply(&0, &year, |state| {
self.initial_filtered_apply(&0, &height, |state| {
state.increment(amount, utxo_count, price).unwrap();
})
} else {
@@ -120,9 +116,7 @@ impl UTXOCohortsDurableStates {
previous_last_block_data.timestamp,
);
let year = block_data.timestamp.to_year();
self.initial_filtered_apply(&days_old, &year, |state| {
self.initial_filtered_apply(&days_old, &block_data.height, |state| {
state
.decrement(amount, utxo_count, block_data.price)
.unwrap_or_else(|report| {

View File

@@ -1,6 +1,5 @@
use std::{cmp::Ordering, collections::BTreeMap};
use chrono::Datelike;
use derive_deref::{Deref, DerefMut};
use crate::{
@@ -33,8 +32,6 @@ impl UTXOCohortsSentStates {
.for_each(|(block_path, sent_data)| {
let date_data = date_data_vec.get_date_data(block_path).unwrap();
let year = date_data.date.year() as u32;
let block_data = date_data.get_block_data(block_path).unwrap();
let days_old = Timestamp::difference_in_days_between(
@@ -44,10 +41,11 @@ impl UTXOCohortsSentStates {
let previous_timestamp = block_data.timestamp;
let previous_price = block_data.price;
let height = block_data.height;
let amount_sent = sent_data.volume;
self.initial_filtered_apply(&days_old, &year, |state| {
self.initial_filtered_apply(&days_old, &height, |state| {
state.input.iterate(sent_data.count as f64, amount_sent);
let previous_value = previous_price * amount_sent;

View File

@@ -2,6 +2,8 @@ use allocative::Allocative;
use super::{UTXOCheck, UTXOCohortId, UTXO_FILTERS};
use crate::structs::Height;
#[derive(Default, Allocative)]
pub struct SplitByUTXOCohort<T> {
pub sth: T,
@@ -41,22 +43,11 @@ pub struct SplitByUTXOCohort<T> {
pub from_10y: T,
pub from_15y: T,
pub year_2009: T,
pub year_2010: T,
pub year_2011: T,
pub year_2012: T,
pub year_2013: T,
pub year_2014: T,
pub year_2015: T,
pub year_2016: T,
pub year_2017: T,
pub year_2018: T,
pub year_2019: T,
pub year_2020: T,
pub year_2021: T,
pub year_2022: T,
pub year_2023: T,
pub year_2024: T,
pub epoch_1: T,
pub epoch_2: T,
pub epoch_3: T,
pub epoch_4: T,
pub epoch_5: T,
}
impl<T> SplitByUTXOCohort<T> {
@@ -93,22 +84,11 @@ impl<T> SplitByUTXOCohort<T> {
UTXOCohortId::From4y => &self.from_4y,
UTXOCohortId::From10y => &self.from_10y,
UTXOCohortId::From15y => &self.from_15y,
UTXOCohortId::Year2009 => &self.year_2009,
UTXOCohortId::Year2010 => &self.year_2010,
UTXOCohortId::Year2011 => &self.year_2011,
UTXOCohortId::Year2012 => &self.year_2012,
UTXOCohortId::Year2013 => &self.year_2013,
UTXOCohortId::Year2014 => &self.year_2014,
UTXOCohortId::Year2015 => &self.year_2015,
UTXOCohortId::Year2016 => &self.year_2016,
UTXOCohortId::Year2017 => &self.year_2017,
UTXOCohortId::Year2018 => &self.year_2018,
UTXOCohortId::Year2019 => &self.year_2019,
UTXOCohortId::Year2020 => &self.year_2020,
UTXOCohortId::Year2021 => &self.year_2021,
UTXOCohortId::Year2022 => &self.year_2022,
UTXOCohortId::Year2023 => &self.year_2023,
UTXOCohortId::Year2024 => &self.year_2024,
UTXOCohortId::Epoch1 => &self.epoch_1,
UTXOCohortId::Epoch2 => &self.epoch_2,
UTXOCohortId::Epoch3 => &self.epoch_3,
UTXOCohortId::Epoch4 => &self.epoch_4,
UTXOCohortId::Epoch5 => &self.epoch_5,
UTXOCohortId::ShortTermHolders => &self.sth,
UTXOCohortId::LongTermHolders => &self.lth,
}
@@ -147,28 +127,17 @@ impl<T> SplitByUTXOCohort<T> {
UTXOCohortId::From4y => &mut self.from_4y,
UTXOCohortId::From10y => &mut self.from_10y,
UTXOCohortId::From15y => &mut self.from_15y,
UTXOCohortId::Year2009 => &mut self.year_2009,
UTXOCohortId::Year2010 => &mut self.year_2010,
UTXOCohortId::Year2011 => &mut self.year_2011,
UTXOCohortId::Year2012 => &mut self.year_2012,
UTXOCohortId::Year2013 => &mut self.year_2013,
UTXOCohortId::Year2014 => &mut self.year_2014,
UTXOCohortId::Year2015 => &mut self.year_2015,
UTXOCohortId::Year2016 => &mut self.year_2016,
UTXOCohortId::Year2017 => &mut self.year_2017,
UTXOCohortId::Year2018 => &mut self.year_2018,
UTXOCohortId::Year2019 => &mut self.year_2019,
UTXOCohortId::Year2020 => &mut self.year_2020,
UTXOCohortId::Year2021 => &mut self.year_2021,
UTXOCohortId::Year2022 => &mut self.year_2022,
UTXOCohortId::Year2023 => &mut self.year_2023,
UTXOCohortId::Year2024 => &mut self.year_2024,
UTXOCohortId::Epoch1 => &mut self.epoch_1,
UTXOCohortId::Epoch2 => &mut self.epoch_2,
UTXOCohortId::Epoch3 => &mut self.epoch_3,
UTXOCohortId::Epoch4 => &mut self.epoch_4,
UTXOCohortId::Epoch5 => &mut self.epoch_5,
UTXOCohortId::ShortTermHolders => &mut self.sth,
UTXOCohortId::LongTermHolders => &mut self.lth,
}
}
/// Excluding years since they're static
/// Excluding epochs since they're static
pub fn duo_filtered_apply(
&mut self,
current_days_old: &u32,
@@ -455,175 +424,158 @@ impl<T> SplitByUTXOCohort<T> {
}
}
/// Includes years since it's the initial apply
pub fn initial_filtered_apply(&mut self, days_old: &u32, year: &u32, apply: impl Fn(&mut T)) {
if UTXO_FILTERS.up_to_1d.check(days_old, year) {
/// Includes epochs since it's the initial apply
pub fn initial_filtered_apply(
&mut self,
days_old: &u32,
height: &Height,
apply: impl Fn(&mut T),
) {
if UTXO_FILTERS.up_to_1d.check(days_old, height) {
apply(&mut self.up_to_1d);
} else if UTXO_FILTERS.from_1d_to_1w.check(days_old, year) {
} else if UTXO_FILTERS.from_1d_to_1w.check(days_old, height) {
apply(&mut self.from_1d_to_1w);
} else if UTXO_FILTERS.from_1w_to_1m.check(days_old, year) {
} else if UTXO_FILTERS.from_1w_to_1m.check(days_old, height) {
apply(&mut self.from_1w_to_1m);
} else if UTXO_FILTERS.from_1m_to_3m.check(days_old, year) {
} else if UTXO_FILTERS.from_1m_to_3m.check(days_old, height) {
apply(&mut self.from_1m_to_3m);
} else if UTXO_FILTERS.from_3m_to_6m.check(days_old, year) {
} else if UTXO_FILTERS.from_3m_to_6m.check(days_old, height) {
apply(&mut self.from_3m_to_6m);
} else if UTXO_FILTERS.from_6m_to_1y.check(days_old, year) {
} else if UTXO_FILTERS.from_6m_to_1y.check(days_old, height) {
apply(&mut self.from_6m_to_1y);
} else if UTXO_FILTERS.from_1y_to_2y.check(days_old, year) {
} else if UTXO_FILTERS.from_1y_to_2y.check(days_old, height) {
apply(&mut self.from_1y_to_2y);
} else if UTXO_FILTERS.from_2y_to_3y.check(days_old, year) {
} else if UTXO_FILTERS.from_2y_to_3y.check(days_old, height) {
apply(&mut self.from_2y_to_3y);
} else if UTXO_FILTERS.from_3y_to_5y.check(days_old, year) {
} else if UTXO_FILTERS.from_3y_to_5y.check(days_old, height) {
apply(&mut self.from_3y_to_5y);
} else if UTXO_FILTERS.from_5y_to_7y.check(days_old, year) {
} else if UTXO_FILTERS.from_5y_to_7y.check(days_old, height) {
apply(&mut self.from_5y_to_7y);
} else if UTXO_FILTERS.from_7y_to_10y.check(days_old, year) {
} else if UTXO_FILTERS.from_7y_to_10y.check(days_old, height) {
apply(&mut self.from_7y_to_10y);
} else if UTXO_FILTERS.from_10y_to_15y.check(days_old, year) {
} else if UTXO_FILTERS.from_10y_to_15y.check(days_old, height) {
apply(&mut self.from_10y_to_15y);
}
if UTXO_FILTERS.year_2009.check(days_old, year) {
apply(&mut self.year_2009);
} else if UTXO_FILTERS.year_2010.check(days_old, year) {
apply(&mut self.year_2010);
} else if UTXO_FILTERS.year_2011.check(days_old, year) {
apply(&mut self.year_2011);
} else if UTXO_FILTERS.year_2012.check(days_old, year) {
apply(&mut self.year_2012);
} else if UTXO_FILTERS.year_2013.check(days_old, year) {
apply(&mut self.year_2013);
} else if UTXO_FILTERS.year_2014.check(days_old, year) {
apply(&mut self.year_2014);
} else if UTXO_FILTERS.year_2015.check(days_old, year) {
apply(&mut self.year_2015);
} else if UTXO_FILTERS.year_2016.check(days_old, year) {
apply(&mut self.year_2016);
} else if UTXO_FILTERS.year_2017.check(days_old, year) {
apply(&mut self.year_2017);
} else if UTXO_FILTERS.year_2018.check(days_old, year) {
apply(&mut self.year_2018);
} else if UTXO_FILTERS.year_2019.check(days_old, year) {
apply(&mut self.year_2019);
} else if UTXO_FILTERS.year_2020.check(days_old, year) {
apply(&mut self.year_2020);
} else if UTXO_FILTERS.year_2021.check(days_old, year) {
apply(&mut self.year_2021);
} else if UTXO_FILTERS.year_2022.check(days_old, year) {
apply(&mut self.year_2022);
} else if UTXO_FILTERS.year_2023.check(days_old, year) {
apply(&mut self.year_2023);
} else if UTXO_FILTERS.year_2024.check(days_old, year) {
apply(&mut self.year_2024);
if UTXO_FILTERS.epoch_1.check(days_old, height) {
apply(&mut self.epoch_1);
} else if UTXO_FILTERS.epoch_2.check(days_old, height) {
apply(&mut self.epoch_2);
} else if UTXO_FILTERS.epoch_3.check(days_old, height) {
apply(&mut self.epoch_3);
} else if UTXO_FILTERS.epoch_4.check(days_old, height) {
apply(&mut self.epoch_4);
} else if UTXO_FILTERS.epoch_5.check(days_old, height) {
apply(&mut self.epoch_5);
}
if UTXO_FILTERS.sth.check(days_old, year) {
if UTXO_FILTERS.sth.check(days_old, height) {
apply(&mut self.sth);
} else if UTXO_FILTERS.lth.check(days_old, year) {
} else if UTXO_FILTERS.lth.check(days_old, height) {
apply(&mut self.lth);
} else {
unreachable!()
}
if UTXO_FILTERS.from_1y.check(days_old, year) {
if UTXO_FILTERS.from_1y.check(days_old, height) {
apply(&mut self.from_1y);
}
if UTXO_FILTERS.from_2y.check(days_old, year) {
if UTXO_FILTERS.from_2y.check(days_old, height) {
apply(&mut self.from_2y);
}
if UTXO_FILTERS.from_4y.check(days_old, year) {
if UTXO_FILTERS.from_4y.check(days_old, height) {
apply(&mut self.from_4y);
}
if UTXO_FILTERS.from_10y.check(days_old, year) {
if UTXO_FILTERS.from_10y.check(days_old, height) {
apply(&mut self.from_10y);
}
if UTXO_FILTERS.from_15y.check(days_old, year) {
if UTXO_FILTERS.from_15y.check(days_old, height) {
apply(&mut self.from_15y);
}
if UTXO_FILTERS.up_to_15y.check(days_old, year) {
if UTXO_FILTERS.up_to_15y.check(days_old, height) {
apply(&mut self.up_to_15y);
} else {
return;
}
if UTXO_FILTERS.up_to_10y.check(days_old, year) {
if UTXO_FILTERS.up_to_10y.check(days_old, height) {
apply(&mut self.up_to_10y);
} else {
return;
}
if UTXO_FILTERS.up_to_7y.check(days_old, year) {
if UTXO_FILTERS.up_to_7y.check(days_old, height) {
apply(&mut self.up_to_7y);
} else {
return;
}
if UTXO_FILTERS.up_to_5y.check(days_old, year) {
if UTXO_FILTERS.up_to_5y.check(days_old, height) {
apply(&mut self.up_to_5y);
} else {
return;
}
if UTXO_FILTERS.up_to_3y.check(days_old, year) {
if UTXO_FILTERS.up_to_3y.check(days_old, height) {
apply(&mut self.up_to_3y);
} else {
return;
}
if UTXO_FILTERS.up_to_2y.check(days_old, year) {
if UTXO_FILTERS.up_to_2y.check(days_old, height) {
apply(&mut self.up_to_2y);
} else {
return;
}
if UTXO_FILTERS.up_to_1y.check(days_old, year) {
if UTXO_FILTERS.up_to_1y.check(days_old, height) {
apply(&mut self.up_to_1y);
} else {
return;
}
if UTXO_FILTERS.up_to_6m.check(days_old, year) {
if UTXO_FILTERS.up_to_6m.check(days_old, height) {
apply(&mut self.up_to_6m);
} else {
return;
}
if UTXO_FILTERS.up_to_5m.check(days_old, year) {
if UTXO_FILTERS.up_to_5m.check(days_old, height) {
apply(&mut self.up_to_5m);
} else {
return;
}
if UTXO_FILTERS.up_to_4m.check(days_old, year) {
if UTXO_FILTERS.up_to_4m.check(days_old, height) {
apply(&mut self.up_to_4m);
} else {
return;
}
if UTXO_FILTERS.up_to_3m.check(days_old, year) {
if UTXO_FILTERS.up_to_3m.check(days_old, height) {
apply(&mut self.up_to_3m);
} else {
return;
}
if UTXO_FILTERS.up_to_2m.check(days_old, year) {
if UTXO_FILTERS.up_to_2m.check(days_old, height) {
apply(&mut self.up_to_2m);
} else {
return;
}
if UTXO_FILTERS.up_to_1m.check(days_old, year) {
if UTXO_FILTERS.up_to_1m.check(days_old, height) {
apply(&mut self.up_to_1m);
} else {
return;
}
if UTXO_FILTERS.up_to_1w.check(days_old, year) {
if UTXO_FILTERS.up_to_1w.check(days_old, height) {
apply(&mut self.up_to_1w);
}
}
@@ -662,22 +614,11 @@ impl<T> SplitByUTXOCohort<T> {
(&self.from_4y, UTXOCohortId::From4y),
(&self.from_10y, UTXOCohortId::From10y),
(&self.from_15y, UTXOCohortId::From15y),
(&self.year_2009, UTXOCohortId::Year2009),
(&self.year_2010, UTXOCohortId::Year2010),
(&self.year_2011, UTXOCohortId::Year2011),
(&self.year_2012, UTXOCohortId::Year2012),
(&self.year_2013, UTXOCohortId::Year2013),
(&self.year_2014, UTXOCohortId::Year2014),
(&self.year_2015, UTXOCohortId::Year2015),
(&self.year_2016, UTXOCohortId::Year2016),
(&self.year_2017, UTXOCohortId::Year2017),
(&self.year_2018, UTXOCohortId::Year2018),
(&self.year_2019, UTXOCohortId::Year2019),
(&self.year_2020, UTXOCohortId::Year2020),
(&self.year_2021, UTXOCohortId::Year2021),
(&self.year_2022, UTXOCohortId::Year2022),
(&self.year_2023, UTXOCohortId::Year2023),
(&self.year_2024, UTXOCohortId::Year2024),
(&self.epoch_1, UTXOCohortId::Epoch1),
(&self.epoch_2, UTXOCohortId::Epoch2),
(&self.epoch_3, UTXOCohortId::Epoch3),
(&self.epoch_4, UTXOCohortId::Epoch4),
(&self.epoch_5, UTXOCohortId::Epoch5),
(&self.sth, UTXOCohortId::ShortTermHolders),
(&self.lth, UTXOCohortId::LongTermHolders),
]
@@ -717,22 +658,11 @@ impl<T> SplitByUTXOCohort<T> {
(&mut self.from_4y, UTXOCohortId::From4y),
(&mut self.from_10y, UTXOCohortId::From10y),
(&mut self.from_15y, UTXOCohortId::From15y),
(&mut self.year_2009, UTXOCohortId::Year2009),
(&mut self.year_2010, UTXOCohortId::Year2010),
(&mut self.year_2011, UTXOCohortId::Year2011),
(&mut self.year_2012, UTXOCohortId::Year2012),
(&mut self.year_2013, UTXOCohortId::Year2013),
(&mut self.year_2014, UTXOCohortId::Year2014),
(&mut self.year_2015, UTXOCohortId::Year2015),
(&mut self.year_2016, UTXOCohortId::Year2016),
(&mut self.year_2017, UTXOCohortId::Year2017),
(&mut self.year_2018, UTXOCohortId::Year2018),
(&mut self.year_2019, UTXOCohortId::Year2019),
(&mut self.year_2020, UTXOCohortId::Year2020),
(&mut self.year_2021, UTXOCohortId::Year2021),
(&mut self.year_2022, UTXOCohortId::Year2022),
(&mut self.year_2023, UTXOCohortId::Year2023),
(&mut self.year_2024, UTXOCohortId::Year2024),
(&mut self.epoch_1, UTXOCohortId::Epoch1),
(&mut self.epoch_2, UTXOCohortId::Epoch2),
(&mut self.epoch_3, UTXOCohortId::Epoch3),
(&mut self.epoch_4, UTXOCohortId::Epoch4),
(&mut self.epoch_5, UTXOCohortId::Epoch5),
(&mut self.sth, UTXOCohortId::ShortTermHolders),
(&mut self.lth, UTXOCohortId::LongTermHolders),
]

View File

@@ -0,0 +1,20 @@
use super::{Height, MapKey};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Epoch(pub u16);
impl Epoch {
pub const BLOCKS_PER_EPOCH: usize = 210_000;
}
impl From<Height> for Epoch {
fn from(height: Height) -> Self {
Self(((height.to_usize() / Self::BLOCKS_PER_EPOCH) + 1) as u16)
}
}
impl From<&Height> for Epoch {
fn from(height: &Height) -> Self {
Self(((height.to_usize() / Self::BLOCKS_PER_EPOCH) + 1) as u16)
}
}

View File

@@ -202,10 +202,12 @@ where
}
pub fn insert(&mut self, key: Key, value: Value) -> Value {
self.to_insert
.entry(key.to_chunk_id())
.or_default()
.insert(key.to_serialized_key(), value);
if !self.is_key_safe(key) {
self.to_insert
.entry(key.to_chunk_id())
.or_default()
.insert(key.to_serialized_key(), value);
}
value
}
@@ -443,15 +445,14 @@ where
SourceValue,
&Key,
&mut GenericMap<Key, SourceValue, ChunkId, SourceSerialized>,
&Self,
&mut Self,
),
) -> Value,
{
keys.iter().for_each(|key| {
self.insert(
*key,
transform((source.get_or_import(key).unwrap(), key, source, self)),
);
let value = transform((source.get_or_import(key).unwrap(), key, source, self));
self.insert(*key, value);
});
}

View File

@@ -17,6 +17,7 @@ mod date_data;
mod date_map;
mod date_map_chunk_id;
mod empty_address_data;
mod epoch;
mod exit;
mod generic_map;
mod height;
@@ -53,6 +54,7 @@ pub use date_data::*;
pub use date_map::*;
pub use date_map_chunk_id::*;
pub use empty_address_data::*;
pub use epoch::*;
pub use exit::*;
pub use generic_map::*;
pub use height::*;

View File

@@ -795,6 +795,11 @@
color: var(--off-color);
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
h4 + & {
font-size: var(--font-size-sm);
line-height: var(--line-height-sm);
}
}
span {
@@ -960,6 +965,14 @@
details[open] > summary > & {
background-color: var(--background-color);
}
summary:hover > & {
border-color: var(--orange) !important;
}
details:not([open]) > summary:hover > & {
background-color: var(--orange);
}
}
> small {
@@ -2041,79 +2054,7 @@
<!-- </div> -->
</div>
</div>
<div id="simulation" hidden>
<div>
<div>
<h4>Amount</h4>
<div>Initial</div>
<input
id="simulation-amount-initial"
type="number"
placeholder="US Dollars"
min="0"
/>
<input
type="range"
id="simulation-amount-initial-range"
title="Initially invested"
min="0"
max="100"
/>
<div>Recurrent</div>
<input
id="simulation-amount-recurrent"
type="number"
placeholder="US Dollars"
min="0"
/>
</div>
<hr />
<div>
<h4>Frequency</h4>
<ul>
<li>Daily</li>
<li>Weekly</li>
<li>Monthly</li>
</ul>
<ul id="simulation-frequency-choices">
<li>Monday</li>
<li>Tuesday</li>
<li>Wednesday</li>
<li>Thursday</li>
<li>Friday</li>
<li>Saturday</li>
<li>Sunday</li>
</ul>
</div>
<hr />
<div>
<h4>Interval</h4>
<div>
<input type="date" value="2021-04-15" />
<button>Reset</button>
</div>
<div>
<input type="date" value="2024-04-15" />
<button>Reset</button>
</div>
</div>
<hr />
<div>
<h4>Fees</h4>
<input type="number" value="0.025" />
</div>
<hr />
<div>
<h4>Strategy</h4>
<ul id="simulation-strategy">
<li>All in</li>
<li>Weighted Local</li>
<li>Weighted Cycle</li>
</ul>
</div>
</div>
<div></div>
</div>
<div id="simulation" hidden></div>
</section>
</aside>
<div id="share-div" hidden>

View File

@@ -262,7 +262,7 @@ export function init({
),
);
const field = utils.dom.createField({
const field = utils.dom.createHorizontalChoiceField({
choices: chartModes,
selected: chartMode(),
id,
@@ -326,8 +326,8 @@ export function init({
ratio =
utils.getNumberOfDaysBetweenTwoDates(
utils.dateFromTime(from),
utils.dateFromTime(to),
utils.date.fromTime(from),
utils.date.fromTime(to),
) / width;
} else {
const to = /** @type {number} */ (visibleTimeRange.to);
@@ -963,7 +963,7 @@ export function init({
let number;
if (scale === "date") {
const date = utils.dateFromTime(data.time);
const date = utils.date.fromTime(data.time);
number = date.getTime();

View File

@@ -30,13 +30,55 @@ function initPackages() {
/**
* @template T
* @param {T} initialValue
* @param {SignalOptions<T>} [options]
* @param {SignalOptions<T> & {save?: {id?: string; param?: string; serialize: (v: NonNullable<T>) => string; deserialize: (v: string) => NonNullable<T>}}} [options]
* @returns {Signal<T>}
*/
createSignal(initialValue, options) {
const [get, set] = this.createSolidSignal(initialValue, options);
// @ts-ignore
get.set = set;
if (options?.save) {
const save = options.save;
let serialized = null;
if (save.param) {
serialized = utils.url.readParam(save.param);
}
if (serialized === null && save.id) {
serialized = utils.storage.read(save.id);
}
if (serialized) {
set(save.deserialize(serialized));
}
let firstEffect = true;
this.createEffect(() => {
const value = get();
if (!save) return;
if (!firstEffect && save.id) {
if (value !== undefined && value !== null) {
localStorage.setItem(save.id, save.serialize(value));
} else {
localStorage.removeItem(save.id);
}
}
if (save.param) {
if (value !== undefined && value !== null) {
utils.url.writeParam(save.param, save.serialize(value));
} else {
utils.url.removeParam(save.param);
}
}
firstEffect = false;
});
}
// @ts-ignore
return get;
},
@@ -434,7 +476,7 @@ function initPackages() {
whitespaceStartDateDate + i,
);
const time = utils.dateToString(date);
const time = utils.date.toString(date);
if (i === whitespaceDateDataset.length - 1) {
whitespaceDateDataset[i] = {
@@ -859,7 +901,7 @@ const utils = {
* @param {string} args.selected
* @param {{createEffect: CreateEffect}} args.signals
*/
createField({ title, id, choices, selected, signals }) {
createHorizontalChoiceField({ title, id, choices, selected, signals }) {
const field = window.document.createElement("div");
field.classList.add("field");
@@ -941,12 +983,12 @@ const utils = {
},
/**
* @param {string} key
* @param {string | boolean | undefined} value
* @param {string | boolean | null | undefined} value
*/
writeParam(key, value) {
const urlParams = new URLSearchParams(window.location.search);
if (value !== undefined) {
if (value !== null && value !== undefined) {
urlParams.set(key, String(value));
} else {
urlParams.delete(key);
@@ -966,9 +1008,7 @@ const utils = {
* @returns {boolean | null}
*/
readBoolParam(key) {
const urlParams = new URLSearchParams(window.location.search);
const parameter = urlParams.get(key);
const parameter = this.readParam(key);
if (parameter) {
return utils.isSerializedBooleanTrue(parameter);
@@ -976,6 +1016,29 @@ const utils = {
return null;
},
/**
*
* @param {string} key
* @returns {number | null}
*/
readNumberParam(key) {
const parameter = this.readParam(key);
if (parameter) {
return Number(parameter);
}
return null;
},
/**
*
* @param {string} key
* @returns {string | null}
*/
readParam(key) {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(key);
},
pathnameToSelectedId() {
return window.document.location.pathname.substring(1);
},
@@ -1044,11 +1107,21 @@ const utils = {
},
},
storage: {
/**
* @param {string} key
*/
readNumber(key) {
const saved = this.read(key);
if (saved) {
return Number(saved);
}
return null;
},
/**
* @param {string} key
*/
readBool(key) {
const saved = localStorage.getItem(key);
const saved = this.read(key);
if (saved) {
return utils.isSerializedBooleanTrue(saved);
}
@@ -1056,7 +1129,13 @@ const utils = {
},
/**
* @param {string} key
* @param {string | boolean | undefined} value
*/
read(key) {
return localStorage.getItem(key);
},
/**
* @param {string} key
* @param {string | boolean | null | undefined} value
*/
write(key, value) {
value !== undefined && value !== null
@@ -1070,6 +1149,22 @@ const utils = {
this.write(key, undefined);
},
},
serde: {
number: {
/**
* @param {number} v
*/
serialize(v) {
return String(v);
},
/**
* @param {string} v
*/
deserialize(v) {
return Number(v);
},
},
},
formatters: {
dollars: new Intl.NumberFormat("en-US", {
style: "currency",
@@ -1083,6 +1178,37 @@ const utils = {
maximumFractionDigits: 2,
}),
},
date: {
todayUTC() {
const today = new Date();
return new Date(
Date.UTC(
today.getUTCFullYear(),
today.getUTCMonth(),
today.getUTCDate(),
0,
0,
0,
),
);
},
/**
* @param {Date} date
* @returns {string}
*/
toString(date) {
return date.toJSON().split("T")[0];
},
/**
* @param {Time} time
*/
fromTime(time) {
return typeof time === "string"
? new Date(time)
: // @ts-ignore
new Date(time.year, time.month, time.day);
},
},
/**
*
* @template {(...args: any[]) => any} F
@@ -1118,22 +1244,6 @@ const utils = {
setTimeout(callback, timeout);
}
},
/**
* @param {Date} date
* @returns {string}
*/
dateToString(date) {
return date.toJSON().split("T")[0];
},
/**
* @param {Time} time
*/
dateFromTime(time) {
return typeof time === "string"
? new Date(time)
: // @ts-ignore
new Date(time.year, time.month, time.day);
},
/**
* @param {Date} oldest
* @param {Date} youngest
@@ -1186,9 +1296,9 @@ const env = initEnv();
function createConstants() {
const ONE_SECOND_IN_MS = 1_000;
const FIVE_SECOND_IN_MS = 5 * ONE_SECOND_IN_MS;
const TEN_SECOND_IN_MS = 2 * FIVE_SECOND_IN_MS;
const ONE_MINUTE_IN_MS = 6 * TEN_SECOND_IN_MS;
const FIVE_SECONDS_IN_MS = 5 * ONE_SECOND_IN_MS;
const TEN_SECONDS_IN_MS = 2 * FIVE_SECONDS_IN_MS;
const ONE_MINUTE_IN_MS = 6 * TEN_SECONDS_IN_MS;
const FIVE_MINUTES_IN_MS = 5 * ONE_MINUTE_IN_MS;
const TEN_MINUTES_IN_MS = 2 * FIVE_MINUTES_IN_MS;
const ONE_HOUR_IN_MS = 6 * TEN_MINUTES_IN_MS;
@@ -1200,8 +1310,8 @@ function createConstants() {
return {
ONE_SECOND_IN_MS,
FIVE_SECOND_IN_MS,
TEN_SECOND_IN_MS,
FIVE_SECONDS_IN_MS,
TEN_SECONDS_IN_MS,
ONE_MINUTE_IN_MS,
FIVE_MINUTES_IN_MS,
TEN_MINUTES_IN_MS,
@@ -1596,22 +1706,11 @@ function createColors(dark) {
probability0_1p: red,
probability0_5p: orange,
probability1p: yellow,
year_2009: yellow,
year_2010: yellow,
year_2011: yellow,
year_2012: yellow,
year_2013: yellow,
year_2014: yellow,
year_2015: yellow,
year_2016: yellow,
year_2017: yellow,
year_2018: yellow,
year_2019: yellow,
year_2020: yellow,
year_2021: yellow,
year_2022: yellow,
year_2023: yellow,
year_2024: yellow,
epoch_1: red,
epoch_2: orange,
epoch_3: yellow,
epoch_4: green,
epoch_5: blue,
};
}
/**
@@ -2059,7 +2158,7 @@ function initWebSockets(signals) {
const date = new Date(Number(timestamp) * 1000);
const dateStr = utils.dateToString(date);
const dateStr = utils.date.toString(date);
/** @type {DatasetCandlestickData} */
const candle = {
@@ -2121,7 +2220,7 @@ packages.signals().then((signals) =>
});
}
fetchLastHeight();
setInterval(fetchLastHeight, consts.TEN_SECOND_IN_MS, {});
setInterval(fetchLastHeight, consts.TEN_SECONDS_IN_MS, {});
return lastHeight;
}

View File

@@ -217,23 +217,12 @@ function createPartialOptions(colors) {
},
]);
const year = /** @type {const} */ ([
{ id: "year-2009", key: "year_2009", name: "2009" },
{ id: "year-2010", key: "year_2010", name: "2010" },
{ id: "year-2011", key: "year_2011", name: "2011" },
{ id: "year-2012", key: "year_2012", name: "2012" },
{ id: "year-2013", key: "year_2013", name: "2013" },
{ id: "year-2014", key: "year_2014", name: "2014" },
{ id: "year-2015", key: "year_2015", name: "2015" },
{ id: "year-2016", key: "year_2016", name: "2016" },
{ id: "year-2017", key: "year_2017", name: "2017" },
{ id: "year-2018", key: "year_2018", name: "2018" },
{ id: "year-2019", key: "year_2019", name: "2019" },
{ id: "year-2020", key: "year_2020", name: "2020" },
{ id: "year-2021", key: "year_2021", name: "2021" },
{ id: "year-2022", key: "year_2022", name: "2022" },
{ id: "year-2023", key: "year_2023", name: "2023" },
{ id: "year-2024", key: "year_2024", name: "2024" },
const epochs = /** @type {const} */ ([
{ id: "epoch-1", key: "epoch_1", name: "1" },
{ id: "epoch-2", key: "epoch_2", name: "2" },
{ id: "epoch-3", key: "epoch_3", name: "3" },
{ id: "epoch-4", key: "epoch_4", name: "4" },
{ id: "epoch-5", key: "epoch_5", name: "5" },
]);
const age = /** @type {const} */ ([
@@ -246,7 +235,7 @@ function createPartialOptions(colors) {
...upTo,
...fromXToY,
...fromX,
...year,
...epochs,
]);
const size = /** @type {const} */ ([
@@ -475,7 +464,7 @@ function createPartialOptions(colors) {
upTo,
fromX,
fromXToY,
year,
epochs,
age,
type,
size,
@@ -3713,8 +3702,8 @@ function createPartialOptions(colors) {
),
},
{
name: "Years",
tree: groups.year.map(({ key, id, name }) =>
name: "Epochs",
tree: groups.epochs.map(({ key, id, name }) =>
createCohortOptionsGroup({
scale,
color: colors[key],
@@ -4824,10 +4813,10 @@ function createPartialOptions(colors) {
name: "Simulations",
tree: [
{
icon: "🧪",
icon: "💰",
kind: "simulation",
title: "Dollar Cost Average Simulation",
name: "Dollar Cost Average",
title: "Simulation: Save In Bitcoin",
name: "Save In Bitcoin",
},
],
},

View File

@@ -32,4 +32,378 @@ export function init({
webSockets,
}) {
const simulationElement = elements.simulation;
const parametersElement = window.document.createElement("div");
simulationElement.append(parametersElement);
const resultsElement = window.document.createElement("div");
simulationElement.append(resultsElement);
const storagePrefix = "save-in-bitcoin";
const settings = {
initial: signals.createSignal(/** @type {number | null} */ (1000), {
save: {
...utils.serde.number,
id: `${storagePrefix}-initial-amount`,
param: "initial-amount",
},
}),
later: signals.createSignal(/** @type {number | null} */ (0), {
save: {
...utils.serde.number,
id: `${storagePrefix}-later-amount`,
param: "later-amount",
},
}),
recurrent: {
amount: signals.createSignal(/** @type {number | null} */ (100), {
save: {
...utils.serde.number,
id: `${storagePrefix}-recurrent-amount`,
param: "recurrent-amount",
},
}),
},
};
const initialGroup = createParameterGroup({
title: "Initial",
description:
"The initial amount of dollars you're willing to eventually save in Bitcoin.",
});
parametersElement.append(initialGroup);
initialGroup.append(
createInputField({
name: "Directly converted",
input: createInputDollar({
id: "simulation-dollars-initial",
title: "Initial amount of dollars converted",
signal: settings.initial,
}),
}),
);
initialGroup.append(
createInputField({
name: "Converted over time",
input: createInputDollar({
id: "simulation-dollars-later",
title: "Dollars to spread over time",
signal: settings.later,
}),
}),
);
parametersElement.append(createHrElement());
const recurrentGroup = createParameterGroup({
title: "Recurrent",
description:
"The recurrent amount of dollars you're willing to eventually save in Bitcoin.",
});
parametersElement.append(recurrentGroup);
recurrentGroup.append(
createInputField({
name: "Amount",
input: createInputDollar({
id: "simulation-dollars-recurrent",
title: "Recurrent dollar amount",
signal: settings.recurrent.amount,
}),
}),
);
const frequencyUL = appendUl({ parent: recurrentGroup });
[{ name: "Daily" }, { name: "Weekly" }, { name: "Monthly" }].forEach(
({ name }) => {
const li = appendLi({ name, parent: frequencyUL });
},
);
const frequencyChoiceUL = appendUl({ parent: recurrentGroup });
[
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
].forEach((name) => {
const li = appendLi({ name, parent: frequencyChoiceUL });
});
parametersElement.append(createHrElement());
const today = signals.createSignal(utils.date.todayUTC());
setInterval(() => {
today.set(utils.date.todayUTC());
}, consts.FIVE_SECONDS_IN_MS);
const intervalGroup = createParameterGroup({
title: "Interval",
description: "wkfpweokf",
});
parametersElement.append(intervalGroup);
/**
* @param {Object} args
* @param {HTMLElement} args.parent
*/
function appendDiv({ parent }) {
const div = window.document.createElement("div");
parent.append(div);
return div;
}
function createInputDateField() {
const div = appendDiv({ parent: intervalGroup });
appendInputDate({
id: "",
title: "",
value: "2021-04-15",
parent: div,
signals,
today,
});
appendButton({
onClick: () => {},
text: "Reset",
title: "",
parent: div,
});
return div;
}
createInputDateField();
createInputDateField();
parametersElement.append(createHrElement());
const feesGroup = createParameterGroup({
title: "Fees",
description:
"The amount of fees (in %) from where you'll be exchanging your currency",
});
parametersElement.append(feesGroup);
createInputNumber({
id: "",
title: "",
value: 0.25,
parent: feesGroup,
min: 0,
max: 10,
});
parametersElement.append(createHrElement());
const strategyGroup = createParameterGroup({
title: "Strategy",
description: "The strategy used to convert your fiat into Bitcoin",
});
parametersElement.append(strategyGroup);
const ulStrategies = appendUl({ parent: strategyGroup });
["All in", "Weighted Local", "Weighted Cycle"].forEach((strategy) => {
appendLi({
name: strategy,
parent: ulStrategies,
});
});
//
// On the side
// Value in Bitcoin
// Value in Dollars + total converted
//
// Value min estimated value in 4 years
//
}
/**
* @param {Object} args
* @param {HTMLInputElement} args.input
* @param {string} args.name
*/
function createInputField({ name, input }) {
const div = window.document.createElement("div");
const label = window.document.createElement("label");
div.append(label);
// @ts-ignore
label.for = input.id;
label.innerHTML = name;
div.append(input);
return div;
}
/**
* @param {Object} args
* @param {string} args.title
* @param {string} args.description
*/
function createParameterGroup({ title, description }) {
const div = window.document.createElement("div");
const wrapper = window.document.createElement("div");
div.append(wrapper);
const titleElement = window.document.createElement("h4");
titleElement.innerHTML = title;
wrapper.append(titleElement);
const descriptionElement = window.document.createElement("small");
descriptionElement.innerHTML = description;
wrapper.append(descriptionElement);
return div;
}
function createHrElement() {
return window.document.createElement("hr");
}
/**
*@param {Object} args
*@param {string} args.id
*@param {string} args.title
*@param {number} args.value
*@param {HTMLElement} args.parent
*@param {number} args.min
*@param {number} args.max
*/
function createInputNumber({ id, title, value, parent, min, max }) {
const input = window.document.createElement("input");
input.id = id;
input.title = title;
input.type = "number";
input.value = String(value);
input.min = String(min);
input.max = String(max);
parent.append(input);
return input;
}
/**
* @param {Object} args
* @param {string} args.id
* @param {string} args.title
* @param {Signal<number | null>} args.signal
*/
function createInputDollar({ id, title, signal }) {
const input = window.document.createElement("input");
input.id = id;
input.type = "number";
input.placeholder = "US Dollars";
input.min = "0";
input.title = title;
const value = signal();
input.value = value !== null ? String(value) : "";
input.addEventListener("input", () => {
const value = input.value;
signal.set(value ? Number(value) : null);
});
return input;
}
/**
* @param {Object} args
* @param {string} args.id
* @param {string} args.title
* @param {Signal<number | null>} args.signal
*/
function createInputRangePercentage({ id, title, signal }) {
const input = window.document.createElement("input");
input.id = id;
input.title = title;
input.type = "range";
input.min = "0";
input.max = "100";
const value = signal();
input.value = value !== null ? String(value) : "";
input.addEventListener("input", () => {
const value = input.value;
signal.set(value ? Number(value) : null);
});
return input;
}
/**
* @param {Object} args
* @param {HTMLElement} args.parent
*/
function appendUl({ parent }) {
const ul = window.document.createElement("ul");
parent.append(ul);
return ul;
}
/**
* @param {Object} args
* @param {string} args.name
* @param {HTMLUListElement} args.parent
*/
function appendLi({ name, parent }) {
const li = window.document.createElement("li");
li.innerHTML = name;
parent.append(li);
return li;
}
/**
*@param {Object} args
*@param {string} args.id
*@param {string} args.title
*@param {string} args.value
*@param {HTMLElement} args.parent
*@param {Accessor<Date>} args.today
*@param {Signals} args.signals
*/
function appendInputDate({ id, title, value, parent, today, signals }) {
const input = window.document.createElement("input");
input.id = id;
input.title = title;
input.type = "date";
input.value = value;
input.min = "2011-01-01";
signals.createEffect(() => {
input.max = today().toJSON().split("T")[0];
});
parent.append(input);
return input;
}
/**
*@param {Object} args
*@param {string} args.title
*@param {string} args.text
*@param {HTMLElement} args.parent
*@param {VoidFunction} args.onClick
*/
function appendButton({ title, text, onClick, parent }) {
const button = window.document.createElement("button");
button.title = title;
button.innerHTML = text;
button.addEventListener("click", onClick);
parent.append(button);
return button;
}

View File

@@ -127,8 +127,8 @@ interface PartialChartOption extends PartialOption {
interface PartialSimulationOption extends PartialOption {
kind: "simulation";
title: "Dollar Cost Average Simulation";
name: "Dollar Cost Average";
title: string;
name: string;
}
interface PartialPdfOption extends PartialOption {

View File

@@ -1,144 +1,144 @@
#charts {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
> #legend {
display: flex;
align-items: center;
gap: 1.5rem;
margin: -1rem -1.5rem;
padding: 1rem 1.5rem;
overflow-x: auto;
min-width: 0;
> div {
flex: 0;
display: flex;
align-items: center;
> label {
margin: -0.375rem 0;
color: var(--color);
&:has(input:not(:checked)) {
color: var(--off-color);
> span.main > span.name {
text-decoration-style: wavy;
text-decoration-thickness: 1.5px;
text-decoration-color: var(--color);
text-decoration-line: line-through;
}
&:hover {
* {
color: var(--off-color) !important;
}
> span.main > span.name {
text-decoration-color: var(--orange) !important;
}
}
}
}
> a {
padding: 0.375rem;
margin: -0.375rem;
> svg {
/* padding: 0.375rem; */
margin: 0rem;
width: 1rem;
height: 1rem;
}
}
}
}
> #chart-list {
margin-top: 1rem;
position: relative;
margin-left: -1.5rem /* -24px */;
margin-right: -2rem /* -32px */;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
z-index: 20;
> #legend {
> .chart-wrapper {
height: 100%;
position: relative;
min-height: 0px;
width: 100%;
cursor: crosshair;
&:has(+ .chart-wrapper:not([hidden])) {
height: calc(100% - 62px);
}
> fieldset {
pointer-events: none;
position: absolute;
left: 0px;
top: 0px;
z-index: 10;
display: flex;
align-items: center;
gap: 1.5rem;
margin: -1rem -1.5rem;
padding: 1rem 1.5rem;
overflow-x: auto;
min-width: 0;
padding-left: 1.5rem /* 24px */;
padding-right: 1.5rem /* 24px */;
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
> div {
flex: 0;
display: flex;
align-items: center;
> label {
margin: -0.375rem 0;
color: var(--color);
&:has(input:not(:checked)) {
color: var(--off-color);
> span.main > span.name {
text-decoration-style: wavy;
text-decoration-thickness: 1.5px;
text-decoration-color: var(--color);
text-decoration-line: line-through;
}
&:hover {
* {
color: var(--off-color) !important;
}
> span.main > span.name {
text-decoration-color: var(--orange) !important;
}
}
}
}
> a {
padding: 0.375rem;
margin: -0.375rem;
> svg {
/* padding: 0.375rem; */
margin: 0rem;
width: 1rem;
height: 1rem;
}
}
> * + * {
margin-left: 0.5rem; /* 8px */
}
}
> #chart-list {
margin-top: 1rem;
position: relative;
margin-left: -1.5rem /* -24px */;
margin-right: -2rem /* -32px */;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
z-index: 20;
> .chart-wrapper {
height: 100%;
position: relative;
min-height: 0px;
width: 100%;
cursor: crosshair;
&:has(+ .chart-wrapper:not([hidden])) {
height: calc(100% - 62px);
}
> fieldset {
pointer-events: none;
position: absolute;
left: 0px;
top: 0px;
z-index: 10;
display: flex;
align-items: center;
padding-left: 1.5rem /* 24px */;
padding-right: 1.5rem /* 24px */;
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
> * + * {
margin-left: 0.5rem; /* 8px */
}
> * + span {
color: var(--off-color);
}
}
> .chart-div {
width: 100%;
height: 100%;
}
> * + span {
color: var(--off-color);
}
}
> .chart-div {
width: 100%;
height: 100%;
}
}
}
> hr {
margin-top: 1rem;
}
> #timescale {
> #timescale-date-buttons,
> #timescale-height-buttons {
display: flex;
overflow-x: auto;
display: flex;
overflow-x: auto;
margin-bottom: -0.5rem;
padding: 0.5rem 0.5rem;
padding-top: 0.5rem;
margin-left: -1.5rem;
margin-right: -1.5rem;
> button {
color: var(--off-color);
flex-shrink: 0;
flex-grow: 1;
padding: 0.5rem;
white-space: nowrap;
min-width: 5rem;
}
@media (max-width: 767px) {
margin-bottom: -1.5rem;
padding-top: 0.5rem;
}
}
> hr {
margin-top: 1rem;
}
> #timescale {
> #timescale-date-buttons,
> #timescale-height-buttons {
display: flex;
overflow-x: auto;
display: flex;
overflow-x: auto;
margin-bottom: -0.5rem;
padding: 0.5rem 0.5rem;
padding-top: 0.5rem;
margin-left: -1.5rem;
margin-right: -1.5rem;
> button {
color: var(--off-color);
flex-shrink: 0;
flex-grow: 1;
padding: 0.5rem;
white-space: nowrap;
min-width: 5rem;
}
@media (max-width: 767px) {
margin-bottom: -1.5rem;
padding-top: 0.5rem;
}
}
}
}

View File

@@ -1,23 +1,29 @@
#simulation {
min-height: 0;
min-height: 0;
display: flex;
height: 100%;
width: 100%;
> div {
flex: 1;
/* min-height: 0; */
overflow-y: auto;
display: flex;
height: 100%;
width: 100%;
flex-direction: column;
width: 32rem;
gap: 1rem;
> div {
flex: 1;
/* min-height: 0; */
overflow-y: auto;
display: flex;
flex-direction: column;
width: 32rem;
gap: 1rem;
display: flex;
flex-direction: column;
> div {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
gap: 0.5rem;
}
}
div:has(> input[type="date"] + button) {
display: flex;
gap: 0.5rem;
align-items: center;
}
}