bindex: contained fjall code

This commit is contained in:
nym21
2025-01-27 23:25:28 +01:00
parent 90a5c4fbf8
commit d68c6f9f2e
172 changed files with 397 additions and 254 deletions

163
website/scripts/chart.js Normal file
View File

@@ -0,0 +1,163 @@
// @ts-check
/**
* @param {Object} args
* @param {Colors} args.colors
* @param {LightweightCharts} args.lightweightCharts
* @param {Accessor<ChartOption>} args.selected
* @param {Signals} args.signals
* @param {Utilities} args.utils
* @param {Datasets} args.datasets
* @param {WebSockets} args.webSockets
* @param {Elements} args.elements
*/
export function init({
colors,
datasets,
elements,
lightweightCharts,
selected,
signals,
utils,
webSockets,
}) {
console.log("init chart state");
const scale = signals.createMemo(() => selected().scale);
elements.charts.append(utils.dom.createShadow("left"));
elements.charts.append(utils.dom.createShadow("right"));
const { headerElement, titleElement, descriptionElement } =
utils.dom.createHeader({});
elements.charts.append(headerElement);
signals.createEffect(selected, (option) => {
titleElement.innerHTML = option.title;
descriptionElement.innerHTML = option.serializedPath;
});
const chart = lightweightCharts.createChart({
parent: elements.charts,
signals,
colors,
id: "chart",
scale: scale(),
kind: "moveable",
utils,
});
const activeDatasets = signals.createSignal(
/** @type {Set<ResourceDataset<any, any>>} */ (new Set()),
{
equals: false,
},
);
function createFetchChunksOfVisibleDatasetsEffect() {
signals.createEffect(
() => ({
ids: chart.visibleDatasetIds(),
activeDatasets: activeDatasets(),
}),
({ ids, activeDatasets }) => {
const datasets = Array.from(activeDatasets);
if (ids.length === 0 || datasets.length === 0) return;
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
for (let j = 0; j < datasets.length; j++) {
datasets[j].fetch(id);
}
}
},
);
}
createFetchChunksOfVisibleDatasetsEffect();
/**
* @param {ChartOption} option
*/
function applyChartOption(option) {
const scale = option.scale;
chart.visibleTimeRange.set(chart.getInitialVisibleTimeRange());
activeDatasets.set((s) => {
s.clear();
return s;
});
const chartsBlueprints = [option.top || [], option.bottom].flatMap(
(list) => (list ? [list] : []),
);
chartsBlueprints.map((seriesBlueprints, paneIndex) => {
const chartPane = chart.createPane({
paneIndex,
unit: paneIndex ? option.unit : "US Dollars",
});
if (!paneIndex) {
/** @type {AnyDatasetPath} */
const datasetPath = `${scale}-to-price`;
const dataset = datasets.getOrCreate(scale, datasetPath);
// Don't trigger reactivity by design
activeDatasets().add(dataset);
const priceSeries = chartPane.createSplitSeries({
blueprint: {
datasetPath,
title: "BTC Price",
type: "Candlestick",
},
dataset,
id: option.id,
index: -1,
});
signals.createEffect(webSockets.kraken1dCandle.latest, (latest) => {
if (!latest) return;
const index = utils.chunkIdToIndex(scale, latest.year);
priceSeries.forEach((splitSeries) => {
const series = splitSeries.chunks.at(index);
if (series) {
signals.createEffect(series, (series) => {
series?.update(latest);
});
}
});
});
}
[...seriesBlueprints].reverse().forEach((blueprint, index) => {
const dataset = datasets.getOrCreate(scale, blueprint.datasetPath);
// Don't trigger reactivity by design
activeDatasets().add(dataset);
chartPane.createSplitSeries({
index,
blueprint,
id: option.id,
dataset,
});
});
activeDatasets.set((s) => s);
return chart;
});
}
function createApplyChartOptionEffect() {
signals.createEffect(selected, (option) => {
chart.reset({ scale: option.scale, owner: signals.getOwner() });
applyChartOption(option);
});
}
createApplyChartOptionEffect();
}

View File

