global: snapshot

This commit is contained in:
k
2024-11-20 10:50:14 +01:00
parent 9a73ee6952
commit d01ea13de4
61 changed files with 1907 additions and 950 deletions

View File

@@ -224,14 +224,6 @@ export function init({
}
createFetchChunksOfVisibleDatasetsEffect();
function resetChartListElement() {
while (
elements.chartList.lastElementChild?.classList.contains("chart-wrapper")
) {
elements.chartList.lastElementChild?.remove();
}
}
/**
* @param {HTMLElement} parent
* @param {number} chartIndex
@@ -767,7 +759,7 @@ export function init({
/** @type {AnyDatasetPath} */
const datasetPath = `${s}-to-price`;
const dataset = datasets.getOrImport(s, datasetPath);
const dataset = datasets.getOrCreate(s, datasetPath);
// Don't trigger reactivity by design
activeDatasets().add(dataset);
@@ -806,7 +798,7 @@ export function init({
});
function createLiveCandleUpdateEffect() {
signals.createEffect(webSockets.krakenCandle.latest, (latest) => {
signals.createEffect(webSockets.kraken1dCandle.latest, (latest) => {
if (!latest) return;
const index = utils.chunkIdToIndex(s, latest.year);
@@ -825,95 +817,6 @@ export function init({
return priceSeries;
}
function resetLegendElement() {
elements.legend.innerHTML = "";
}
function initTimeScaleElement() {
const GENESIS_DAY = "2009-01-03";
/**
* @param {HTMLButtonElement} button
* @param {ChartOption} option
*/
function setTimeScale(button, option) {
const chart = charts.at(-1);
if (!chart) return;
const timeScale = chart.timeScale();
const year = button.dataset.year;
let days = button.dataset.days;
let toHeight = button.dataset.to;
switch (option.scale) {
case "date": {
let from = new Date();
let to = new Date();
to.setUTCHours(0, 0, 0, 0);
if (!days && typeof button.dataset.yearToDate === "string") {
days = String(
Math.ceil(
(to.getTime() -
new Date(`${to.getUTCFullYear()}-01-01`).getTime()) /
consts.ONE_DAY_IN_MS,
),
);
}
if (year) {
from = new Date(`${year}-01-01`);
to = new Date(`${year}-12-31`);
} else if (days) {
from.setDate(from.getUTCDate() - Number(days));
} else {
from = new Date(GENESIS_DAY);
}
timeScale.setVisibleRange({
from: /** @type {Time} */ (from.getTime() / 1000),
to: /** @type {Time} */ (to.getTime() / 1000),
});
break;
}
case "height": {
timeScale.setVisibleRange({
from: /** @type {Time} */ (0),
to: /** @type {Time} */ (Number(toHeight?.slice(0, -1)) * 1_000),
});
break;
}
}
}
/**
* @param {HTMLElement} timeScaleButtons
*/
function initGoToButtons(timeScaleButtons) {
Array.from(timeScaleButtons.children).forEach((button) => {
if (button.tagName !== "BUTTON") throw "Expect a button";
button.addEventListener("click", () => {
const option = options.selected();
if (option.kind === "chart") {
setTimeScale(/** @type {HTMLButtonElement} */ (button), option);
}
});
});
}
// initGoToButtons(elements.timeScaleDateButtons);
// initGoToButtons(elements.timeScaleHeightButtons);
// function createScaleButtonsToggleEffect() {
// const isDate = signals.createMemo(() => scale() === "date");
// signals.createEffect(isDate, (isDate) => {
// elements.timeScaleDateButtons.hidden = !isDate;
// elements.timeScaleHeightButtons.hidden = isDate;
// });
// }
// createScaleButtonsToggleEffect();
}
initTimeScaleElement();
/**
* @param {ChartOption} option
*/
@@ -933,15 +836,12 @@ export function init({
(list) => (list ? [list] : []),
);
resetLegendElement();
resetChartListElement();
/** @type {Series[]} */
const allSeries = [];
charts = chartsBlueprints.map((seriesBlueprints, chartIndex) => {
const { chartDiv, unitName, chartMode } = createChartDiv(
elements.chartList,
elements.chartsChartList,
chartIndex,
);
@@ -1148,7 +1048,7 @@ export function init({
}
[...seriesBlueprints].reverse().forEach((seriesBlueprint, index) => {
const dataset = datasets.getOrImport(
const dataset = datasets.getOrCreate(
scale,
seriesBlueprint.datasetPath,
);
@@ -1266,11 +1166,31 @@ export function init({
});
}
function resetLegendElement() {
elements.legend.innerHTML = "";
}
function resetChartListElement() {
while (
elements.chartsChartList.lastElementChild?.classList.contains(
"chart-wrapper",
)
) {
elements.chartsChartList.lastElementChild?.remove();
}
}
function reset() {
charts.forEach((chart) => chart.remove());
charts = [];
resetLegendElement();
resetChartListElement();
}
function createApplyChartOptionEffect() {
signals.createEffect(selected, (option) => {
signals.createRoot(() => {
applyChartOption(option);
});
reset();
applyChartOption(option);
});
}
createApplyChartOptionEffect();

View File

@@ -0,0 +1,41 @@
/**
* @import {Options} from './options';
*/
/**
* @param {Object} args
* @param {Colors} args.colors
* @param {Consts} args.consts
* @param {LightweightCharts} args.lightweightCharts
* @param {Signals} args.signals
* @param {Utilities} args.utils
* @param {Options} args.options
* @param {Datasets} args.datasets
* @param {WebSockets} args.webSockets
* @param {Elements} args.elements
* @param {Ids} args.ids
* @param {Accessor<boolean>} args.dark
*/
export function init({
colors,
consts,
dark,
datasets,
elements,
ids,
lightweightCharts,
options,
signals,
utils,
webSockets,
}) {
const livePriceElement = elements.livePrice;
const price = window.document.createElement("h1");
livePriceElement.append(price);
signals.createEffect(webSockets.kraken1dCandle.latest, (candle) => {
if (!candle) return;
price.innerHTML = utils.formatters.dollars.format(candle.close);
});
}

View File

@@ -4,7 +4,7 @@
* @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 {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, LogicalRange, SeriesMarker, CandlestickData, SeriesType, BaselineStyleOptions, SeriesOptionsCommon } from "../packages/lightweight-charts/v4.2.0/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"
* @import { DatePath, HeightPath, LastPath } from "./types/paths";
* @import { SignalOptions } from "../packages/solid-signals/2024-11-02/types/core/core"
* @import { getOwner as GetOwner, onCleanup as OnCleanup, Owner } from "../packages/solid-signals/2024-11-02/types/core/owner"
@@ -19,7 +19,22 @@ function initPackages() {
createSolidSignal: /** @type {CreateSignal} */ (
_signals.createSignal
),
createEffect: /** @type {CreateEffect} */ (_signals.createEffect),
createSolidEffect: /** @type {CreateEffect} */ (
_signals.createEffect
),
createEffect: /** @type {CreateEffect} */ (compute, effect) => {
let dispose = /** @type {VoidFunction | null} */ (null);
// @ts-ignore
_signals.createEffect(compute, (v) => {
dispose?.();
signals.createRoot((_dispose) => {
dispose = _dispose;
effect(v);
});
signals.onCleanup(() => dispose?.());
});
signals.onCleanup(() => dispose?.());
},
createMemo: /** @type {CreateMemo} */ (_signals.createMemo),
createRoot: /** @type {CreateRoot} */ (_signals.createRoot),
getOwner: /** @type {GetOwner} */ (_signals.getOwner),
@@ -220,8 +235,15 @@ function initPackages() {
* @param {HTMLElement} args.element
* @param {Signals} args.signals
* @param {Colors} args.colors
* @param {DeepPartial<ChartOptions>} [args.options]
*/
function createChart({ scale, element, signals, colors }) {
function createChart({
scale,
element,
signals,
colors,
options: _options = {},
}) {
/** @satisfies {DeepPartial<ChartOptions>} */
const options = {
autoSize: true,
@@ -257,6 +279,7 @@ function initPackages() {
}
: {}),
},
..._options,
};
/** @type {IChartApi} */
@@ -597,7 +620,6 @@ function initPackages() {
let packagePromise = null;
return function () {
let p = null;
if (!packagePromise) {
// @ts-ignore
packagePromise = imports[key]();
@@ -640,6 +662,20 @@ const utils = {
yield() {
return this.sleep(0);
},
array: {
/**
* @param {number} start
* @param {number} end
*/
range(start, end) {
const range = [];
while (start <= end) {
range.push(start);
start += 1;
}
return range;
},
},
dom: {
/**
* @param {string} id
@@ -743,13 +779,16 @@ const utils = {
/**
* @param {Object} arg
* @param {string} arg.text
* @param {string} arg.title
* @param {VoidFunction} arg.onClick
*/
createButtonElement({ text, onClick }) {
createButtonElement({ text, onClick, title }) {
const button = window.document.createElement("button");
button.innerHTML = text;
button.title = title;
button.addEventListener("click", onClick);
return button;
@@ -773,6 +812,8 @@ const utils = {
}) {
const label = window.document.createElement("label");
inputId = inputId.toLowerCase();
const input = window.document.createElement("input");
input.type = "radio";
input.name = inputName;
@@ -898,6 +939,130 @@ const utils = {
return field;
},
createUlElement() {
return window.document.createElement("ul");
},
createLiElement() {
return window.document.createElement("li");
},
/**
* @param {Object} args
* @param {string} args.id
* @param {string} args.title
* @param {Signal<number | null>} args.signal
* @param {number} args.min
* @param {number} args.max
* @param {number} args.step
* @param {{createEffect: typeof CreateEffect}} args.signals
*/
createInputNumberElement({ id, title, signal, min, max, step, signals }) {
const input = window.document.createElement("input");
input.id = id;
input.title = title;
input.type = "number";
input.min = String(min);
input.max = String(max);
input.step = String(step);
let stateValue = /** @type {string | null} */ (null);
signals.createEffect(
() => {
const value = signal();
return value ? String(value) : "";
},
(value) => {
if (stateValue !== value) {
input.value = value;
stateValue = value;
}
},
);
input.addEventListener("input", () => {
const valueSer = input.value;
const value = Number(valueSer);
if (value >= min && value <= max) {
stateValue = valueSer;
signal.set(value);
}
});
return input;
},
/**
* @param {Object} args
* @param {string} args.id
* @param {string} args.title
* @param {Signal<Date | null>} args.signal
* @param {{createEffect: typeof CreateEffect}} args.signals
*/
createInputDate({ id, title, signal, signals }) {
const input = window.document.createElement("input");
input.id = id;
input.title = title;
input.type = "date";
const min = "2011-01-01";
const minDate = new Date(min);
const maxDate = new Date();
const max = utils.date.toString(maxDate);
input.min = min;
input.max = max;
let stateValue = /** @type {string | null} */ (null);
signals.createEffect(
() => {
const date = signal();
return date ? utils.date.toString(date) : "";
},
(value) => {
if (stateValue !== value) {
input.value = value;
stateValue = value;
}
},
);
input.addEventListener("change", () => {
const value = input.value;
const date = new Date(value);
if (date >= minDate && date <= maxDate) {
stateValue = value;
signal.set(value ? date : null);
}
});
return input;
},
/**
* @param {Object} param0
* @param {string} param0.title
* @param {string} param0.description
*/
createHeader({ title, description }) {
const headerElement = window.document.createElement("header");
const div = window.document.createElement("div");
headerElement.append(div);
const h1 = window.document.createElement("h1");
div.append(h1);
const titleElement = window.document.createElement("span");
titleElement.append(title);
h1.append(titleElement);
const descriptionElement = window.document.createElement("small");
descriptionElement.append(description);
h1.append(descriptionElement);
return {
headerElement,
titleElement,
descriptionElement,
};
},
},
url: {
chartParamsWhitelist: ["from", "to"],
@@ -1124,6 +1289,20 @@ const utils = {
return Number(v);
},
},
date: {
/**
* @param {Date} v
*/
serialize(v) {
return utils.date.toString(v);
},
/**
* @param {string} v
*/
deserialize(v) {
return new Date(v);
},
},
},
formatters: {
dollars: new Intl.NumberFormat("en-US", {
@@ -1168,6 +1347,25 @@ const utils = {
: // @ts-ignore
new Date(time.year, time.month, time.day);
},
/**
* @param {Date} start
*/
getRangeUpToToday(start) {
return this.getRange(start, new Date());
},
/**
* @param {Date} start
* @param {Date} end
*/
getRange(start, end) {
const dates = /** @type {Date[]} */ ([]);
let currentDate = new Date(start);
while (currentDate <= end) {
dates.push(new Date(currentDate));
currentDate.setUTCDate(currentDate.getUTCDate() + 1);
}
return dates;
},
},
/**
*
@@ -1328,14 +1526,14 @@ const elements = {
selectedTitle: utils.dom.getElementById("selected-title"),
selectedDescription: utils.dom.getElementById("selected-description"),
selectors: utils.dom.getElementById("frame-selectors"),
chartList: utils.dom.getElementById("chart-list"),
chartsChartList: utils.dom.getElementById("charts-chart-list"),
legend: utils.dom.getElementById("legend"),
style: getComputedStyle(window.document.documentElement),
// timeScaleDateButtons: utils.dom.getElementById("timescale-date-buttons"),
// timeScaleHeightButtons: utils.dom.getElementById("timescale-height-buttons"),
selectedHeader: utils.dom.getElementById("selected-header"),
charts: utils.dom.getElementById("charts"),
simulation: utils.dom.getElementById("simulation"),
livePrice: utils.dom.getElementById("live-price"),
moscowTime: utils.dom.getElementById("moscow-time"),
};
/** @typedef {typeof elements} Elements */
@@ -1951,6 +2149,21 @@ function createDatasets(signals) {
scale,
url: baseURL,
fetch: _fetch,
fetchRange(start, end) {
const promises = /** @type {Promise<void>[]} */ ([]);
switch (scale) {
case "date": {
utils.array.range(start, end).forEach((year) => {
promises.push(this.fetch(year));
});
break;
}
default: {
throw "Unsupported";
}
}
return Promise.all(promises);
},
fetchedJSONs,
// drop() {
// dispose();
@@ -1985,7 +2198,7 @@ function createDatasets(signals) {
* @param {DatasetPath<S>} path
* @returns {ResourceDataset<S>}
*/
function getOrImport(scale, path) {
function getOrCreate(scale, path) {
if (scale === "date") {
const found = date.get(/** @type {DatePath} */ (path));
if (found) return /** @type {ResourceDataset<S>} */ (found);
@@ -2018,7 +2231,7 @@ function createDatasets(signals) {
}
return {
getOrImport,
getOrCreate,
};
}
/** @typedef {ReturnType<typeof createDatasets>} Datasets */
@@ -2088,18 +2301,18 @@ function initWebSockets(signals) {
/**
* @param {(candle: DatasetCandlestickData) => void} callback
* @returns
* @param {number} interval
*/
function krakenCandleWebSocketCreator(callback) {
const ws = new WebSocket("wss://ws.kraken.com");
function krakenCandleWebSocketCreator(callback, interval) {
const ws = new WebSocket("wss://ws.kraken.com/v2");
ws.addEventListener("open", () => {
ws.send(
JSON.stringify({
event: "subscribe",
pair: ["XBT/USD"],
subscription: {
name: "ohlc",
method: "subscribe",
params: {
channel: "ohlc",
symbol: ["BTC/USD"],
interval: 1440,
},
}),
@@ -2109,11 +2322,11 @@ function initWebSockets(signals) {
ws.addEventListener("message", (message) => {
const result = JSON.parse(message.data);
if (!Array.isArray(result)) return;
if (result.channel !== "ohlc") return;
const [timestamp, _, open, high, low, close, __, volume] = result[1];
const { timestamp, open, high, low, close } = result.data.at(-1);
const date = new Date(Number(timestamp) * 1000);
const date = new Date(timestamp);
const dateStr = utils.date.toString(date);
@@ -2134,12 +2347,18 @@ function initWebSockets(signals) {
return ws;
}
const krakenCandle = createWebsocket(krakenCandleWebSocketCreator);
const kraken1dCandle = createWebsocket((callback) =>
krakenCandleWebSocketCreator(callback, 1440),
);
const kraken5mnCandle = createWebsocket((callback) =>
krakenCandleWebSocketCreator(callback, 5),
);
krakenCandle.open();
kraken1dCandle.open();
kraken5mnCandle.open();
function createDocumentTitleEffect() {
signals.createEffect(krakenCandle.latest, (latest) => {
signals.createEffect(kraken5mnCandle.latest, (latest) => {
if (latest) {
const close = latest.close;
console.log("close:", close);
@@ -2153,7 +2372,8 @@ function initWebSockets(signals) {
createDocumentTitleEffect();
return {
krakenCandle,
kraken1dCandle,
// kraken5mnCandle,
};
}
/** @typedef {ReturnType<typeof initWebSockets>} WebSockets */
@@ -2252,6 +2472,8 @@ packages.signals().then((signals) =>
);
let firstChartOption = true;
let firstSimulationOption = true;
let firstLivePriceOption = true;
let firstMoscowTimeOption = true;
signals.createEffect(options.selected, (option) => {
if (previousElement) {
@@ -2262,7 +2484,13 @@ packages.signals().then((signals) =>
utils.url.replaceHistory({ pathname: option.id });
}
const hideTop = option.kind === "home" || option.kind === "pdf";
const hideTop =
option.kind === "home" ||
option.kind === "pdf" ||
option.kind === "live-price" ||
option.kind === "converter" ||
option.kind === "moscow-time";
elements.selectedHeader.hidden = hideTop;
elements.selectedTitle.innerHTML = option.title;
@@ -2277,6 +2505,8 @@ packages.signals().then((signals) =>
// break;
// }
case "chart": {
console.log("chart", option);
element = elements.charts;
lastChartOption.set(option);
@@ -2333,7 +2563,7 @@ packages.signals().then((signals) =>
ids,
lightweightCharts,
options,
selected: /** @type {any} */ (lastChartOption),
selected: option,
signals,
utils,
webSockets,
@@ -2347,7 +2577,77 @@ packages.signals().then((signals) =>
break;
}
default: {
case "live-price": {
console.log("live-price");
element = elements.livePrice;
if (firstLivePriceOption) {
const lightweightCharts = packages.lightweightCharts();
const script = import("./live-price.js");
utils.dom.importStyleAndThen("/styles/live-price.css", () =>
script.then(({ init }) =>
lightweightCharts.then((lightweightCharts) =>
signals.runWithOwner(owner, () =>
init({
colors,
consts,
dark,
datasets,
elements,
ids,
lightweightCharts,
options,
signals,
utils,
webSockets,
}),
),
),
),
);
}
firstLivePriceOption = false;
break;
}
case "moscow-time": {
console.log("moscow-time");
element = elements.moscowTime;
if (firstLivePriceOption) {
const lightweightCharts = packages.lightweightCharts();
const script = import("./moscow-time.js");
utils.dom.importStyleAndThen("/styles/moscow-time.css", () =>
script.then(({ init }) =>
signals.runWithOwner(owner, () =>
init({
colors,
consts,
dark,
datasets,
elements,
ids,
options,
signals,
utils,
webSockets,
}),
),
),
);
}
firstLivePriceOption = false;
break;
}
case "converter":
case "home":
case "pdf":
case "url": {
return;
}
}

View File

@@ -0,0 +1,45 @@
/**
* @import {Options} from './options';
*/
/**
* @param {Object} args
* @param {Colors} args.colors
* @param {Consts} args.consts
* @param {Signals} args.signals
* @param {Utilities} args.utils
* @param {Options} args.options
* @param {Datasets} args.datasets
* @param {WebSockets} args.webSockets
* @param {Elements} args.elements
* @param {Ids} args.ids
* @param {Accessor<boolean>} args.dark
*/
export function init({
colors,
consts,
dark,
datasets,
elements,
ids,
options,
signals,
utils,
webSockets,
}) {
const moscowTimeElement = elements.moscowTime;
const satsPerDollar = signals.createMemo(
() =>
100_000_000 /
// webSockets.kraken5mnCandle.latest()?.close ||
(webSockets.kraken1dCandle.latest()?.close || 0),
);
const p = window.document.createElement("h1");
moscowTimeElement.append(p);
signals.createEffect(satsPerDollar, (satsPerDollar) => {
p.innerHTML = utils.formatters.dollars.format(satsPerDollar);
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
* @param {Colors} args.colors
* @param {Consts} args.consts
* @param {LightweightCharts} args.lightweightCharts
* @param {Accessor<ChartOption>} args.selected
* @param {SimulationOption} args.selected
* @param {Signals} args.signals
* @param {Utilities} args.utils
* @param {Options} args.options
@@ -38,22 +38,27 @@ export function init({
const resultsElement = window.document.createElement("div");
simulationElement.append(resultsElement);
const getDefaultIntervalStart = () => new Date("2021-04-15");
const getDefaultIntervalEnd = () => new Date();
const storagePrefix = "save-in-bitcoin";
const settings = {
initial: signals.createSignal(/** @type {number | null} */ (1000), {
save: {
...utils.serde.number,
id: `${storagePrefix}-initial-amount`,
param: "initial-amount",
},
}),
later: signals.createSignal(/** @type {number | null} */ (0), {
save: {
...utils.serde.number,
id: `${storagePrefix}-later-amount`,
param: "later-amount",
},
}),
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",
},
}),
},
recurrent: {
amount: signals.createSignal(/** @type {number | null} */ (100), {
save: {
@@ -63,8 +68,45 @@ export function init({
},
}),
},
interval: {
start: signals.createSignal(
/** @type {Date | null} */ (getDefaultIntervalStart()),
{
save: {
...utils.serde.date,
id: `${storagePrefix}-interval-start`,
param: "interval-start",
},
},
),
end: signals.createSignal(
/** @type {Date | null} */ (getDefaultIntervalEnd()),
{
save: {
...utils.serde.date,
id: `${storagePrefix}-interval-end`,
param: "interval-end",
},
},
),
},
fees: {
percentage: signals.createSignal(/** @type {number | null} */ (0.25), {
save: {
...utils.serde.number,
id: `${storagePrefix}-percentage`,
param: "percentage",
},
}),
},
};
const { headerElement } = utils.dom.createHeader({
title: selected.title,
description: selected.serializedPath,
});
parametersElement.append(headerElement);
const initialGroup = createParameterGroup({
title: "Initial",
description:
@@ -78,7 +120,7 @@ export function init({
input: createInputDollar({
id: "simulation-dollars-initial",
title: "Initial amount of dollars converted",
signal: settings.initial,
signal: settings.initial.firstDay,
}),
}),
);
@@ -89,12 +131,17 @@ export function init({
input: createInputDollar({
id: "simulation-dollars-later",
title: "Dollars to spread over time",
signal: settings.later,
signal: settings.initial.overTime,
}),
}),
);
parametersElement.append(createHrElement());
const topUpGroup = createParameterGroup({
title: "Top Up",
description:
"The topUp amount of dollars you're willing to eventually save in Bitcoin.",
});
parametersElement.append(topUpGroup);
const recurrentGroup = createParameterGroup({
title: "Recurrent",
@@ -105,7 +152,7 @@ export function init({
recurrentGroup.append(
createInputField({
name: "Amount",
name: "Maximum Amount",
input: createInputDollar({
id: "simulation-dollars-recurrent",
title: "Recurrent dollar amount",
@@ -114,34 +161,92 @@ export function init({
}),
);
const frequencyUL = appendUl({ parent: recurrentGroup });
[{ name: "Daily" }, { name: "Weekly" }, { name: "Monthly" }].forEach(
({ name }) => {
const li = appendLi({ name, parent: frequencyUL });
},
);
const frequencyChoiceUL = appendUl({ parent: recurrentGroup });
const frequencyUL = utils.dom.createUlElement();
recurrentGroup.append(frequencyUL);
[
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
].forEach((name) => {
const li = appendLi({ name, parent: frequencyChoiceUL });
{ 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(createHrElement());
const today = signals.createSignal(utils.date.todayUTC());
setInterval(() => {
today.set(utils.date.todayUTC());
}, consts.FIVE_SECONDS_IN_MS);
const frequencyChoiceUL = utils.dom.createUlElement();
recurrentGroup.append(frequencyChoiceUL);
const intervalGroup = createParameterGroup({
title: "Interval",
@@ -149,42 +254,22 @@ export function init({
});
parametersElement.append(intervalGroup);
/**
* @param {Object} args
* @param {HTMLElement} args.parent
*/
function appendDiv({ parent }) {
const div = window.document.createElement("div");
parent.append(div);
return div;
}
console.log("weofpwklfpwkofwepokf");
function createInputDateField() {
const div = appendDiv({ parent: intervalGroup });
appendInputDate({
id: "",
title: "",
value: "2021-04-15",
parent: div,
signals,
today,
});
appendButton({
onClick: () => {},
text: "Reset",
title: "",
parent: div,
});
return div;
}
createInputDateField();
createInputDateField();
parametersElement.append(createHrElement());
createInputDateField({
signal: settings.interval.start,
getDefault: getDefaultIntervalStart,
parent: intervalGroup,
signals,
utils,
});
createInputDateField({
signal: settings.interval.end,
getDefault: getDefaultIntervalEnd,
parent: intervalGroup,
signals,
utils,
});
const feesGroup = createParameterGroup({
title: "Fees",
@@ -193,39 +278,275 @@ export function init({
});
parametersElement.append(feesGroup);
createInputNumber({
const input = utils.dom.createInputNumberElement({
id: "",
title: "",
value: 0.25,
parent: feesGroup,
signal: settings.fees.percentage,
min: 0,
max: 10,
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 parent = window.document.createElement("div");
parent.classList.add("chart-list");
resultsElement.append(parent);
signals.createEffect(settings.interval.start, (start) => {
console.log("start", start);
});
parametersElement.append(createHrElement());
const strategyGroup = createParameterGroup({
title: "Strategy",
description: "The strategy used to convert your fiat into Bitcoin",
signals.createEffect(settings.interval.end, (end) => {
console.log("end", end);
});
parametersElement.append(strategyGroup);
const ulStrategies = appendUl({ parent: strategyGroup });
const owner = signals.getOwner();
["All in", "Weighted Local", "Weighted Cycle"].forEach((strategy) => {
appendLi({
name: strategy,
parent: ulStrategies,
const closes = datasets.getOrCreate("date", "date-to-close");
closes.fetchRange(2009, new Date().getUTCFullYear()).then(() => {
signals.runWithOwner(owner, () => {
signals.createEffect(
() => ({
initialAmount: settings.initial.firstDay() || 0,
recurrentAmount: settings.recurrent.amount() || 0,
dollarsLeft: settings.initial.overTime() || 0,
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 }) => {
console.log({ start, end });
parent.innerHTML = "";
if (!start || !end || start > end) return;
const range = utils.date.getRange(start, end);
let investedAmount = 0;
/** @type {LineData<Time>[]} */
const investedData = [];
/** @type {LineData<Time>[]} */
const returnData = [];
/** @type {LineData<Time>[]} */
const bitcoinData = [];
/** @type {LineData<Time>[]} */
const resultData = [];
/** @type {LineData<Time>[]} */
const dollarsData = [];
/** @type {LineData<Time>[]} */
const totalData = [];
/** @type {LineData<Time>[]} */
const investmentData = [];
/** @type {LineData<Time>[]} */
const bitcoinAddedData = [];
let bitcoin = 0;
let feesPaid = 0;
range.forEach((date, index) => {
const year = date.getUTCFullYear();
const time = utils.date.toString(date);
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 investment = investmentPreFees * (1 - (fees || 0) / 100);
feesPaid += investmentPreFees - investment;
const bitcoinAdded = investment / close;
bitcoin += bitcoinAdded;
investedAmount += investment;
const _return = close * bitcoin;
bitcoinData.push({
time,
value: bitcoin,
});
investedData.push({
time,
value: investedAmount,
});
returnData.push({
time,
value: _return,
});
resultData.push({
time,
value: (_return / investedAmount - 1) * 100,
});
dollarsData.push({
time,
value: dollarsLeft,
});
totalData.push({
time,
value: dollarsLeft + _return,
});
investmentData.push({
time,
value: investment,
});
bitcoinAddedData.push({
time,
value: bitcoinAdded,
});
});
(() => {
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(investedData);
const line2 = chart.addLineSeries();
line2.setData(returnData);
const line3 = chart.addLineSeries();
line3.setData(dollarsData);
const line4 = chart.addLineSeries();
line4.setData(totalData);
const line5 = chart.addLineSeries();
line5.setData(investmentData);
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(bitcoinData);
const line2 = chart.addLineSeries();
line2.setData(bitcoinAddedData);
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(resultData);
chart.timeScale().fitContent();
})();
},
);
});
});
//
// On the side
// Value in Bitcoin
// Value in Dollars + total converted
//
// Value min estimated value in 4 years
//
}
/**
@@ -269,31 +590,6 @@ function createParameterGroup({ title, description }) {
return div;
}
function createHrElement() {
return window.document.createElement("hr");
}
/**
*@param {Object} args
*@param {string} args.id
*@param {string} args.title
*@param {number} args.value
*@param {HTMLElement} args.parent
*@param {number} args.min
*@param {number} args.max
*/
function createInputNumber({ id, title, value, parent, min, max }) {
const input = window.document.createElement("input");
input.id = id;
input.title = title;
input.type = "number";
input.value = String(value);
input.min = String(min);
input.max = String(max);
parent.append(input);
return input;
}
/**
* @param {Object} args
* @param {string} args.id
@@ -320,90 +616,36 @@ function createInputDollar({ id, title, signal }) {
}
/**
* @param {Object} args
* @param {string} args.id
* @param {string} args.title
* @param {Signal<number | null>} args.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 createInputRangePercentage({ id, title, signal }) {
const input = window.document.createElement("input");
input.id = id;
input.title = title;
input.type = "range";
input.min = "0";
input.max = "100";
function createInputDateField({ signal, getDefault, parent, signals, utils }) {
const div = window.document.createElement("div");
parent.append(div);
const value = signal();
input.value = value !== null ? String(value) : "";
div.append(
utils.dom.createInputDate({
id: "",
title: "",
signal,
signals,
}),
);
input.addEventListener("input", () => {
const value = input.value;
signal.set(value ? Number(value) : null);
const button = utils.dom.createButtonElement({
onClick: () => {
signal.set(getDefault());
},
text: "Reset",
title: "Reset field",
});
return input;
}
/**
* @param {Object} args
* @param {HTMLElement} args.parent
*/
function appendUl({ parent }) {
const ul = window.document.createElement("ul");
parent.append(ul);
return ul;
}
/**
* @param {Object} args
* @param {string} args.name
* @param {HTMLUListElement} args.parent
*/
function appendLi({ name, parent }) {
const li = window.document.createElement("li");
li.innerHTML = name;
parent.append(li);
return li;
}
/**
*@param {Object} args
*@param {string} args.id
*@param {string} args.title
*@param {string} args.value
*@param {HTMLElement} args.parent
*@param {Accessor<Date>} args.today
*@param {Signals} args.signals
*/
function appendInputDate({ id, title, value, parent, today, signals }) {
const input = window.document.createElement("input");
input.id = id;
input.title = title;
input.type = "date";
input.value = value;
input.min = "2011-01-01";
signals.createEffect(today, (today) => {
input.max = today.toJSON().split("T")[0];
});
parent.append(input);
return input;
}
/**
*@param {Object} args
*@param {string} args.title
*@param {string} args.text
*@param {HTMLElement} args.parent
*@param {VoidFunction} args.onClick
*/
function appendButton({ title, text, onClick, parent }) {
const button = window.document.createElement("button");
button.title = title;
button.innerHTML = text;
button.addEventListener("click", onClick);
parent.append(button);
return button;
div.append(button);
return div;
}

View File

@@ -101,7 +101,6 @@ type Unit =
| "Weight";
interface PartialOption {
// icon: string;
name: string;
}
@@ -111,6 +110,18 @@ interface PartialHomeOption extends PartialOption {
name: "Home";
}
interface PartialLivePriceOption extends PartialOption {
kind: "live-price";
}
interface PartialMoscowTimeOption extends PartialOption {
kind: "moscow-time";
}
interface PartialConverterOption extends PartialOption {
kind: "converter";
}
interface PartialChartOption extends PartialOption {
scale: TimeScale;
title: string;
@@ -147,6 +158,9 @@ interface PartialOptionsGroup {
type AnyPartialOption =
| PartialHomeOption
| PartialLivePriceOption
| PartialMoscowTimeOption
| PartialConverterOption
| PartialChartOption
| PartialSimulationOption
| PartialPdfOption
@@ -168,6 +182,9 @@ type OptionPath = {
type HomeOption = PartialHomeOption & ProcessedOptionAddons;
type SimulationOption = PartialSimulationOption & ProcessedOptionAddons;
type LivePriceOption = PartialLivePriceOption & ProcessedOptionAddons;
type MoscowTimeOption = PartialMoscowTimeOption & ProcessedOptionAddons;
type ConverterOption = PartialConverterOption & ProcessedOptionAddons;
interface PdfOption extends PartialPdfOption, ProcessedOptionAddons {
kind: "pdf";
@@ -183,6 +200,9 @@ interface ChartOption extends PartialChartOption, ProcessedOptionAddons {
type Option =
| HomeOption
| LivePriceOption
| MoscowTimeOption
| ConverterOption
| PdfOption
| UrlOption
| ChartOption
@@ -208,7 +228,8 @@ interface ResourceDataset<
> {
scale: Scale;
url: string;
fetch: (id: number) => void;
fetch: (id: number) => Promise<void>;
fetchRange: (start: number, end: number) => Promise<void[]>;
fetchedJSONs: FetchedResult<Scale, Type>[];
// drop: VoidFunction;
}