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
+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;
}