@@ -0,0 +1,43 @@
// @ts-check
/**
* @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);
});
}

2697
website/scripts/main.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
// @ts-check
/**
* @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);
});
}

5659
website/scripts/options.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,89 @@
// @ts-check
const version = "v1";
self.addEventListener("install", (_event) => {
console.log("service-worker: install");
const event = /** @type {any} */ (_event);
event.waitUntil(
caches.open(version).then((cache) => {
return cache.addAll([
"/",
"/index.html",
"/assets/fonts/satoshi/2024-09/font.var.woff2",
"/scripts/main.js",
"/scripts/options.js",
"/scripts/chart.js",
"/styles/chart.css",
"/scripts/simulation.js",
"/styles/simulation.css",
"/packages/lean-qr/v2.3.4/script.js",
"/packages/lightweight-charts/v4.2.2/script.js",
"/packages/solid-signals/2024-11-02/script.js",
"/packages/ufuzzy/v1.0.14/script.js",
]);
}),
);
// @ts-ignore
self.skipWaiting();
});
self.addEventListener("fetch", (_event) => {
const event = /** @type {any} */ (_event);
/** @type {Request} */
let request = event.request;
const method = request.method;
let url = request.url;
const { pathname, origin } = new URL(url);
const slashMatches = url.match(/\//g);
const dotMatches = pathname.split("/").at(-1)?.match(/./g);
const endsWithDotHtml = pathname.endsWith(".html");
const slashApiSlashMatches = url.match(/\/api\//g);
if (
slashMatches &&
slashMatches.length <= 3 &&
!slashApiSlashMatches &&
(!dotMatches || endsWithDotHtml)
) {
url = `${origin}/`;
}
request = new Request(url, request.mode !== "navigate" ? request : undefined);
console.log(`service-worker: fetching: ${url}`);
event.respondWith(
caches.match(request).then(async (cachedResponse) => {
return fetch(request)
.then((response) => {
const { status } = response;
if (method !== "GET" || slashApiSlashMatches) {
// API calls are cached in script.js
return response;
} else if (status === 200 || status === 304) {
if (status === 200) {
const clonedResponse = response.clone();
caches.open(version).then((cache) => {
cache.put(request, clonedResponse);
});
}
return response;
} else {
return cachedResponse || response;
}
})
.catch(() => {
console.log("service-worker: offline");
return cachedResponse;
});
}),
);
});

View File

@@ -0,0 +1,995 @@
// @ts-check
/**
* @import { Options } from './options';
* @import { ColorName, Frequencies, Frequency } from './types/self';
*/
/**
* @param {Object} args
* @param {Colors} args.colors
* @param {LightweightCharts} args.lightweightCharts
* @param {Signals} args.signals
* @param {Utilities} args.utils
* @param {Datasets} args.datasets
* @param {Elements} args.elements
* @param {Signal<LastValues>} args.lastValues
*/
export function init({
colors,
datasets,
elements,
lightweightCharts,
signals,
utils,
lastValues,
}) {
const simulationElement = elements.simulation;
const parametersElement = window.document.createElement("div");
simulationElement.append(parametersElement);
const resultsElement = window.document.createElement("div");
simulationElement.append(resultsElement);
const frequencies = computeFrequencies();
const keyPrefix = "save-in-bitcoin";
const settings = {
dollars: {
initial: {
amount: signals.createSignal(/** @type {number | null} */ (1000), {
save: {
...utils.serde.number,
keyPrefix,
key: "initial-amount",
},
}),
},
topUp: {
amount: signals.createSignal(/** @type {number | null} */ (150), {
save: {
...utils.serde.number,
keyPrefix,
key: "top-up-amount",
},
}),
frenquency: signals.createSignal(
/** @type {Frequency} */ (frequencies.list[3].list[0]),
{
save: {
...frequencies.serde,
keyPrefix,
key: "top-up-freq",
},
},
),
},
},
bitcoin: {
investment: {
initial: signals.createSignal(/** @type {number | null} */ (1000), {
save: {
...utils.serde.number,
keyPrefix,
key: "initial-swap",
},
}),
recurrent: signals.createSignal(/** @type {number | null} */ (5), {
save: {
...utils.serde.number,
keyPrefix,
key: "recurrent-swap",
},
}),
frequency: signals.createSignal(
/** @type {Frequency} */ (frequencies.list[0]),
{
save: {
...frequencies.serde,
keyPrefix,
key: "swap-freq",
},
},
),
},
},
interval: {
start: signals.createSignal(
/** @type {Date | null} */ (new Date("2021-04-15")),
{
save: {
...utils.serde.date,
keyPrefix,
key: "interval-start",
},
},
),
end: signals.createSignal(/** @type {Date | null} */ (new Date()), {
save: {
...utils.serde.date,
keyPrefix,
key: "interval-end",
},
}),
},
fees: {
percentage: signals.createSignal(/** @type {number | null} */ (0.25), {
save: {
...utils.serde.number,
keyPrefix,
key: "percentage",
},
}),
},
};
parametersElement.append(
utils.dom.createHeader({
title: "Save in Bitcoin",
description: "What if you bought Bitcoin in the past ?",
}).headerElement,
);
/**
* @param {Object} param0
* @param {ColorName} 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 {ColorName} param0.color
* @param {string} param0.text
*/
function createColoredSpan({ color, text }) {
return `<span style="color: ${colors[color]()}; font-weight: var(--font-weight-bold)">${text}</span>`;
}
parametersElement.append(
utils.dom.createFieldElement({
title: createColoredTypeHTML({
color: "green",
type: "Dollars",
text: "Initial Amount",
}),
description:
"The amount of dollars you have ready on the exchange on day one.",
input: utils.dom.createResetableInput(
utils.dom.createInputDollar({
id: "simulation-dollars-initial",
title: "Initial Dollar Amount",
signal: settings.dollars.initial.amount,
signals,
}),
),
}),
);
parametersElement.append(
utils.dom.createFieldElement({
title: createColoredTypeHTML({
color: "green",
type: "Dollars",
text: "Top Up Frequency",
}),
description:
"The frequency at which you'll top up your account at the exchange.",
input: utils.dom.createResetableInput(
utils.dom.createSelect({
id: "top-up-frequency",
list: frequencies.list,
signal: settings.dollars.topUp.frenquency,
}),
),
}),
);
parametersElement.append(
utils.dom.createFieldElement({
title: createColoredTypeHTML({
color: "green",
type: "Dollars",
text: "Top Up Amount",
}),
description:
"The recurrent amount of dollars you'll be transfering to said exchange.",
input: utils.dom.createResetableInput(
utils.dom.createInputDollar({
id: "simulation-dollars-top-up-amount",
title: "Top Up Dollar Amount",
signal: settings.dollars.topUp.amount,
signals,
}),
),
}),
);
parametersElement.append(
utils.dom.createFieldElement({
title: createColoredTypeHTML({
color: "orange",
type: "Bitcoin",
text: "Initial Investment",
}),
description:
"The amount, if available, of dollars that will be used to buy Bitcoin on day one.",
input: utils.dom.createResetableInput(
utils.dom.createInputDollar({
id: "simulation-bitcoin-initial-investment",
title: "Initial Swap Amount",
signal: settings.bitcoin.investment.initial,
signals,
}),
),
}),
);
parametersElement.append(
utils.dom.createFieldElement({
title: createColoredTypeHTML({
color: "orange",
type: "Bitcoin",
text: "Investment Frequency",
}),
description: "The frequency at which you'll be buying Bitcoin.",
input: utils.dom.createResetableInput(
utils.dom.createSelect({
id: "investment-frequency",
list: frequencies.list,
signal: settings.bitcoin.investment.frequency,
}),
),
}),
);
parametersElement.append(
utils.dom.createFieldElement({
title: createColoredTypeHTML({
color: "orange",
type: "Bitcoin",
text: "Recurrent Investment",
}),
description:
"The recurrent amount, if available, of dollars that will be used to buy Bitcoin.",
input: utils.dom.createResetableInput(
utils.dom.createInputDollar({
id: "simulation-bitcoin-recurrent-investment",
title: "Bitcoin Recurrent Investment",
signal: settings.bitcoin.investment.recurrent,
signals,
}),
),
}),
);
parametersElement.append(
utils.dom.createFieldElement({
title: createColoredTypeHTML({
color: "sky",
type: "Interval",
text: "Start",
}),
description: "The first day of the simulation.",
input: utils.dom.createResetableInput(
utils.dom.createInputDate({
id: "simulation-inverval-start",
title: "First Simulation Date",
signal: settings.interval.start,
signals,
}),
),
}),
);
parametersElement.append(
utils.dom.createFieldElement({
title: createColoredTypeHTML({
color: "sky",
type: "Interval",
text: "End",
}),
description: "The last day of the simulation.",
input: utils.dom.createResetableInput(
utils.dom.createInputDate({
id: "simulation-inverval-end",
title: "Last Simulation Day",
signal: settings.interval.end,
signals,
}),
),
}),
);
parametersElement.append(
utils.dom.createFieldElement({
title: createColoredTypeHTML({
color: "red",
type: "Fees",
text: "Exchange",
}),
description: "The amount of trading fees (in %) at the exchange.",
input: utils.dom.createResetableInput(
utils.dom.createInputNumberElement({
id: "simulation-fees",
title: "Exchange Fees",
signal: settings.fees.percentage,
min: 0,
max: 50,
step: 0.01,
signals,
placeholder: "Fees",
}),
),
}),
);
const p1 = window.document.createElement("p");
resultsElement.append(p1);
const p2 = window.document.createElement("p");
resultsElement.append(p2);
const p3 = window.document.createElement("p");
resultsElement.append(p3);
const p4 = window.document.createElement("p");
resultsElement.append(p4);
const owner = signals.getOwner();
const totalInvestedAmountData = signals.createSignal(
/** @type {LineData<Time>[]} */ ([]),
{
equals: false,
},
);
const bitcoinValueData = signals.createSignal(
/** @type {LineData<Time>[]} */ ([]),
{
equals: false,
},
);
const bitcoinData = signals.createSignal(
/** @type {LineData<Time>[]} */ ([]),
{
equals: false,
},
);
const resultData = signals.createSignal(
/** @type {LineData<Time>[]} */ ([]),
{
equals: false,
},
);
const dollarsLeftData = signals.createSignal(
/** @type {LineData<Time>[]} */ ([]),
{
equals: false,
},
);
const totalValueData = signals.createSignal(
/** @type {LineData<Time>[]} */ ([]),
{
equals: false,
},
);
const investmentData = signals.createSignal(
/** @type {LineData<Time>[]} */ ([]),
{
equals: false,
},
);
const bitcoinAddedData = signals.createSignal(
/** @type {LineData<Time>[]} */ ([]),
{
equals: false,
},
);
const averagePricePaidData = signals.createSignal(
/** @type {LineData<Time>[]} */ ([]),
{
equals: false,
},
);
const bitcoinPriceData = signals.createSignal(
/** @type {LineData<Time>[]} */ ([]),
{
equals: false,
},
);
const buyCountData = signals.createSignal(
/** @type {LineData<Time>[]} */ ([]),
{
equals: false,
},
);
const totalFeesPaidData = signals.createSignal(
/** @type {LineData<Time>[]} */ ([]),
{
equals: false,
},
);
const daysCountData = signals.createSignal(
/** @type {LineData<Time>[]} */ ([]),
{
equals: false,
},
);
const profitableDaysRatioData = signals.createSignal(
/** @type {LineData<Time>[]} */ ([]),
{
equals: false,
},
);
const unprofitableDaysRatioData = signals.createSignal(
/** @type {LineData<Time>[]} */ ([]),
{
equals: false,
},
);
lightweightCharts.createChart({
parent: resultsElement,
signals,
colors,
id: `simulation-0`,
kind: "static",
scale: "date",
utils,
config: [
{
unit: "US Dollars",
config: [
{
title: "Fees Paid",
type: "Line",
color: colors.rose,
data: totalFeesPaidData,
},
{
title: "Dollars Left",
type: "Line",
color: colors.offDollars,
data: dollarsLeftData,
},
{
title: "Dollars Converted",
type: "Line",
color: colors.dollars,
data: totalInvestedAmountData,
},
{
title: "Bitcoin Value",
type: "Line",
color: colors.amber,
data: bitcoinValueData,
},
],
},
],
});
lightweightCharts.createChart({
parent: resultsElement,
signals,
colors,
id: `simulation-1`,
scale: "date",
kind: "static",
utils,
config: [
{
unit: "US Dollars",
config: [
{
title: "Bitcoin Stack",
type: "Line",
color: colors.bitcoin,
data: bitcoinData,
},
],
},
],
});
lightweightCharts.createChart({
parent: resultsElement,
signals,
colors,
id: `simulation-average-price`,
scale: "date",
kind: "static",
utils,
config: [
{
unit: "US Dollars",
config: [
{
title: "Bitcoin Price",
type: "Line",
color: colors.default,
data: bitcoinPriceData,
},
{
title: "Average Price Paid",
type: "Line",
color: colors.lightDollars,
data: averagePricePaidData,
},
],
},
],
});
lightweightCharts.createChart({
parent: resultsElement,
signals,
colors,
id: `simulation-return-ratio`,
scale: "date",
kind: "static",
utils,
config: [
{
unit: "US Dollars",
config: [
{
title: "Return Of Investment",
type: "Baseline",
data: resultData,
// TODO: Doesn't work for some reason
// options: {
// baseLineColor: "#888",
// baseLineVisible: true,
// baseLineWidth: 1,
// baseValue: {
// price: 0,
// type: "price",
// },
// },
},
],
},
],
});
lightweightCharts.createChart({
parent: resultsElement,
signals,
colors,
id: `simulation-profitability-ratios`,
kind: "static",
scale: "date",
utils,
owner,
config: [
{
unit: "Percentage",
config: [
{
title: "Unprofitable Days Ratio",
type: "Line",
color: colors.red,
data: unprofitableDaysRatioData,
},
{
title: "Profitable Days Ratio",
type: "Line",
color: colors.green,
data: profitableDaysRatioData,
},
],
},
],
});
const closes = datasets.getOrCreate("date", "date-to-close");
closes.fetchRange(2009, new Date().getUTCFullYear()).then(() => {
signals.runWithOwner(owner, () => {
signals.createEffect(
() => ({
initialDollarAmount: settings.dollars.initial.amount() || 0,
topUpAmount: settings.dollars.topUp.amount() || 0,
topUpFrequency: settings.dollars.topUp.frenquency(),
initialSwap: settings.bitcoin.investment.initial() || 0,
recurrentSwap: settings.bitcoin.investment.recurrent() || 0,
swapFrequency: settings.bitcoin.investment.frequency(),
start: settings.interval.start(),
end: settings.interval.end(),
fees: settings.fees.percentage(),
}),
({
initialDollarAmount,
topUpAmount,
topUpFrequency,
initialSwap,
recurrentSwap,
swapFrequency,
start,
end,
fees,
}) => {
if (!start || !end || start > end) return;
const range = utils.date.getRange(start, end);
totalInvestedAmountData().length = 0;
bitcoinValueData().length = 0;
bitcoinData().length = 0;
resultData().length = 0;
dollarsLeftData().length = 0;
totalValueData().length = 0;
investmentData().length = 0;
bitcoinAddedData().length = 0;
averagePricePaidData().length = 0;
bitcoinPriceData().length = 0;
buyCountData().length = 0;
totalFeesPaidData().length = 0;
daysCountData().length = 0;
profitableDaysRatioData().length = 0;
unprofitableDaysRatioData().length = 0;
let bitcoin = 0;
let sats = 0;
let dollars = initialDollarAmount;
let investedAmount = 0;
let postFeesInvestedAmount = 0;
let buyCount = 0;
let averagePricePaid = 0;
let bitcoinValue = 0;
let roi = 0;
let totalValue = 0;
let totalFeesPaid = 0;
let daysCount = range.length;
let profitableDays = 0;
let unprofitableDays = 0;
let profitableDaysRatio = 0;
let unprofitableDaysRatio = 0;
let lastInvestDay = range[0];
let dailyInvestment = 0;
let bitcoinAdded = 0;
let satsAdded = 0;
let lastSatsAdded = 0;
range.forEach((date, index) => {
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;
dailyInvestment = 0;
/** @param {number} value */
function invest(value) {
value = Math.min(dollars, value);
dailyInvestment += value;
dollars -= value;
buyCount += 1;
lastInvestDay = date;
}
if (!index) {
invest(initialSwap);
}
if (swapFrequency.isTriggerDay(date) && dollars > 0) {
invest(recurrentSwap);
}
investedAmount += dailyInvestment;
let dailyInvestmentPostFees =
dailyInvestment * (1 - (fees || 0) / 100);
totalFeesPaid += dailyInvestment - dailyInvestmentPostFees;
bitcoinAdded = dailyInvestmentPostFees / close;
bitcoin += bitcoinAdded;
satsAdded = Math.floor(bitcoinAdded * 100_000_000);
if (satsAdded > 0) {
lastSatsAdded = satsAdded;
}
sats += satsAdded;
postFeesInvestedAmount += dailyInvestmentPostFees;
bitcoinValue = close * bitcoin;
totalValue = dollars + bitcoinValue;
averagePricePaid = postFeesInvestedAmount / bitcoin;
roi = (bitcoinValue / postFeesInvestedAmount - 1) * 100;
const daysCount = index + 1;
profitableDaysRatio = profitableDays / daysCount;
unprofitableDaysRatio = unprofitableDays / daysCount;
if (roi >= 0) {
profitableDays += 1;
} else {
unprofitableDays += 1;
}
bitcoinPriceData().push({
time,
value: close,
});
bitcoinData().push({
time,
value: bitcoin,
});
totalInvestedAmountData().push({
time,
value: investedAmount,
});
bitcoinValueData().push({
time,
value: bitcoinValue,
});
resultData().push({
time,
value: roi,
});
dollarsLeftData().push({
time,
value: dollars,
});
totalValueData().push({
time,
value: totalValue,
});
investmentData().push({
time,
value: dailyInvestment,
});
bitcoinAddedData().push({
time,
value: bitcoinAdded,
});
averagePricePaidData().push({
time,
value: averagePricePaid,
});
buyCountData().push({
time,
value: buyCount,
});
totalFeesPaidData().push({
time,
value: totalFeesPaid,
});
daysCountData().push({
time,
value: daysCount,
});
profitableDaysRatioData().push({
time,
value: profitableDaysRatio * 100,
});
unprofitableDaysRatioData().push({
time,
value: unprofitableDaysRatio * 100,
});
});
const f = utils.locale.numberToUSFormat;
/** @param {number} v */
const fd = (v) => utils.formatters.dollars.format(v);
/** @param {number} v */
const fp = (v) => utils.formatters.percentage.format(v);
/**
* @param {ColorName} c
* @param {string} t
*/
const c = (c, t) => createColoredSpan({ color: c, text: t });
const serInvestedAmount = c("dollars", fd(investedAmount));
const serDaysCount = c("sky", f(daysCount));
const serSats = c("orange", f(sats));
const serBitcoin = c("orange", `~${f(bitcoin)}`);
const serBitcoinValue = c("amber", fd(bitcoinValue));
const serAveragePricePaid = c("lightDollars", fd(averagePricePaid));
const serRoi = c("yellow", fp(roi / 100));
const serDollars = c("offDollars", fd(dollars));
const serTotalFeesPaid = c("rose", fd(totalFeesPaid));
p1.innerHTML = `After exchanging ${serInvestedAmount} in the span of ${serDaysCount} days, you would have accumulated ${serSats} Satoshis (${serBitcoin} Bitcoin) worth today ${serBitcoinValue} at an average price of ${serAveragePricePaid} per Bitcoin with a return of investment of ${serRoi}, have ${serDollars} left and paid a total of ${serTotalFeesPaid} in fees.`;
const dayDiff = Math.floor(
utils.date.differenceBetween(new Date(), lastInvestDay),
);
const serDailyInvestment = c("offDollars", fd(dailyInvestment));
const setLastSatsAdded = c("bitcoin", f(lastSatsAdded));
p2.innerHTML = `You would've last bought ${c("blue", dayDiff ? `${f(dayDiff)} ${dayDiff > 1 ? "days" : "day"} ago` : "today")} and exchanged ${serDailyInvestment} for approximately ${setLastSatsAdded} Satoshis`;
const serProfitableDaysRatio = c("green", fp(profitableDaysRatio));
const serUnprofitableDaysRatio = c("red", fp(unprofitableDaysRatio));
p3.innerHTML = `You would've been ${serProfitableDaysRatio} of the time profitable and ${serUnprofitableDaysRatio} of the time unprofitable.`;
signals.createEffect(lastValues, (lastValues) => {
const lowestAnnual4YReturn = 0.2368;
// const lowestAnnual4YReturn = lastValues?.["price-4y-compound-return"] || 0
const serLowestAnnual4YReturn = c(
"cyan",
`${fp(lowestAnnual4YReturn)}`,
);
const lowestAnnual4YReturnPercentage = 1 + lowestAnnual4YReturn;
/**
* @param {number} power
*/
function bitcoinValueReturn(power) {
return (
bitcoinValue * Math.pow(lowestAnnual4YReturnPercentage, power)
);
}
const bitcoinValueAfter4y = bitcoinValueReturn(4);
const serBitcoinValueAfter4y = c("purple", fd(bitcoinValueAfter4y));
const bitcoinValueAfter10y = bitcoinValueReturn(10);
const serBitcoinValueAfter10y = c(
"fuchsia",
fd(bitcoinValueAfter10y),
);
const bitcoinValueAfter21y = bitcoinValueReturn(21);
const serBitcoinValueAfter21y = c("pink", fd(bitcoinValueAfter21y));
/** @param {number} v */
p4.innerHTML = `The lowest annual return after 4 years has historically been ${serLowestAnnual4YReturn}.<br/>Using it as the baseline, your Bitcoin would be worth ${serBitcoinValueAfter4y} after 4 years, ${serBitcoinValueAfter10y} after 10 years and ${serBitcoinValueAfter21y} after 21 years.`;
});
totalInvestedAmountData.set((a) => a);
bitcoinValueData.set((a) => a);
bitcoinData.set((a) => a);
resultData.set((a) => a);
dollarsLeftData.set((a) => a);
totalValueData.set((a) => a);
investmentData.set((a) => a);
bitcoinAddedData.set((a) => a);
averagePricePaidData.set((a) => a);
bitcoinPriceData.set((a) => a);
buyCountData.set((a) => a);
totalFeesPaidData.set((a) => a);
daysCountData.set((a) => a);
profitableDaysRatioData.set((a) => a);
unprofitableDaysRatioData.set((a) => a);
},
);
});
});
}
/** @param {number} day */
function getOrdinalDay(day) {
const rest = (day % 30) % 20;
return `${day}${
rest === 1 ? "st" : rest === 2 ? "nd" : rest === 3 ? "rd" : "th"
}`;
}
function computeFrequencies() {
const weekDays = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
];
const maxDays = 28;
/** @satisfies {([Frequency, Frequencies, Frequencies, Frequencies])} */
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 };
}

