general: snapshot

This commit is contained in:
k
2024-11-23 16:17:06 +01:00
parent cfae483d9d
commit c234c17352
15 changed files with 957 additions and 457 deletions
+5 -4
View File
@@ -68,7 +68,7 @@ Please open an issue if you want to add another instance
- `-txindex=1`
- `-blocksxor=0`
- RPC credentials
- Example: `bitcoind -datadir="$HOME/.bitcoin" -blocksonly -txindex=1 -blocksxor=0 -rpcuser="satoshi" -rpcpassword="nakamoto"`
- Example: `bitcoind -datadir="$HOME/.bitcoin" -blocksonly -txindex=1 -blocksxor=0`
- Git
### Manual
@@ -122,11 +122,12 @@ Now we can finally start by running the parser, you need to use the `./run.sh` s
For the first launch, the parser will need several information such as:
- `--datadir`: which is bitcoin data directory path, prefer `$HOME` to `~` as the latter might not work
- `--rpcuser`: the username of the RPC credentials to talk to the bitcoin server
- `--rpcpassword`: the password of the RPC credentials
Optionally you can also specify:
- `--rpccookiefile`: the path to the cookie file if not default
- `--rpcuser`: the username of the RPC credentials to talk to the bitcoin server if set
- `--rpcpassword`: the password of the RPC credentials if set
- `--rpcconnect`: if the bitcoin core server's IP is different than `localhost`
- `--rpcport`: if the port is different than `8332`
@@ -135,7 +136,7 @@ Everything will be saved in a `config.toml` file, which will allow you to simply
Here's an example
```bash
./run.sh --datadir=$HOME/Developer/bitcoin --rpcuser=satoshi --rpcpassword=nakamoto
./run.sh --datadir=$HOME/Developer/bitcoin
```
In a **new** terminal, go to the `server`'s folder of the repository
+2 -2
View File
@@ -145,8 +145,8 @@ impl Binance {
})
.collect::<BTreeMap<_, _>>())
},
30,
10,
5,
)
}
@@ -195,7 +195,7 @@ impl Binance {
})
.collect::<BTreeMap<_, _>>())
},
10,
30,
10,
)
}
+2 -2
View File
@@ -55,7 +55,7 @@ impl Kibo {
.map(Self::value_to_ohlc)
.collect_vec())
},
10,
30,
RETRIES,
)
}
@@ -92,7 +92,7 @@ impl Kibo {
})
.collect::<BTreeMap<_, _>>())
},
10,
30,
RETRIES,
)
}
+2 -2
View File
@@ -60,7 +60,7 @@ impl Kraken {
})
.collect::<BTreeMap<_, _>>())
},
10,
30,
10,
)
}
@@ -115,7 +115,7 @@ impl Kraken {
})
.collect::<BTreeMap<_, _>>())
},
10,
30,
10,
)
}
+51 -14
View File
@@ -1,6 +1,11 @@
use std::fs::{self};
use std::{
fs::{self},
path::{Path, PathBuf},
};
use biter::bitcoincore_rpc::Auth;
use clap::Parser;
use color_eyre::eyre::eyre;
use serde::{Deserialize, Serialize};
use crate::log;
@@ -20,6 +25,10 @@ pub struct Config {
#[arg(long, value_name = "PORT")]
pub rpcport: Option<u16>,
/// Bitcoin RPC cookie file, saved
#[arg(long, value_name = "PATH")]
pub rpccookiefile: Option<String>,
/// Bitcoin RPC username, saved
#[arg(long, value_name = "USERNAME")]
pub rpcuser: Option<String>,
@@ -75,6 +84,10 @@ impl Config {
config_saved.rpcport = Some(rpcport);
}
if let Some(rpccookiefile) = config_args.rpccookiefile.take() {
config_saved.rpccookiefile = Some(rpccookiefile);
}
if let Some(rpcuser) = config_args.rpcuser.take() {
config_saved.rpcuser = Some(rpcuser);
}
@@ -109,6 +122,7 @@ impl Config {
log(&format!("datadir: {:?}", config.datadir));
log(&format!("rpcconnect: {:?}", config.rpcconnect));
log(&format!("rpcport: {:?}", config.rpcport));
log(&format!("rpccookiefile: {:?}", config.rpccookiefile));
log(&format!("rpcuser: {:?}", config.rpcuser));
log(&format!("rpcpassword: {:?}", config.rpcpassword));
log(&format!("delay: {:?}", config.delay));
@@ -132,30 +146,53 @@ impl Config {
fn check(&self) {
if self.datadir.is_none() {
Self::exit("datadir");
println!(
"You need to set the --datadir parameter at least once to run the parser.\nRun the program with '-h' for help."
);
std::process::exit(1);
}
if self.rpcuser.is_none() {
Self::exit("rpcuser");
let path = Path::new(self.datadir.as_ref().unwrap());
if !path.is_dir() {
println!("Expect path '{:#?}' to be a directory.", path);
std::process::exit(1);
}
if self.rpcpassword.is_none() {
Self::exit("rpcpassword");
if self.to_rpc_auth().is_err() {
println!(
"No way found to authenticate the RPC client, please either set --rpccookiefile or --rpcuser and --rpcpassword.\nRun the program with '-h' for help."
);
std::process::exit(1);
}
}
fn exit(attribute: &str) {
println!(
"You need to set the --{} parameter at least once to run the parser.\nRun the program with '-h' for help.", attribute
);
std::process::exit(1);
}
fn write(&self) -> std::io::Result<()> {
fs::write(Self::PATH, toml::to_string(self).unwrap())
}
pub fn to_rpc_auth(&self) -> color_eyre::Result<Auth> {
let cookie = Path::new(self.datadir.as_ref().unwrap()).join(".cookie");
if cookie.is_file() {
Ok(Auth::CookieFile(cookie))
} else if self
.rpccookiefile
.as_ref()
.is_some_and(|cookie| Path::new(cookie).is_file())
{
Ok(Auth::CookieFile(PathBuf::from(
self.rpccookiefile.as_ref().unwrap(),
)))
} else if self.rpcuser.is_some() && self.rpcpassword.is_some() {
Ok(Auth::UserPass(
self.rpcuser.clone().unwrap(),
self.rpcpassword.clone().unwrap(),
))
} else {
Err(eyre!("Failed to find correct auth"))
}
}
pub fn dry_run(&self) -> bool {
self.dry_run.is_some_and(|b| b)
}
+3 -9
View File
@@ -699,7 +699,7 @@ where
})
.into();
if previous_average.is_nan() {
if previous_average.is_nan() || previous_average.is_infinite() {
previous_average = 0.0;
}
@@ -708,17 +708,11 @@ where
panic!()
}));
if last_value.is_nan() {
if last_value.is_nan() || last_value.is_infinite() {
last_value = 0.0;
}
let _average = (previous_average * (len - 1.0) + last_value) / len;
if _average.is_nan() || _average.is_infinite() {
average.replace(0.0.into());
} else {
average.replace(_average.into());
}
average.replace(((previous_average * (len - 1.0) + last_value) / len).into());
self.insert_computed(*key, average.unwrap());
});
+2 -5
View File
@@ -1,4 +1,4 @@
use biter::bitcoincore_rpc::{Auth, Client};
use biter::bitcoincore_rpc::Client;
use crate::Config;
@@ -12,9 +12,6 @@ pub fn create_rpc(config: &Config) -> color_eyre::Result<Client> {
.unwrap_or(&"localhost".to_owned()),
config.rpcport.unwrap_or(8332)
),
Auth::UserPass(
config.rpcuser.clone().unwrap(),
config.rpcpassword.clone().unwrap(),
),
config.to_rpc_auth().unwrap(),
)?)
}
+68 -57
View File
@@ -37,11 +37,10 @@
line-height: 1.5;
-webkit-text-size-adjust: 100%;
tab-size: 4;
font-family: var(--default-font-family), ui-sans-serif, system-ui,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
font-family: "Satoshi", ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Noto Color Emoji";
font-feature-settings: var(--default-font-feature-settings, normal);
font-variation-settings: var(--default-font-variation-settings, normal);
font-feature-settings: "ss03";
-webkit-tap-highlight-color: transparent;
}
@@ -137,12 +136,6 @@
background: transparent;
}
input:where(:not([type="button"], [type="reset"], [type="submit"])),
select,
textarea {
border: 1px solid;
}
button,
input:where([type="button"], [type="reset"], [type="submit"]),
::file-selector-button {
@@ -221,10 +214,6 @@
:root {
color-scheme: light dark;
--default-font-family: "Satoshi";
--default-font-feature-settings: "normal";
--default-font-variation-settings: "normal";
/* before: #ffffe3 */
/* current: oklch(99% 0.01 44) */
--white: #fffaf6;
@@ -243,9 +232,27 @@
/* before: #10100e */
/* current: oklch(17.5% 0.005 44) */
--black: #12100f;
--red: oklch(0.607 0.241 26.328);
/* before: #f26610 */
/* current: oklch(67.64% 0.191 44.41) */
--orange: #f26610;
--orange: oklch(67.64% 0.191 44.41); /*oklch(0.6755 0.2175 44.36); */
--amber: oklch(0.7175 0.1835 64.199);
--yellow: oklch(0.738 0.173 80.9405);
--avocado: oklch(0.72 0.19 110);
--lime: oklch(0.708 0.2165 131.267);
--green: oklch(0.675 0.2065 149.3965);
--emerald: oklch(0.646 0.1575 162.8525);
--teal: oklch(0.652 0.129 183.6035);
--cyan: oklch(0.662 0.1345 218.472);
--sky: oklch(0.6365 0.1635 239.6445);
--blue: oklch(0.5845 0.2295 261.348);
--indigo: oklch(0.548 0.2475 277.0415);
--violet: oklch(0.5735 0.2655 292.863);
--purple: oklch(0.5925 0.2765 303.1105);
--fuchsia: oklch(0.629 0.294 322.523);
--pink: oklch(0.624 0.245 357.444);
--rose: oklch(0.6155 0.2495 17.012);
--dollar: var(--green);
--font-size-2xs: 0.625rem;
--line-height-2xs: 1rem;
@@ -273,12 +280,19 @@
--transform-scale-active: scaleY(0.9);
--default-main-width: 400px;
--default-main-width: 25rem;
--background-color: light-dark(var(--white), var(--black));
--color: light-dark(var(--black), var(--white));
--off-color: light-dark(var(--light-gray), var(--dark-gray));
--border-color: light-dark(var(--lighter-gray), var(--darker-gray));
--emoji-filter: grayscale(1) contrast(5) invert(1);
@media (prefers-color-scheme: dark) {
--emoji-filter: grayscale(1) contrast(5);
}
--bottom-area: 69vh;
}
[data-resize] {
@@ -377,7 +391,13 @@
display: flex;
width: 100%;
flex: 1;
margin-bottom: 4rem;
margin-bottom: calc(var(--main-padding) + 1.5rem);
@media (max-width: 767px) {
html[data-display="standalone"] & {
margin-bottom: calc(var(--main-padding) + 2rem);
}
}
@media (min-width: 768px) {
border-left: 1px;
@@ -386,23 +406,7 @@
}
header {
flex-shrink: 0;
display: flex;
white-space: nowrap;
overflow-x: auto;
padding-bottom: 1rem;
margin-bottom: -1rem;
padding-left: var(--main-padding);
margin-left: var(--negative-main-padding);
padding-right: var(--main-padding);
margin-right: var(--negative-main-padding);
& > * {
flex: 1;
}
& > div > small {
small {
font-weight: var(--font-weight-base);
font-size: var(--font-size-base);
line-height: var(--line-height-base);
@@ -596,11 +600,17 @@
left: 0;
display: flex;
margin: var(--main-padding);
margin-bottom: calc(var(--main-padding) + 0.5rem);
margin-bottom: var(--main-padding);
z-index: 20;
pointer-events: none;
justify-content: center;
@media (max-width: 767px) {
html[data-display="standalone"] & {
margin-bottom: calc(var(--main-padding) + 0.5rem);
}
}
> fieldset {
background-color: var(--border-color);
display: flex;
@@ -639,22 +649,22 @@
background-color: white;
}
nav,
search,
section {
flex: 1;
min-width: 0;
}
nav,
search {
flex: 1;
overflow-x: hidden;
overflow-y: auto;
padding: var(--main-padding);
height: 100%;
&:not(#selected-frame) {
padding-bottom: 69vh;
}
> * + * {
margin-top: 2rem;
}
display: flex;
flex-direction: column;
gap: 2rem;
padding-bottom: var(--bottom-area);
}
sup {
@@ -727,6 +737,7 @@
user-select: none;
-webkit-user-select: none;
text-transform: capitalize;
margin-top: -0.375rem;
button::after {
color: var(--off-color);
@@ -1459,7 +1470,7 @@
<small style="display: block">
<strong>Bitcoin</strong> is
<b style="color: var(--color)">hope</b>
<span style="filter: grayscale(1) contrast(5)">🕊️</span>
<span style="filter: var(--emoji-filter)">🕊️</span>
</small>
</header>
</nav>
@@ -1517,19 +1528,19 @@
</footer>
</main>
<aside id="aside">
<div class="shadow-left"></div>
<div class="shadow-right"></div>
<section id="selected-frame">
<header id="selected-header" hidden>
<div>
<h1 style="display: flex; flex-direction: column">
<span id="selected-title" style="display: block"></span>
<small id="selected-description"></small>
</h1>
</div>
</header>
<div id="charts" hidden>
<div class="shadow-left"></div>
<div class="shadow-right"></div>
<!-- TODO: Use utils.dom.createHeader; instead -->
<header id="selected-header" hidden>
<div>
<h1 style="display: flex; flex-direction: column">
<span id="selected-title" style="display: block"></span>
<small id="selected-description"></small>
</h1>
</div>
</header>
<fieldset id="legend"></fieldset>
<div id="charts-chart-list" class="chart-list">
<div class="shadow-top"></div>
+46
View File
@@ -0,0 +1,46 @@
(async () => {
const theme = await (
await fetch(
"https://raw.githubusercontent.com/tailwindlabs/tailwindcss/refs/heads/next/packages/tailwindcss/theme.css",
)
).text();
console.log(
[
"red",
"orange",
"amber",
"yellow",
"lime",
"green",
"emerald",
"teal",
"cyan",
"sky",
"blue",
"indigo",
"violet",
"purple",
"fuchsia",
"pink",
"rose",
]
.map((color) => {
const [a, b] = [500, 600].map((shade) => {
const regExp = new RegExp(
`(?<=${`${color}-${shade}: oklch\(`})(.*?)(?=\\s*${`\);`})`,
"g",
);
let res = regExp.exec(theme)?.[2];
if (!res) throw "err";
res = res.replace("(", "");
res = res.replace(")", "");
// return res
return res.split(" ").map((s) => Number(s));
});
const mult = 10_000;
return `--${color}: oklch(${[0, 1, 2].map((i) => Math.round(((a[i] + b[i]) / 2) * mult) / mult).join(" ")})`;
})
.join(";\n"),
);
})();
+210 -59
View File
@@ -1,7 +1,7 @@
// @ts-check
/**
* @import { Option, ResourceDataset, TimeScale, TimeRange, Unit, Marker, Weighted, DatasetPath, OHLC, FetchedJSON, DatasetValue, FetchedResult, AnyDatasetPath, SeriesBlueprint, BaselineSpecificSeriesBlueprint, CandlestickSpecificSeriesBlueprint, LineSpecificSeriesBlueprint, SpecificSeriesBlueprintWithChart, Signal, Color, DatasetCandlestickData, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, AnyPath, SimulationOption } from "./types/self"
* @import { Option, ResourceDataset, TimeScale, TimeRange, Unit, Marker, Weighted, DatasetPath, OHLC, FetchedJSON, DatasetValue, FetchedResult, AnyDatasetPath, SeriesBlueprint, BaselineSpecificSeriesBlueprint, CandlestickSpecificSeriesBlueprint, LineSpecificSeriesBlueprint, SpecificSeriesBlueprintWithChart, Signal, Color, DatasetCandlestickData, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, AnyPath, SimulationOption, Frequency } from "./types/self"
* @import {createChart as CreateClassicChart, createChartEx as CreateCustomChart, LineStyleOptions} from "../packages/lightweight-charts/v4.2.0/types";
* @import * as _ from "../packages/ufuzzy/v1.0.14/types"
* @import { DeepPartial, ChartOptions, IChartApi, IHorzScaleBehavior, WhitespaceData, SingleValueData, ISeriesApi, Time, LineData, LogicalRange, SeriesMarker, CandlestickData, SeriesType, BaselineStyleOptions, SeriesOptionsCommon } from "../packages/lightweight-charts/v4.2.0/types"
@@ -56,6 +56,9 @@ function initPackages() {
// @ts-ignore
get.set = set;
// @ts-ignore
get.reset = () => set(initialValue);
if (options?.save) {
const save = options.save;
@@ -75,7 +78,13 @@ function initPackages() {
if (!save) return;
if (!firstEffect && save.id) {
if (value !== undefined && value !== null) {
if (
value !== undefined &&
value !== null &&
(initialValue === undefined ||
initialValue === null ||
save.serialize(value) !== save.serialize(initialValue))
) {
localStorage.setItem(save.id, save.serialize(value));
} else {
localStorage.removeItem(save.id);
@@ -83,7 +92,13 @@ function initPackages() {
}
if (save.param) {
if (value !== undefined && value !== null) {
if (
value !== undefined &&
value !== null &&
(initialValue === undefined ||
initialValue === null ||
save.serialize(value) !== save.serialize(initialValue))
) {
utils.url.writeParam(save.param, save.serialize(value));
} else {
utils.url.removeParam(save.param);
@@ -1048,10 +1063,13 @@ const utils = {
const h1 = window.document.createElement("h1");
div.append(h1);
h1.style.display = "flex";
h1.style.flexDirection = "column";
const titleElement = window.document.createElement("span");
titleElement.append(title);
h1.append(titleElement);
titleElement.style.display = "block";
const descriptionElement = window.document.createElement("small");
descriptionElement.append(description);
@@ -1063,6 +1081,61 @@ const utils = {
descriptionElement,
};
},
/**
* @param {Object} param0
* @param {string} param0.name
* @param {string} param0.value
*/
createOption({ name, value }) {
const option = window.document.createElement("option");
option.value = value;
option.innerText = name;
return option;
},
/**
* @template {{name: string; value: string}} T
* @param {Object} param0
* @param {string} param0.id
* @param {(({name: string; value: string} & T) | {name: string; list: ({name: string; value: string} & T)[]})[]} param0.list
* @param {Signal<T>} param0.signal
*/
createSelect({ id, list, signal }) {
const select = window.document.createElement("select");
select.name = id;
select.value = signal().value;
/** @type {Record<string, VoidFunction>} */
const setters = {};
list.forEach((anyOption, index) => {
if ("list" in anyOption) {
const { name, list } = anyOption;
const optGroup = window.document.createElement("optgroup");
optGroup.label = name;
select.append(optGroup);
list.forEach((option) => {
optGroup.append(this.createOption(option));
setters[option.value] = () => signal.set(option);
});
} else {
select.append(this.createOption(anyOption));
setters[anyOption.value] = () => signal.set(anyOption);
}
if (index !== list.length - 1) {
select.append(window.document.createElement("hr"));
}
});
select.addEventListener("change", () => {
const callback = setters[select.value];
// @ts-ignore
if (callback) {
callback();
}
});
return select;
},
},
url: {
chartParamsWhitelist: ["from", "to"],
@@ -1367,6 +1440,101 @@ const utils = {
return dates;
},
},
color: {
/**
*
* @param {readonly [number, number, number, number, number, number, number, number, number]} A
* @param {readonly [number, number, number]} B
* @returns
*/
multiplyMatrices(A, B) {
return /** @type {const} */ ([
A[0] * B[0] + A[1] * B[1] + A[2] * B[2],
A[3] * B[0] + A[4] * B[1] + A[5] * B[2],
A[6] * B[0] + A[7] * B[1] + A[8] * B[2],
]);
},
/**
* @param {readonly [number, number, number]} param0
*/
oklch2oklab([l, c, h]) {
return /** @type {const} */ ([
l,
isNaN(h) ? 0 : c * Math.cos((h * Math.PI) / 180),
isNaN(h) ? 0 : c * Math.sin((h * Math.PI) / 180),
]);
},
/**
* @param {readonly [number, number, number]} rgb
*/
srgbLinear2rgb(rgb) {
return rgb.map((c) =>
Math.abs(c) > 0.0031308
? (c < 0 ? -1 : 1) * (1.055 * Math.abs(c) ** (1 / 2.4) - 0.055)
: 12.92 * c,
);
},
/**
* @param {readonly [number, number, number]} lab
*/
oklab2xyz(lab) {
const LMSg = this.multiplyMatrices(
/** @type {const} */ ([
1, 0.3963377773761749, 0.2158037573099136, 1, -0.1055613458156586,
-0.0638541728258133, 1, -0.0894841775298119, -1.2914855480194092,
]),
lab,
);
const LMS = /** @type {[number, number, number]} */ (
LMSg.map((val) => val ** 3)
);
return this.multiplyMatrices(
/** @type {const} */ ([
1.2268798758459243, -0.5578149944602171, 0.2813910456659647,
-0.0405757452148008, 1.112286803280317, -0.0717110580655164,
-0.0763729366746601, -0.4214933324022432, 1.5869240198367816,
]),
LMS,
);
},
/**
* @param {readonly [number, number, number]} xyz
*/
xyz2rgbLinear(xyz) {
return this.multiplyMatrices(
[
3.2409699419045226, -1.537383177570094, -0.4986107602930034,
-0.9692436362808796, 1.8759675015077202, 0.04155505740717559,
0.05563007969699366, -0.20397695888897652, 1.0569715142428786,
],
xyz,
);
},
/** @param {string} oklch */
oklch2hex(oklch) {
oklch = oklch.replace("oklch(", "");
oklch = oklch.replace(")", "");
const lch = oklch.split(" ").map((v, i) => {
if (!i && v.includes("%")) {
return Number(v.replace("%", "")) / 100;
} else {
return Number(v);
}
});
const [r, g, b] = this.srgbLinear2rgb(
this.xyz2rgbLinear(
this.oklab2xyz(
this.oklch2oklab(/** @type {[number, number, number]} */ (lch)),
),
),
).map((v) => {
v = Math.max(Math.min(Math.round(v * 255), 255), 0);
const s = v.toString(16);
return s.length === 1 ? `0${s}` : s;
});
return `#${r}${g}${b}`;
},
},
/**
*
* @template {(...args: any[]) => any} F
@@ -1421,6 +1589,12 @@ const utils = {
? id - 2009
: Math.floor(id / consts.HEIGHT_CHUNK_SIZE);
},
/**
* @param {string} str
*/
stringToId(str) {
return str.toLowerCase().replace(" ", "-");
},
};
/** @typedef {typeof utils} Utilities */
@@ -1647,86 +1821,65 @@ createKeyDownEventListener();
* @param {Accessor<boolean>} dark
*/
function createColors(dark) {
function lightRed() {
const tailwindRed300 = "#fca5a5";
const tailwindRed800 = "#991b1b";
return dark() ? tailwindRed300 : tailwindRed800;
/**
* @param {string} color
*/
function getColor(color) {
return utils.color.oklch2hex(elements.style.getPropertyValue(`--${color}`));
}
function red() {
return "#e63636"; // 550
}
function darkRed() {
const tailwindRed900 = "#7f1d1d";
const tailwindRed100 = "#fee2e2";
return dark() ? tailwindRed900 : tailwindRed100;
return getColor("red");
}
function orange() {
return elements.style.getPropertyValue("--orange"); // 550
}
function darkOrange() {
const tailwindOrange900 = "#7c2d12";
const tailwindOrange100 = "#ffedd5";
return dark() ? tailwindOrange900 : tailwindOrange100;
return getColor("orange");
}
function amber() {
return "#e78a05"; // 550
return getColor("amber");
}
function yellow() {
return "#db9e03"; // 550
return getColor("yellow");
}
function avocado() {
return getColor("avocado");
}
function lime() {
return "#74b713"; // 550
return getColor("line");
}
function green() {
return "#1cb454";
}
function darkGreen() {
const tailwindGreen900 = "#14532d";
const tailwindGreen100 = "#dcfce7";
return dark() ? tailwindGreen900 : tailwindGreen100;
return getColor("green");
}
function emerald() {
return "#0ba775";
}
function darkEmerald() {
const tailwindEmerald900 = "#064e3b";
const tailwindEmerald100 = "#d1fae5";
return dark() ? tailwindEmerald900 : tailwindEmerald100;
return getColor("emerald");
}
function teal() {
return "#10a697"; // 550
return getColor("teal");
}
function cyan() {
return "#06a3c3"; // 550
return getColor("cyan");
}
function sky() {
return "#0794d8"; // 550
return getColor("sky");
}
function blue() {
return "#2f73f1"; // 550
return getColor("blue");
}
function indigo() {
return "#5957eb";
return getColor("indigo");
}
function violet() {
return "#834cf2";
return getColor("violet");
}
function purple() {
return "#9d45f0";
return getColor("purple");
}
function fuchsia() {
return "#cc37e1";
return getColor("fuchsia");
}
function pink() {
return "#e53882";
return getColor("pink");
}
function rose() {
return "#ea3053";
}
function darkRose() {
const tailwindRose900 = "#881337";
const tailwindRose100 = "#ffe4e6";
return dark() ? tailwindRose900 : tailwindRose100;
return getColor("rose");
}
/**
@@ -1751,16 +1904,16 @@ function createColors(dark) {
off,
lightBitcoin: yellow,
bitcoin: orange,
darkBitcoin: darkOrange,
offBitcoin: red,
lightDollars: lime,
dollars: emerald,
darkDollars: darkEmerald,
dollars: green,
offDollars: emerald,
yellow,
orange,
red,
_1d: lightRed,
_1d: pink,
_1w: red,
_8d: orange,
_13d: amber,
@@ -1806,17 +1959,15 @@ function createColors(dark) {
cvdd: lime,
terminalPrice: red,
loss: red,
darkLoss: darkRed,
profit: green,
darkProfit: darkGreen,
thermoCap: green,
investorCap: rose,
realizedCap: orange,
darkLiveliness: darkRose,
offLiveliness: red,
liveliness: rose,
vaultedness: green,
activityToVaultednessRatio: violet,
up_to_1d: lightRed,
up_to_1d: pink,
up_to_1w: red,
up_to_1m: orange,
up_to_2m: amber,
@@ -2324,9 +2475,9 @@ function initWebSockets(signals) {
if (result.channel !== "ohlc") return;
const { timestamp, open, high, low, close } = result.data.at(-1);
const { interval_begin, open, high, low, close } = result.data.at(-1);
const date = new Date(timestamp);
const date = new Date(interval_begin);
const dateStr = utils.date.toString(date);
+9 -9
View File
@@ -1602,7 +1602,7 @@ function createPartialOptions(colors) {
},
{
title: "Mined",
color: colors.darkBitcoin,
color: colors.offBitcoin,
datasetPath: `date-to-blocks-mined`,
main: true,
},
@@ -2395,7 +2395,7 @@ function createPartialOptions(colors) {
},
{
title: "Fees",
color: colors.darkBitcoin,
color: colors.offBitcoin,
datasetPath:
scale === "date"
? `${scale}-to-fees-to-coinbase-1d-ratio`
@@ -2443,7 +2443,7 @@ function createPartialOptions(colors) {
},
{
title: "Rate",
color: colors.darkBitcoin,
color: colors.offBitcoin,
datasetPath: `date-to-hash-rate`,
},
],
@@ -2637,7 +2637,7 @@ function createPartialOptions(colors) {
},
{
title: "Raw",
color: colors.darkBitcoin,
color: colors.offBitcoin,
datasetPath:
scale === "date"
? `${scale}-to-transaction-count-1d-sum`
@@ -2675,7 +2675,7 @@ function createPartialOptions(colors) {
},
{
title: "Raw",
color: colors.darkBitcoin,
color: colors.offBitcoin,
datasetPath:
scale === "date"
? `${scale}-to-transaction-volume-1d-sum`
@@ -2708,7 +2708,7 @@ function createPartialOptions(colors) {
},
{
title: "Raw",
color: colors.darkDollars,
color: colors.offDollars,
datasetPath:
scale === "date"
? `${scale}-to-transaction-volume-in-dollars-1d-sum`
@@ -2789,7 +2789,7 @@ function createPartialOptions(colors) {
},
{
title: "Raw",
color: colors.darkBitcoin,
color: colors.offBitcoin,
datasetPath: `${scale}-to-transactions-per-second`,
main: true,
},
@@ -4534,7 +4534,7 @@ function createPartialOptions(colors) {
},
{
title: "Concurrent Liveliness",
color: colors.darkLiveliness,
color: colors.offLiveliness,
datasetPath: `${scale}-to-concurrent-liveliness`,
},
],
@@ -4548,7 +4548,7 @@ function createPartialOptions(colors) {
bottom: [
{
title: "Liveliness Incremental Change",
color: colors.darkLiveliness,
color: colors.offLiveliness,
type: "Baseline",
datasetPath: `date-to-liveliness-net-change`,
},
+493 -261
View File
@@ -38,39 +38,71 @@ export function init({
const resultsElement = window.document.createElement("div");
simulationElement.append(resultsElement);
const getDefaultIntervalStart = () => new Date("2021-04-15");
const getDefaultIntervalEnd = () => new Date();
const frequencies = computeFrequencies();
const storagePrefix = "save-in-bitcoin";
const settings = {
initial: {
firstDay: signals.createSignal(/** @type {number | null} */ (1000), {
save: {
...utils.serde.number,
id: `${storagePrefix}-initial-amount`,
param: "initial-amount",
},
}),
overTime: signals.createSignal(/** @type {number | null} */ (0), {
save: {
...utils.serde.number,
id: `${storagePrefix}-later-amount`,
param: "later-amount",
},
}),
dollars: {
initial: {
amount: signals.createSignal(/** @type {number | null} */ (1000), {
save: {
...utils.serde.number,
id: `${storagePrefix}-initial-amount`,
param: "initial-amount",
},
}),
},
topUp: {
amount: signals.createSignal(/** @type {number | null} */ (10), {
save: {
...utils.serde.number,
id: `${storagePrefix}-top-up-amount`,
param: "top-up-amount",
},
}),
frenquency: signals.createSignal(
/** @type {Frequency} */ (frequencies.list[0]),
{
save: {
...frequencies.serde,
id: `${storagePrefix}-top-up-freq`,
param: "top-up-freq",
},
},
),
},
},
recurrent: {
amount: signals.createSignal(/** @type {number | null} */ (100), {
save: {
...utils.serde.number,
id: `${storagePrefix}-recurrent-amount`,
param: "recurrent-amount",
swap: {
amount: {
initial: signals.createSignal(/** @type {number | null} */ (1000), {
save: {
...utils.serde.number,
id: `${storagePrefix}-initial-swap`,
param: "initial-swap",
},
}),
recurrent: signals.createSignal(/** @type {number | null} */ (10), {
save: {
...utils.serde.number,
id: `${storagePrefix}-recurrent-swap`,
param: "recurrent-swap",
},
}),
},
frequency: signals.createSignal(
/** @type {Frequency} */ (frequencies.list[0]),
{
save: {
...frequencies.serde,
id: `${storagePrefix}-swap-freq`,
param: "swap-freq",
},
},
}),
),
},
interval: {
start: signals.createSignal(
/** @type {Date | null} */ (getDefaultIntervalStart()),
/** @type {Date | null} */ (new Date("2021-04-15")),
{
save: {
...utils.serde.date,
@@ -79,16 +111,13 @@ export function init({
},
},
),
end: signals.createSignal(
/** @type {Date | null} */ (getDefaultIntervalEnd()),
{
save: {
...utils.serde.date,
id: `${storagePrefix}-interval-end`,
param: "interval-end",
},
end: signals.createSignal(/** @type {Date | null} */ (new Date()), {
save: {
...utils.serde.date,
id: `${storagePrefix}-interval-end`,
param: "interval-end",
},
),
}),
},
fees: {
percentage: signals.createSignal(/** @type {number | null} */ (0.25), {
@@ -101,223 +130,177 @@ export function init({
},
};
const { headerElement } = utils.dom.createHeader({
title: selected.title,
description: selected.serializedPath,
});
parametersElement.append(headerElement);
parametersElement.append(
utils.dom.createHeader({
title: "Save in Bitcoin",
description: "What if you bought Bitcoin in the past ?",
}).headerElement,
);
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",
parametersElement.append(
createFieldElement({
title: createColoredTypeHTML({
color: "green",
type: "Dollars",
text: "Initial Amount",
}),
description: "The amount of dollars you have ready to swap on day one.",
input: createInputDollar({
id: "simulation-dollars-initial",
title: "Initial amount of dollars converted",
signal: settings.initial.firstDay,
title: "Initial Dollar Amount",
signal: settings.dollars.initial.amount,
}),
}),
);
initialGroup.append(
createInputField({
name: "Converted over time",
parametersElement.append(
createFieldElement({
title: createColoredTypeHTML({
color: "green",
type: "Dollars",
text: "Top Up Amount",
}),
description:
"The recurrent amount of dollars you'll be putting aside to swap.",
input: createInputDollar({
id: "simulation-dollars-later",
title: "Dollars to spread over time",
signal: settings.initial.overTime,
title: "Top Up Dollar Amount",
signal: settings.dollars.topUp.amount,
}),
}),
);
const topUpGroup = createParameterGroup({
title: "Top Up",
description:
"The topUp amount of dollars you're willing to eventually save in Bitcoin.",
});
parametersElement.append(topUpGroup);
parametersElement.append(
createFieldElement({
title: createColoredTypeHTML({
color: "green",
type: "Dollars",
text: "Top Up Frequency",
}),
description:
"The frequency at which you'll be putting aside the preceding amount of dollars.",
input: utils.dom.createSelect({
id: "top-up-frequency",
list: frequencies.list,
signal: settings.dollars.topUp.frenquency,
}),
}),
);
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: "Maximum Amount",
parametersElement.append(
createFieldElement({
title: createColoredTypeHTML({
color: "orange",
type: "Swap",
text: "Initial Amount",
}),
description:
"The maximum initial amount of dollars you'll exchange on day one.",
input: createInputDollar({
id: "simulation-dollars-recurrent",
title: "Recurrent dollar amount",
signal: settings.recurrent.amount,
id: "simulation-dollars-later",
title: "Initial Swap Amount",
signal: settings.swap.amount.initial,
}),
}),
);
const frequencyUL = utils.dom.createUlElement();
recurrentGroup.append(frequencyUL);
parametersElement.append(
createFieldElement({
title: createColoredTypeHTML({
color: "orange",
type: "Swap",
text: "Recurrent Amount",
}),
description:
"The maximum recurrent amount of dollars you'll be exchanging.",
input: createInputDollar({
id: "simulation-dollars-later",
title: "Recurrent Swap Amount",
signal: settings.swap.amount.recurrent,
}),
}),
);
[
{ name: "Daily" },
{
name: "Weekly",
sub: [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
],
},
{
name: "Monthly",
sub: [
"The 1st",
"The 2nd",
"The 3rd",
"The 4th",
"The 5th",
"The 6th",
"The 7th",
"The 8th",
"The 9th",
"The 10th",
"The 11th",
"The 12th",
"The 13th",
"The 14th",
"The 15th",
"The 16th",
"The 17th",
"The 18th",
"The 19th",
"The 20th",
"The 21st",
"The 22nd",
"The 23rd",
"The 24th",
"The 25th",
"The 26th",
"The 27th",
"The 28th",
],
},
].forEach(({ name, sub }, index) => {
const li = utils.dom.createLiElement();
const { label, input } = utils.dom.createLabeledInput({
inputId: `frequency-${name}`,
inputName: "frequency",
inputValue: name.toLowerCase(),
labelTitle: name,
inputChecked: !index,
onClick: () => {},
});
label.append(name);
li.append(label);
if (sub) {
const parentName = name;
const ul = utils.dom.createUlElement();
li.append(ul);
sub.forEach((name) => {
const li = utils.dom.createLiElement();
const { label, input } = utils.dom.createLabeledInput({
inputId: `frequency-${parentName}-${name}`,
inputName: `frequency-${parentName}`,
inputValue: name.toLowerCase(),
labelTitle: name,
inputChecked: !index,
onClick: () => {},
});
label.append(name);
li.append(label);
ul.append(li);
});
}
frequencyUL.append(li);
});
parametersElement.append(
createFieldElement({
title: createColoredTypeHTML({
color: "orange",
type: "Swap",
text: "Frequency",
}),
description:
"The frequency at which you'll be exchanging the preceding amount.",
input: utils.dom.createSelect({
id: "top-up-frequency",
list: frequencies.list,
signal: settings.swap.frequency,
}),
}),
);
const frequencyChoiceUL = utils.dom.createUlElement();
recurrentGroup.append(frequencyChoiceUL);
parametersElement.append(
createFieldElement({
title: createColoredTypeHTML({
color: "sky",
type: "Interval",
text: "Start",
}),
description: "The first day of the simulation.",
input: createInputDateField({
signal: settings.interval.start,
signals,
utils,
}),
}),
);
const intervalGroup = createParameterGroup({
title: "Interval",
description: "wkfpweokf",
});
parametersElement.append(intervalGroup);
parametersElement.append(
createFieldElement({
title: createColoredTypeHTML({
color: "sky",
type: "Interval",
text: "End",
}),
description: "The last day of the simulation.",
input: createInputDateField({
signal: settings.interval.end,
signals,
utils,
}),
}),
);
console.log("weofpwklfpwkofwepokf");
parametersElement.append(
createFieldElement({
title: createColoredTypeHTML({
color: "red",
type: "Fees",
text: "Percentage",
}),
description:
"The amount of fees (in %) from where you'll be exchanging your dollars.",
input: utils.dom.createInputNumberElement({
id: "",
title: "",
signal: settings.fees.percentage,
min: 0,
max: 50,
step: 0.01,
signals,
}),
}),
);
createInputDateField({
signal: settings.interval.start,
getDefault: getDefaultIntervalStart,
parent: intervalGroup,
signals,
utils,
});
createInputDateField({
signal: settings.interval.end,
getDefault: getDefaultIntervalEnd,
parent: intervalGroup,
signals,
utils,
});
const firstParagraph = window.document.createElement("p");
resultsElement.append(firstParagraph);
const feesGroup = createParameterGroup({
title: "Fees",
description:
"The amount of fees (in %) from where you'll be exchanging your currency",
});
parametersElement.append(feesGroup);
const input = utils.dom.createInputNumberElement({
id: "",
title: "",
signal: settings.fees.percentage,
min: 0,
max: 50,
step: 0.01,
signals,
});
feesGroup.append(input);
// parametersElement.append(utils.dom.createHrElement());
// const strategyGroup = createParameterGroup({
// title: "Strategy",
// description: "The strategy used to convert your fiat into Bitcoin",
// });
// parametersElement.append(strategyGroup);
// const ulStrategies = utils.dom.createUlElement();
// strategyGroup.append(ulStrategies);
// ["All in", "Weighted Local", "Weighted Cycle"].forEach((strategy) => {
// const li = utils.dom.createLiElement();
// li.append(strategy);
// ulStrategies.append(li);
// });
const secondParagraph = window.document.createElement("p");
resultsElement.append(secondParagraph);
const parent = window.document.createElement("div");
parent.classList.add("chart-list");
resultsElement.append(parent);
signals.createEffect(settings.interval.start, (start) => {
console.log("start", start);
});
signals.createEffect(settings.interval.end, (end) => {
console.log("end", end);
});
const owner = signals.getOwner();
const closes = datasets.getOrCreate("date", "date-to-close");
@@ -325,20 +308,27 @@ export function init({
signals.runWithOwner(owner, () => {
signals.createEffect(
() => ({
initialAmount: settings.initial.firstDay() || 0,
recurrentAmount: settings.recurrent.amount() || 0,
dollarsLeft: settings.initial.overTime() || 0,
initialDollarAmount: settings.dollars.initial.amount() || 0,
topUpAmount: settings.dollars.topUp.amount() || 0,
topUpFrequency: settings.dollars.topUp.frenquency(),
initialSwap: settings.swap.amount.initial() || 0,
recurrentSwap: settings.swap.amount.recurrent() || 0,
swapFrequency: settings.swap.frequency(),
start: settings.interval.start(),
end: settings.interval.end(),
fees: settings.fees.percentage(),
}),
// ({ initialAmount, recurrentAmount, dollarsLeft, start, end }) => {
// console.log({
// start,
// end,
// });
// },
({ initialAmount, recurrentAmount, dollarsLeft, start, end, fees }) => {
({
initialDollarAmount,
topUpAmount,
topUpFrequency,
initialSwap,
recurrentSwap,
swapFrequency,
start,
end,
fees,
}) => {
console.log({ start, end });
parent.innerHTML = "";
@@ -346,8 +336,6 @@ export function init({
const range = utils.date.getRange(start, end);
let investedAmount = 0;
/** @type {LineData<Time>[]} */
const investedData = [];
/** @type {LineData<Time>[]} */
@@ -364,8 +352,20 @@ export function init({
const investmentData = [];
/** @type {LineData<Time>[]} */
const bitcoinAddedData = [];
/** @type {LineData<Time>[]} */
const averagePricePaidData = [];
/** @type {LineData<Time>[]} */
const bitcoinPriceData = [];
/** @type {LineData<Time>[]} */
const investmentsData = [];
let bitcoin = 0;
let dollars = initialDollarAmount;
let investedAmount = 0;
let investmentsCount = 0;
let averagePricePaid = 0;
let _return = 0;
let roi = 0;
let feesPaid = 0;
@@ -373,23 +373,29 @@ export function init({
const year = date.getUTCFullYear();
const time = utils.date.toString(date);
if (topUpFrequency.isTriggerDay(date)) {
dollars += topUpAmount;
}
const close = closes.fetchedJSONs
.at(utils.chunkIdToIndex("date", year))
?.json()?.dataset.map[utils.date.toString(date)];
if (!close) return;
let investmentPreFees =
(!index ? initialAmount : 0) + recurrentAmount;
if (dollarsLeft > 0) {
if (dollarsLeft >= recurrentAmount) {
investmentPreFees += recurrentAmount;
dollarsLeft -= recurrentAmount;
} else {
investmentPreFees += dollarsLeft;
dollarsLeft = 0;
}
let investmentPreFees = 0;
/** @param {number} value */
function invest(value) {
value = Math.min(dollars, value);
investmentPreFees += value;
dollars -= value;
investmentsCount += 1;
}
if (!index) {
invest(initialSwap);
}
if (swapFrequency.isTriggerDay(date) && dollars > 0) {
invest(recurrentSwap);
}
let investment = investmentPreFees * (1 - (fees || 0) / 100);
@@ -400,7 +406,16 @@ export function init({
investedAmount += investment;
const _return = close * bitcoin;
_return = close * bitcoin;
averagePricePaid = investedAmount / bitcoin;
roi = (_return / investedAmount - 1) * 100;
bitcoinPriceData.push({
time,
value: close,
});
bitcoinData.push({
time,
@@ -419,17 +434,17 @@ export function init({
resultData.push({
time,
value: (_return / investedAmount - 1) * 100,
value: roi,
});
dollarsData.push({
time,
value: dollarsLeft,
value: dollars,
});
totalData.push({
time,
value: dollarsLeft + _return,
value: dollars + _return,
});
investmentData.push({
@@ -441,8 +456,36 @@ export function init({
time,
value: bitcoinAdded,
});
averagePricePaidData.push({
time,
value: averagePricePaid,
});
investmentsData.push({
time,
value: investmentsCount,
});
});
// const { headerElement } = utils.dom.createHeader({
// title: "TItle",
// description: "Description",
// });
// parent.append(headerElement);
const f = utils.locale.numberToUSFormat;
/**
* @param {string} c
* @param {string} t
*/
const c = (c, t) => createColoredSpan({ color: c, text: t });
firstParagraph.innerHTML = `After exchanging ${c("dollar", `$${f(investedAmount)}`)} in the span of ${c("sky", f(range.length))} days, you would've accumulated ${c("orange", f(bitcoin))} Bitcoin worth ${c("dollar", `$${f(_return)}`)} at an average price of ${c("dollar", `$${f(averagePricePaid)}`)} per Bitcoin with a return of investment of ${c("yellow", `${f(roi)}%`)}.`;
secondParagraph.innerHTML = `After exchanging ${c("dollar", `$${f(investedAmount)}`)} in the span of ${c("sky", f(range.length))} days, you would've accumulated ${c("orange", f(bitcoin))} Bitcoin worth ${c("dollar", `$${f(_return)}`)} at an average price of ${c("dollar", `$${f(averagePricePaid)}`)} per Bitcoin with a return of investment of ${c("yellow", `${f(roi)}%`)}.`;
(() => {
const chartWrapper = window.document.createElement("div");
chartWrapper.classList.add("chart-wrapper");
@@ -543,6 +586,64 @@ export function init({
chart.timeScale().fitContent();
})();
(() => {
const chartWrapper = window.document.createElement("div");
chartWrapper.classList.add("chart-wrapper");
parent.append(chartWrapper);
const chartDiv = window.document.createElement("div");
chartDiv.classList.add("chart-div");
chartWrapper.append(chartDiv);
const chart = lightweightCharts.createChart({
scale: "date",
element: chartDiv,
signals,
colors,
options: {
handleScale: false,
handleScroll: false,
},
});
const line = chart.addLineSeries();
line.setData(bitcoinPriceData);
const line2 = chart.addLineSeries();
line2.setData(averagePricePaidData);
chart.timeScale().fitContent();
})();
(() => {
const chartWrapper = window.document.createElement("div");
chartWrapper.classList.add("chart-wrapper");
parent.append(chartWrapper);
const chartDiv = window.document.createElement("div");
chartDiv.classList.add("chart-div");
chartWrapper.append(chartDiv);
const chart = lightweightCharts.createChart({
scale: "date",
element: chartDiv,
signals,
colors,
options: {
handleScale: false,
handleScroll: false,
},
});
const line = chart.addLineSeries();
line.setData(investmentsData);
chart.timeScale().fitContent();
})();
},
);
});
@@ -572,20 +673,23 @@ function createInputField({ name, input }) {
* @param {Object} args
* @param {string} args.title
* @param {string} args.description
* @param {HTMLElement} args.input
*/
function createParameterGroup({ title, description }) {
function createFieldElement({ title, description, input }) {
const div = window.document.createElement("div");
const wrapper = window.document.createElement("div");
div.append(wrapper);
const label = window.document.createElement("label");
div.append(label);
const titleElement = window.document.createElement("h4");
const titleElement = window.document.createElement("span");
titleElement.innerHTML = title;
wrapper.append(titleElement);
label.append(titleElement);
const descriptionElement = window.document.createElement("small");
descriptionElement.innerHTML = description;
wrapper.append(descriptionElement);
label.append(descriptionElement);
div.append(input);
return div;
}
@@ -619,14 +723,11 @@ function createInputDollar({ id, title, signal }) {
*
* @param {Object} arg
* @param {Signal<Date | null>} arg.signal
* @param {() => Date | null} arg.getDefault
* @param {HTMLElement} arg.parent
* @param {Utilities} arg.utils
* @param {Signals} arg.signals
*/
function createInputDateField({ signal, getDefault, parent, signals, utils }) {
function createInputDateField({ signal, signals, utils }) {
const div = window.document.createElement("div");
parent.append(div);
div.append(
utils.dom.createInputDate({
@@ -638,9 +739,7 @@ function createInputDateField({ signal, getDefault, parent, signals, utils }) {
);
const button = utils.dom.createButtonElement({
onClick: () => {
signal.set(getDefault());
},
onClick: signal.reset,
text: "Reset",
title: "Reset field",
});
@@ -649,3 +748,136 @@ function createInputDateField({ signal, getDefault, parent, signals, utils }) {
return div;
}
/** @param {number} day */
function getOrdinalDay(day) {
const rest = (day % 30) % 20;
return `${day}${
rest === 1 ? "st" : rest === 2 ? "nd" : rest === 3 ? "rd" : "th"
}`;
}
/**
* @param {Object} param0
* @param {string} param0.color
* @param {string} param0.type
* @param {string} param0.text
*/
function createColoredTypeHTML({ color, type, text }) {
return `${createColoredSpan({ color, text: `${type}:` })} ${text}`;
}
/**
* @param {Object} param0
* @param {string} param0.color
* @param {string} param0.text
*/
function createColoredSpan({ color, text }) {
return `<span style="color: var(--${color}); font-weight: var(--font-weight-bold)">${text}</span>`;
}
function computeFrequencies() {
const weekDays = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
];
const maxDays = 28;
/** @satisfies {((Frequency | {name: string; list: Frequency[]})[])} */
const list = [
{
name: "Every day",
value: "every-day",
/** @param {Date} _ */
isTriggerDay(_) {
return true;
},
},
{
name: "Once a week",
list: weekDays.map((day, index) => ({
name: day,
value: day.toLowerCase(),
/** @param {Date} date */
isTriggerDay(date) {
let day = date.getUTCDay() - 1;
if (day === -1) {
day = 6;
}
return day === index;
},
})),
},
{
name: "Every two weeks",
list: [...Array(Math.round(maxDays / 2)).keys()].map((day) => {
const day1 = day + 1;
const day2 = day + 15;
return {
value: `${day1}+${day2}`,
name: `The ${getOrdinalDay(day1)} and the ${getOrdinalDay(day2)}`,
/** @param {Date} date */
isTriggerDay(date) {
const d = date.getUTCDate();
return d === day1 || d === day2;
},
};
}),
},
{
name: "Once a month",
list: [...Array(maxDays).keys()].map((day) => {
day++;
return {
name: `The ${getOrdinalDay(day)}`,
value: String(day),
/** @param {Date} date */
isTriggerDay(date) {
const d = date.getUTCDate();
return d === day;
},
};
}),
},
];
/** @type {Record<string, Frequency>} */
const idToFrequency = {};
list.forEach((anyFreq, index) => {
if ("list" in anyFreq) {
anyFreq.list?.forEach((freq) => {
idToFrequency[freq.value] = freq;
});
} else {
idToFrequency[anyFreq.value] = anyFreq;
}
});
const serde = {
/**
* @param {Frequency} v
*/
serialize(v) {
return v.value;
},
/**
* @param {string} v
*/
deserialize(v) {
const freq = idToFrequency[v];
if (!freq) throw "Freq not found";
return freq;
},
};
return { list, serde };
}
+10 -4
View File
@@ -26,7 +26,7 @@ type GrowToSize<T, N extends number, A extends T[]> = A["length"] extends N
type FixedArray<T, N extends number> = GrowToSize<T, N, []>;
type Signal<T> = Accessor<T> & { set: Setter<T> };
type Signal<T> = Accessor<T> & { set: Setter<T>; reset: VoidFunction };
type TimeScale = "date" | "height";
@@ -224,7 +224,7 @@ interface OHLC {
interface ResourceDataset<
Scale extends TimeScale,
Type extends OHLC | number = number,
Type extends OHLC | number = number
> {
scale: Scale;
url: string;
@@ -243,7 +243,7 @@ interface FetchedResult<
SingleValueData | ValuedCandlestickData
> = DatasetValue<
Type extends number ? SingleValueData : ValuedCandlestickData
>,
>
> {
at: Date | null;
json: Signal<FetchedJSON<Scale, Type> | null>;
@@ -273,7 +273,7 @@ interface FetchedChunk {
type FetchedDataset<
Scale extends TimeScale,
Type extends number | OHLC,
Type extends number | OHLC
> = Scale extends "date"
? FetchedDateDataset<Type>
: FetchedHeightDataset<Type>;
@@ -370,3 +370,9 @@ interface RatioOptions {
title: string;
list: RatioOption[];
}
interface Frequency {
name: string;
value: string;
isTriggerDay: (date: Date) => boolean;
}
+20 -1
View File
@@ -3,12 +3,31 @@
flex-direction: column;
flex: 1;
min-height: 0;
padding: var(--main-padding);
header {
flex-shrink: 0;
display: flex;
white-space: nowrap;
overflow-x: auto;
padding-bottom: 1rem;
margin-bottom: -1rem;
padding-left: var(--main-padding);
margin-left: var(--negative-main-padding);
padding-right: var(--main-padding);
margin-right: var(--negative-main-padding);
& > * {
flex: 1;
}
}
> #legend {
display: flex;
align-items: center;
gap: 1.5rem;
margin: -1rem var(--negative-main-padding);
margin: 1rem var(--negative-main-padding);
padding: 1rem var(--main-padding);
overflow-x: auto;
min-width: 0;
+34 -28
View File
@@ -5,8 +5,29 @@
width: 100%;
> div:first-child {
max-width: 20rem;
border-right: 1px;x
max-width: var(--default-main-width);
border-right: 1px;
padding-bottom: var(--bottom-area);
}
> div:last-child {
display: flex;
flex-direction: column;
gap: 1rem;
p {
font-size: var(--font-size-lg);
text-wrap: pretty;
}
}
label {
> span {
display: block;
}
small {
font-size: var(--font-size-base);
}
}
> div {
@@ -14,44 +35,29 @@
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 3rem;
gap: 2rem;
padding: var(--main-padding);
> div {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
}
div:has(> input[type="date"] + button) {
div:has(> input + button) {
display: flex;
gap: 0.5rem;
align-items: center;
align-items: baseline;
button {
color: var(--off-color);
font-size: var(--font-size-sm);
line-height: var(--line-height-sm);
}
}
.chart-list {
max-height: 500px;
/* margin-left: var(--negative-main-padding);*/
max-height: 2000px;
margin-right: calc(var(--negative-main-padding) - 0.5rem);
}
li {
label:has(input:not(:checked)) + ul {
display: none;
}
ul {
padding-left: 0.75rem;
margin-left:0.25rem;
border-left: 1px;
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
li {
display: inline;
}
}
}
}