kibo: fix simulation

This commit is contained in:
nym21
2025-04-04 17:44:22 +02:00
parent 118c87faf7
commit be632aaf37
10 changed files with 605 additions and 394 deletions

View File

@@ -920,10 +920,15 @@
align-items: center;
gap: 0.5rem;
&[data-size="sm"] {
font-size: var(--font-size-sm);
line-height: var(--line-height-sm);
}
&[data-size="xs"] {
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
font-weight: 500;
font-weight: 450;
}
> div.field {
@@ -940,6 +945,10 @@
> hr {
min-width: 2rem;
fieldset[data-size="sm"] & {
min-width: 1.5rem;
}
fieldset[data-size="xs"] & {
min-width: 1rem;
}
@@ -954,6 +963,10 @@
display: flex;
gap: 1.5rem;
fieldset[data-size="xs"] & {
gap: 1.25rem;
}
fieldset[data-size="xs"] & {
gap: 1rem;
}
@@ -983,6 +996,9 @@
padding-bottom: 1rem;
overflow-x: auto;
min-width: 0;
font-size: var(--font-size-sm);
line-height: var(--line-height-sm);
height: 2rem;
> div {
flex: 0;
@@ -1031,6 +1047,17 @@
min-height: 0;
height: 100%;
margin-right: var(--negative-main-padding);
margin-left: var(--negative-main-padding);
fieldset {
padding-left: var(--main-padding);
padding-top: 0.5rem;
z-index: 10;
position: absolute;
left: 0;
top: 0;
gap: 0;
}
}
> .panes {

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,7 @@
// import { CanvasRenderingTarget2D } from "fancy-canvas";
type CanvasRenderingTarget2D = any;
declare const baselineSeries: SeriesDefinition<"Baseline">;
declare const candlestickSeries: SeriesDefinition<"Candlestick">;
declare const lineSeries: SeriesDefinition<"Line">;
export declare const customSeriesDefaultOptions: CustomSeriesOptions;
@@ -4032,6 +4033,10 @@ export type UTCTimestamp = Nominal<number, "UTCTimestamp">;
*/
export type VisiblePriceScaleOptions = PriceScaleOptions;
export { candlestickSeries as CandlestickSeries, lineSeries as LineSeries };
export {
baselineSeries as BaselineSeries,
candlestickSeries as CandlestickSeries,
lineSeries as LineSeries,
};
export {};

View File

@@ -1,6 +1,6 @@
// @ts-check
/** @import {IChartApi, ISeriesApi, SeriesDefinition, SingleValueData as _SingleValueData, CandlestickData as _CandlestickData} from './v5.0.5-treeshaked/types' */
/** @import {IChartApi, ISeriesApi, SeriesDefinition, SingleValueData as _SingleValueData, CandlestickData as _CandlestickData, BaselineData, SeriesType} from './v5.0.5-treeshaked/types' */
/**
* @typedef {[number, number, number, number]} OHLCTuple
@@ -47,7 +47,7 @@ export default import("./v5.0.5-treeshaked/script.js").then((lc) => {
autoSize: true,
layout: {
fontFamily: "Geist mono",
fontSize: 14,
fontSize: 13,
background: { color: "transparent" },
attributionLogo: false,
colorSpace: "display-p3",
@@ -130,15 +130,18 @@ export default import("./v5.0.5-treeshaked/script.js").then((lc) => {
* @param {VecsResources} args.vecsResources
* @param {Owner | null} [args.owner]
* @param {true} [args.fitContentOnResize]
* @param {{unit: Unit; blueprints: AnySeriesBlueprint[]}[]} [args.config]
*/
function createChartElement({
parent,
signals,
colors,
utils,
id,
vecsResources,
owner: _owner,
fitContentOnResize,
config,
}) {
let owner = _owner || signals.getOwner();
@@ -179,7 +182,7 @@ export default import("./v5.0.5-treeshaked/script.js").then((lc) => {
* @param {ISeriesApi<SeriesType>} series
* @param {VecResource} valuesResource
*/
function createSetDataEffect(series, valuesResource) {
function createSetFetchedDataEffect(series, valuesResource) {
signals.runWithOwner(owner, () =>
signals.createEffect(
() => [timeResource?.fetched(), valuesResource.fetched()],
@@ -244,7 +247,7 @@ export default import("./v5.0.5-treeshaked/script.js").then((lc) => {
}),
);
return {
const chart = {
inner: () => ichart,
/**
* @param {Object} args
@@ -272,12 +275,23 @@ export default import("./v5.0.5-treeshaked/script.js").then((lc) => {
colors,
utils,
});
if (fitContentOnResize) {
ichart.applyOptions({
handleScroll: false,
handleScale: false,
timeScale: {
minBarSpacing: 0.001,
},
});
}
},
/**
* @param {Object} args
* @param {VecId} args.vecId
* @param {string} args.name
* @param {Unit} args.unit
* @param {VecId} [args.vecId]
* @param {Accessor<CandlestickData[]>} [args.data]
* @param {number} [args.paneIndex]
* @param {boolean} [args.defaultActive]
*/
@@ -287,15 +301,12 @@ export default import("./v5.0.5-treeshaked/script.js").then((lc) => {
unit,
paneIndex: _paneIndex,
defaultActive,
data,
}) {
const paneIndex = _paneIndex ?? 0;
if (!ichart || !timeResource) throw Error("Chart not fully set");
const valuesResource = vecsResources.getOrCreate(vecIndex, vecId);
valuesResource.fetch();
activeResources.push(valuesResource);
const green = colors.green();
const red = colors.red();
const series = ichart.addSeries(
@@ -311,31 +322,56 @@ export default import("./v5.0.5-treeshaked/script.js").then((lc) => {
paneIndex,
);
let url = /** @type {string | undefined} */ (undefined);
if (vecId) {
const valuesResource = vecsResources.getOrCreate(vecIndex, vecId);
valuesResource.fetch();
activeResources.push(valuesResource);
createSetFetchedDataEffect(series, valuesResource);
url = valuesResource.url;
} else if (data) {
signals.runWithOwner(owner, () =>
signals.createEffect(data, (data) => {
series.setData(data);
}),
);
}
legend.add({
series,
name,
defaultActive,
colors: [colors.green, colors.red],
url: valuesResource.url,
url,
});
createPaneHeightObserver({
ichart,
paneIndex,
signals,
utils,
});
createPriceScaleSelectorIfNeeded({
ichart,
paneIndex,
seriesType: "Candlestick",
signals,
id,
unit,
utils,
});
createSetDataEffect(series, valuesResource);
return series;
},
/**
* @param {Object} args
* @param {VecId} args.vecId
* @param {string} args.name
* @param {Unit} args.unit
* @param {Accessor<LineData[]>} [args.data]
* @param {VecId} [args.vecId]
* @param {Color} [args.color]
* @param {number} [args.paneIndex]
* @param {boolean} [args.defaultActive]
@@ -347,15 +383,12 @@ export default import("./v5.0.5-treeshaked/script.js").then((lc) => {
color,
paneIndex: _paneIndex,
defaultActive,
data,
}) {
if (!ichart || !timeResource) throw Error("Chart not fully set");
const paneIndex = _paneIndex ?? 0;
const valuesResource = vecsResources.getOrCreate(vecIndex, vecId);
valuesResource.fetch();
activeResources.push(valuesResource);
color ||= colors.orange;
const series = ichart.addSeries(
@@ -369,16 +402,34 @@ export default import("./v5.0.5-treeshaked/script.js").then((lc) => {
paneIndex,
);
let url = /** @type {string | undefined} */ (undefined);
if (vecId) {
const valuesResource = vecsResources.getOrCreate(vecIndex, vecId);
valuesResource.fetch();
activeResources.push(valuesResource);
createSetFetchedDataEffect(series, valuesResource);
url = valuesResource.url;
} else if (data) {
signals.runWithOwner(owner, () =>
signals.createEffect(data, (data) => {
series.setData(data);
ichart
?.timeScale()
.setVisibleLogicalRange({ from: -1, to: data.length });
}),
);
}
legend.add({
series,
colors: [color],
name,
defaultActive,
url: valuesResource.url,
url,
});
createSetDataEffect(series, valuesResource);
createPaneHeightObserver({
ichart,
paneIndex,
@@ -390,6 +441,94 @@ export default import("./v5.0.5-treeshaked/script.js").then((lc) => {
ichart,
paneIndex,
signals,
seriesType: "Line",
id,
unit,
utils,
});
return series;
},
/**
* @param {Object} args
* @param {string} args.name
* @param {Unit} args.unit
* @param {Accessor<BaselineData[]>} [args.data]
* @param {VecId} [args.vecId]
* @param {number} [args.paneIndex]
* @param {boolean} [args.defaultActive]
*/
addBaselineSeries({
vecId,
name,
unit,
paneIndex: _paneIndex,
defaultActive,
data,
}) {
if (!ichart || !timeResource) throw Error("Chart not fully set");
const paneIndex = _paneIndex ?? 0;
const series = ichart.addSeries(
/** @type {SeriesDefinition<'Baseline'>} */ (lc.BaselineSeries),
{
lineWidth: /** @type {any} */ (1.5),
visible: defaultActive !== false,
topLineColor: colors.green(),
bottomLineColor: colors.red(),
baseValue: {
price: 0,
},
baseLineStyle: 0,
baseLineWidth: 1,
baseLineVisible: true,
lineVisible: true,
},
paneIndex,
);
let url = /** @type {string | undefined} */ (undefined);
if (vecId) {
const valuesResource = vecsResources.getOrCreate(vecIndex, vecId);
valuesResource.fetch();
activeResources.push(valuesResource);
createSetFetchedDataEffect(series, valuesResource);
url = valuesResource.url;
} else if (data) {
signals.runWithOwner(owner, () =>
signals.createEffect(data, (data) => {
series.setData(data);
ichart
?.timeScale()
.setVisibleLogicalRange({ from: -1, to: data.length });
}),
);
}
legend.add({
series,
colors: [colors.green, colors.red],
name,
defaultActive,
url,
});
createPaneHeightObserver({
ichart,
paneIndex,
signals,
utils,
});
createPriceScaleSelectorIfNeeded({
ichart,
paneIndex,
signals,
seriesType: "Baseline",
id,
unit,
utils,
});
@@ -410,6 +549,41 @@ export default import("./v5.0.5-treeshaked/script.js").then((lc) => {
legend.reset();
},
};
config?.forEach(({ unit, blueprints }, paneIndex) => {
chart.create({ index: /** @satisfies {Dateindex} */ (1) });
blueprints.forEach((blueprint) => {
if (blueprint.type === "Candlestick") {
chart.addCandlestickSeries({
name: blueprint.title,
unit,
data: blueprint.data,
defaultActive: blueprint.defaultActive,
paneIndex,
});
} else if (blueprint.type === "Baseline") {
chart.addBaselineSeries({
name: blueprint.title,
unit,
data: blueprint.data,
defaultActive: blueprint.defaultActive,
paneIndex,
});
} else {
chart.addLineSeries({
name: blueprint.title,
unit,
data: blueprint.data,
defaultActive: blueprint.defaultActive,
paneIndex,
color: blueprint.color,
});
}
});
});
return chart;
}
return {
@@ -678,7 +852,6 @@ function createPaneHeightObserver({ ichart, paneIndex, signals, utils }) {
if (!paneIndex) return;
const owner = signals.getOwner();
if (!owner) throw Error("Expect owner");
const one = "1";
@@ -729,6 +902,8 @@ function createPaneHeightObserver({ ichart, paneIndex, signals, utils }) {
* @param {Object} args
* @param {IChartApi} args.ichart
* @param {Unit} args.unit
* @param {string} args.id
* @param {SeriesType} args.seriesType
* @param {number} args.paneIndex
* @param {Signals} args.signals
* @param {Utilities} args.utils
@@ -737,11 +912,12 @@ function createPriceScaleSelectorIfNeeded({
ichart,
unit,
paneIndex,
id,
seriesType,
signals,
utils,
}) {
const owner = signals.getOwner();
if (!owner) throw Error("Expect owner");
setTimeout(
() => {
@@ -759,16 +935,20 @@ function createPriceScaleSelectorIfNeeded({
return;
}
console.log(id);
const choices = /**@type {const} */ (["lin", "log"]);
/** @typedef {(typeof choices)[number]} Choices */
const serializedValue = signals.createSignal(
/** @satisfies {Choices} */ (paneIndex ? "lin" : "log"),
/** @satisfies {Choices} */ (
unit === "US Dollars" && seriesType !== "Baseline" ? "log" : "lin"
),
{
save: {
...utils.serde.string,
keyPrefix: "charts",
key: `price-scale-${paneIndex}`,
keyPrefix: "",
key: `${id}-price-scale-${paneIndex}`,
},
},
);
@@ -777,7 +957,7 @@ function createPriceScaleSelectorIfNeeded({
title: unit,
selected: serializedValue(),
choices: choices,
id: unit,
id: `${id}-${unit.replace(" ", "-")}`,
signals,
});
@@ -789,6 +969,7 @@ function createPriceScaleSelectorIfNeeded({
const element = window.document.createElement(tagName);
element.dataset.size = "xs";
element.id = `${id}-price-scale-${paneIndex}`;
element.append(field);
const mode = signals.createMemo(() => {

View File

@@ -35,7 +35,7 @@ export function init({
parent: elements.charts,
signals,
colors,
id: "chart",
id: "charts",
utils,
vecsResources,
});
@@ -174,6 +174,7 @@ function createIndexSelector({ elements, signals, utils }) {
const fieldset = window.document.createElement("fieldset");
fieldset.append(indexesField);
fieldset.dataset.size = "sm";
elements.charts.append(fieldset);
const index = signals.createMemo(

View File

@@ -1,7 +1,7 @@
// @ts-check
/**
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, Unit } from "./options"
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, Unit, AnySeriesBlueprint } from "./options"
* @import {Valued, SingleValueData, CandlestickData, ChartData, OHLCTuple} from "../packages/lightweight-charts/wrapper"
* @import * as _ from "../packages/ufuzzy/v1.0.14/types"
* @import { createChart as CreateClassicChart, LineStyleOptions, DeepPartial, ChartOptions, IChartApi, IHorzScaleBehavior, WhitespaceData, ISeriesApi, Time, LineData, LogicalRange, SeriesType, BaselineStyleOptions, SeriesOptionsCommon, BaselineData, CandlestickStyleOptions } from "../packages/lightweight-charts/v5.0.5-treeshaked/types"
@@ -952,11 +952,22 @@ function createUtils() {
},
/**
* @param {Date} date
* @returns {string}
*/
toString(date) {
return date.toJSON().split("T")[0];
},
/**
* @param {Date} date
*/
toDateIndex(date) {
if (
date.getUTCFullYear() === 2009 &&
date.getUTCMonth() === 0 &&
date.getUTCDate() === 3
)
return 0;
return this.differenceBetween(date, new Date("2009-01-09"));
},
/**
* @param {Time} time
*/
@@ -988,7 +999,6 @@ function createUtils() {
/**
* @param {Date} date1
* @param {Date} date2
* @returns
*/
differenceBetween(date1, date2) {
return Math.abs(date1.valueOf() - date2.valueOf()) / this.ONE_DAY_IN_MS;

View File

@@ -23,7 +23,6 @@
*
* @typedef {Object} BaseSeriesBlueprint
* @property {string} title
* @property {VecId} key
* @property {boolean} [defaultActive]
*
* @typedef {Object} BaselineSeriesBlueprintSpecific
@@ -49,6 +48,8 @@
*
* @typedef {BaselineSeriesBlueprint | CandlestickSeriesBlueprint | LineSeriesBlueprint} AnySeriesBlueprint
*
* @typedef {AnySeriesBlueprint & {key: VecId}} AnyFetchedSeriesBlueprint
*
* @typedef {Object} PartialOption
* @property {string} name
*
@@ -61,8 +62,8 @@
* @property {"chart"} [kind]
* @property {Unit} [unit]
* @property {string} [title]
* @property {AnySeriesBlueprint[]} [top]
* @property {AnySeriesBlueprint[]} [bottom]
* @property {AnyFetchedSeriesBlueprint[]} [top]
* @property {AnyFetchedSeriesBlueprint[]} [bottom]
* @typedef {PartialOption & PartialChartOptionSpecific} PartialChartOption
* @typedef {Required<PartialChartOption> & ProcessedOptionAddons} ChartOption
*

View File

@@ -263,7 +263,8 @@ export function init({
* @param {string} param0.text
*/
function createColoredSpan({ color, text }) {
return `<span style="color: ${colors[color]()}; font-weight: 500">${text}</span>`;
return `<span style="color: ${colors[color]()}; font-weight: 500; text-transform: uppercase;
font-size: var(--font-size-sm);">${text}</span>`;
}
parametersElement.append(
@@ -550,24 +551,19 @@ export function init({
parent: resultsElement,
signals,
colors,
id: `simulation-0`,
id: `result`,
fitContentOnResize: true,
vecsResources,
utils,
config: [
{
unit: "US Dollars",
config: [
blueprints: [
{
title: "Fees Paid",
title: "Bitcoin Value",
type: "Line",
color: colors.rose,
data: totalFeesPaidData,
},
{
title: "Dollars Left",
type: "Line",
color: colors.offDollars,
data: dollarsLeftData,
color: colors.amber,
data: bitcoinValueData,
},
{
title: "Dollars Converted",
@@ -576,10 +572,18 @@ export function init({
data: totalInvestedAmountData,
},
{
title: "Bitcoin Value",
title: "Dollars Left",
type: "Line",
color: colors.amber,
data: bitcoinValueData,
color: colors.offDollars,
data: dollarsLeftData,
defaultActive: false,
},
{
title: "Fees Paid",
type: "Line",
color: colors.rose,
data: totalFeesPaidData,
defaultActive: false,
},
],
},
@@ -590,13 +594,14 @@ export function init({
parent: resultsElement,
signals,
colors,
id: `simulation-1`,
id: `bitcoin`,
fitContentOnResize: true,
vecsResources,
utils,
config: [
{
unit: "US Dollars",
config: [
unit: "Bitcoin",
blueprints: [
{
title: "Bitcoin Stack",
type: "Line",
@@ -612,13 +617,14 @@ export function init({
parent: resultsElement,
signals,
colors,
id: `simulation-average-price`,
id: `average-price`,
fitContentOnResize: true,
vecsResources,
utils,
config: [
{
unit: "US Dollars",
config: [
blueprints: [
{
title: "Bitcoin Price",
type: "Line",
@@ -640,27 +646,18 @@ export function init({
parent: resultsElement,
signals,
colors,
id: `simulation-return-ratio`,
vecsResources,
id: `return-ratio`,
fitContentOnResize: true,
utils,
config: [
{
unit: "US Dollars",
config: [
blueprints: [
{
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",
// },
// },
},
],
},
@@ -673,333 +670,336 @@ export function init({
colors,
id: `simulation-profitability-ratios`,
fitContentOnResize: true,
vecsResources,
utils,
owner,
config: [
{
unit: "Percentage",
config: [
{
title: "Unprofitable Days Ratio",
type: "Line",
color: colors.red,
data: unprofitableDaysRatioData,
},
blueprints: [
{
title: "Profitable Days Ratio",
type: "Line",
color: colors.green,
data: profitableDaysRatioData,
},
{
title: "Unprofitable Days Ratio",
type: "Line",
color: colors.red,
data: unprofitableDaysRatioData,
},
],
},
],
});
const closes = vecsResources.getOrCreate(
/** @satisfies {Dateindex} */ (1),
"close",
);
vecsResources
.getOrCreate(/** @satisfies {Dateindex} */ (1), "close")
.fetch()
.then((_closes) => {
if (!_closes) return;
const closes = /** @type {number[]} */ (_closes);
closes.fetch().then((_closes) => {
const closes = /** @type {OHLCTuple[] | null} */ (_closes);
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;
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);
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;
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;
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);
range.forEach((date, index) => {
const year = date.getUTCFullYear();
const time = utils.date.toString(date);
if (topUpFrequency.isTriggerDay(date)) {
dollars += topUpAmount;
}
const close = closes.ranges
.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(
() => 0.2368,
(lowestAnnual4YReturn) => {
const serLowestAnnual4YReturn = c(
"cyan",
`${fp(lowestAnnual4YReturn)}`,
);
const lowestAnnual4YReturnPercentage = 1 + lowestAnnual4YReturn;
/**
* @param {number} power
*/
function bitcoinValueReturn(power) {
return (
bitcoinValue * Math.pow(lowestAnnual4YReturnPercentage, power)
);
if (topUpFrequency.isTriggerDay(date)) {
dollars += topUpAmount;
}
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.`;
},
);
const close = closes[utils.date.toDateIndex(date)];
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);
},
);
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(
() => 0.2368,
(lowestAnnual4YReturn) => {
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);
},
);
});
});
});
}

View File

@@ -26,19 +26,5 @@
.chart {
flex: 1;
.lightweight-chart {
margin-left: var(--negative-main-padding);
fieldset {
padding-left: var(--main-padding);
padding-top: 0.5rem;
z-index: 10;
position: absolute;
left: 0;
top: 0;
gap: 0;
}
}
}
}

View File

@@ -5,13 +5,8 @@
> div {
display: flex;
flex-direction: column;
gap: 1.5rem;
gap: 2rem;
padding: var(--main-padding);
> div {
display: flex;
flex-direction: column;
}
}
@media (max-width: 767px) {
@@ -67,11 +62,16 @@
}
.chart {
flex-shrink: 0;
flex: none;
height: 400px;
.lightweight-chart {
margin-left: calc(var(--negative-main-padding) / 2);
margin-left: calc(var(--negative-main-padding) * 0.75);
fieldset {
margin-left: -0.5rem;
}
}
}
}
}