mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 06:01:57 -07:00
general: snapshot
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -145,8 +145,8 @@ impl Binance {
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>())
|
||||
},
|
||||
30,
|
||||
10,
|
||||
5,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ impl Binance {
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>())
|
||||
},
|
||||
10,
|
||||
30,
|
||||
10,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ impl Kraken {
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>())
|
||||
},
|
||||
10,
|
||||
30,
|
||||
10,
|
||||
)
|
||||
}
|
||||
@@ -115,7 +115,7 @@ impl Kraken {
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>())
|
||||
},
|
||||
10,
|
||||
30,
|
||||
10,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
}
|
||||
|
||||
Vendored
+10
-4
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user