302
website/scripts/types/self.d.ts vendored Normal file
View File

@@ -0,0 +1,302 @@
import {
Accessor,
Setter,
} from "../../packages/solid-signals/2024-11-02/types/signals";
import {
DeepPartial,
BaselineStyleOptions,
CandlestickStyleOptions,
LineStyleOptions,
SeriesOptionsCommon,
Range,
Time,
SingleValueData,
CandlestickData,
SeriesType,
ISeriesApi,
BaselineData,
} from "../../packages/lightweight-charts/v4.2.2/types";
import { DatePath, HeightPath, LastPath } from "./paths";
import { AnyPossibleCohortId } from "../options";
import { Signal } from "../../packages/solid-signals/types";
type GrowToSize<T, N extends number, A extends T[]> = A["length"] extends N
? A
: GrowToSize<T, N, [...A, T]>;
type FixedArray<T, N extends number> = GrowToSize<T, N, []>;
type TimeScale = "date" | "height";
type TimeRange = Range<Time | number>;
type DatasetPath<Scale extends TimeScale> = Scale extends "date"
? DatePath
: HeightPath;
type AnyDatasetPath = import("./paths").DatePath | import("./paths").HeightPath;
type AnyPath = AnyDatasetPath | LastPath;
type Color = () => string;
type ColorName = keyof Colors;
type Unit =
| ""
| "Bitcoin"
| "Coinblocks"
| "Count"
| "Date"
| "Dollars / (PetaHash / Second)"
| "ExaHash / Second"
| "Height"
| "Gigabytes"
| "Megabytes"
| "Percentage"
| "Ratio"
| "Satoshis"
| "Seconds"
| "Transactions"
| "US Dollars"
| "Virtual Bytes"
| "Weight";
interface PartialOption {
name: string;
}
interface PartialHomeOption extends PartialOption {
kind: "home";
title: "Home";
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;
shortTitle?: string;
unit: Unit;
description: string;
top?: SplitSeriesBlueprint[];
bottom?: SplitSeriesBlueprint[];
dashboard?: {
ignoreName?: boolean;
skip?: boolean;
};
}
interface PartialSimulationOption extends PartialOption {
kind: "simulation";
title: string;
name: string;
}
interface PartialPdfOption extends PartialOption {
pdf: string;
}
interface PartialUrlOption extends PartialOption {
qrcode?: true;
url: () => string;
}
interface PartialOptionsGroup {
name: string;
tree: PartialOptionsTree;
}
type AnyPartialOption =
| PartialHomeOption
| PartialLivePriceOption
| PartialMoscowTimeOption
| PartialConverterOption
| PartialChartOption
| PartialSimulationOption
| PartialPdfOption
| PartialUrlOption;
type PartialOptionsTree = (AnyPartialOption | PartialOptionsGroup)[];
interface ProcessedOptionAddons {
id: string;
path: OptionPath;
serializedPath: string;
title: string;
}
type OptionPath = {
id: string;
name: string;
}[];
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";
}
interface UrlOption extends PartialUrlOption, ProcessedOptionAddons {
kind: "url";
}
interface ChartOption extends PartialChartOption, ProcessedOptionAddons {
kind: "chart";
}
type Option =
| HomeOption
| LivePriceOption
| MoscowTimeOption
| ConverterOption
| PdfOption
| UrlOption
| ChartOption
| SimulationOption;
type OptionsTree = (Option | OptionsGroup)[];
interface OptionsGroup extends PartialOptionsGroup {
id: string;
tree: OptionsTree;
}
interface OHLC {
open: number;
high: number;
low: number;
close: number;
}
interface ResourceDataset<
Scale extends TimeScale,
Type extends OHLC | number = number,
> {
scale: Scale;
url: string;
fetch: (id: number) => Promise<void>;
fetchRange: (start: number, end: number) => Promise<void[]>;
fetchedJSONs: FetchedResult<Scale, Type>[];
}
type ValuedCandlestickData = CandlestickData & Valued;
interface FetchedResult<
Scale extends TimeScale,
Type extends number | OHLC,
Value extends SingleValueData | ValuedCandlestickData = Type extends number
? SingleValueData
: ValuedCandlestickData,
> {
at: Date | null;
json: Signal<FetchedJSON<Scale, Type> | null>;
vec: Accessor<Value[] | null>;
loading: boolean;
}
interface Valued {
value: number;
}
type DatasetValue<T> = T & Valued;
interface FetchedJSON<Scale extends TimeScale, Type extends number | OHLC> {
source: FetchedSource;
chunk: FetchedChunk;
dataset: FetchedDataset<Scale, Type>;
}
type FetchedSource = string;
interface FetchedChunk {
id: number;
previous: string | null;
next: string | null;
}
type FetchedDataset<
Scale extends TimeScale,
Type extends number | OHLC,
> = Scale extends "date"
? FetchedDateDataset<Type>
: FetchedHeightDataset<Type>;
interface Versioned {
version: number;
}
interface FetchedDateDataset<Type> extends Versioned {
map: Record<string, Type>;
}
interface FetchedHeightDataset<Type> extends Versioned {
map: Type[];
}
interface Weighted {
weight: number;
}
type DatasetCandlestickData = DatasetValue<CandlestickData> & { year: number };
type NotFunction<T> = T extends Function ? never : T;
type Groups = import("../options").Groups;
type DefaultCohortOption = CohortOption<AnyPossibleCohortId>;
interface CohortOption<Id extends AnyPossibleCohortId> {
scale: TimeScale;
name: string;
title: string;
datasetId: Id;
color: Color;
filenameAddon?: string;
}
type DefaultCohortOptions = CohortOptions<AnyPossibleCohortId>;
interface CohortOptions<Id extends AnyPossibleCohortId> {
scale: TimeScale;
name: string;
title: string;
list: CohortOption<Id>[];
}
interface RatioOption {
scale: TimeScale;
color: Color;
valueDatasetPath: AnyDatasetPath;
ratioDatasetPath: AnyDatasetPath;
title: string;
}
interface RatioOptions {
scale: TimeScale;
title: string;
list: RatioOption[];
}
interface Frequency {
name: string;
value: string;
isTriggerDay: (date: Date) => boolean;
}
type Frequencies = { name: string; list: Frequency[] };
type LastValues = Record<LastPath, number> | null;