// @ts-check /** * @param {Object} args * @param {Colors} args.colors * @param {LightweightCharts} args.lightweightCharts * @param {Signals} args.signals * @param {Utilities} args.utils * @param {Elements} args.elements * @param {VecsResources} args.vecsResources */ export function init({ colors, elements, lightweightCharts, signals, utils, vecsResources, }) { /** * @typedef {Object} Frequency * @property {string} name * @property {string} value * @property {(date: Date) => boolean} isTriggerDay * * @typedef {Object} Frequencies * @property {string} name * @property {Frequency[]} list */ const simulationElement = elements.simulation; const dom = { /** * @param {Object} args * @param {string} args.id * @param {string} args.title * @param {string} args.placeholder * @param {Signal} args.signal * @param {number} args.min * @param {number} args.step * @param {number} [args.max] * @param {{createEffect: typeof CreateEffect}} args.signals */ createInputNumberElement({ id, title, signal, min, max, step, placeholder, signals, }) { const input = window.document.createElement("input"); if (!id || !title || !placeholder) throw Error("input attribute missing"); input.id = id; input.title = title; input.placeholder = placeholder; input.type = "number"; input.min = String(min); if (max) { 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; stateValue = valueSer; const value = Number(valueSer); if (value >= min && (max ? value <= max : true)) { signal.set(value); } }); return { input, signal }; }, /** * @param {Object} args * @param {string} args.id * @param {string} args.title * @param {Signal} args.signal * @param {{createEffect: typeof CreateEffect}} args.signals */ createInputDollar({ id, title, signal, signals }) { return this.createInputNumberElement({ id, placeholder: "USD", min: 0, title, signal, signals, step: 1, }); }, /** * @param {Object} args * @param {string} args.id * @param {string} args.title * @param {Signal} 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 dateSignal = signal(); return dateSignal ? utils.date.toString(dateSignal) : ""; }, (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, signal }; }, /** * @param {Object} param0 * @param {Signal} param0.signal * @param {HTMLInputElement} [param0.input] * @param {HTMLSelectElement} [param0.select] */ createResetableInput({ input, select, signal }) { const div = window.document.createElement("div"); const element = input || select; if (!element) throw "createResetableField element missing"; div.append(element); const button = utils.dom.createButtonElement({ onClick: signal.reset, inside: "Reset", title: "Reset field", }); button.type = "reset"; div.append(button); return div; }, }; const parametersElement = window.document.createElement("div"); simulationElement.append(parametersElement); const resultsElement = window.document.createElement("div"); simulationElement.append(resultsElement); function computeFrequencies() { const weekDays = [ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday", ]; const maxDays = 28; /** @param {number} day */ function getOrdinalDay(day) { const rest = (day % 30) % 20; return `${day}${ rest === 1 ? "st" : rest === 2 ? "nd" : rest === 3 ? "rd" : "th" }`; } /** @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} */ 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 }; } const frequencies = computeFrequencies(); const keyPrefix = "save-in-bitcoin"; const settings = { dollars: { initial: { amount: signals.createSignal(/** @type {number | null} */ (1000), { save: { ...utils.serde.optNumber, keyPrefix, key: "initial-amount", }, }), }, topUp: { amount: signals.createSignal(/** @type {number | null} */ (150), { save: { ...utils.serde.optNumber, 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.optNumber, keyPrefix, key: "initial-swap", }, }), recurrent: signals.createSignal(/** @type {number | null} */ (5), { save: { ...utils.serde.optNumber, 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.optDate, keyPrefix, key: "interval-start", }, }, ), end: signals.createSignal(/** @type {Date | null} */ (new Date()), { save: { ...utils.serde.optDate, keyPrefix, key: "interval-end", }, }), }, fees: { percentage: signals.createSignal(/** @type {number | null} */ (0.25), { save: { ...utils.serde.optNumber, keyPrefix, key: "percentage", }, }), }, }; parametersElement.append( utils.dom.createHeader("Save in Bitcoin").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 `${text}`; } 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: dom.createResetableInput( 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: dom.createResetableInput( utils.dom.createSelect({ id: "top-up-frequency", list: frequencies.list, signal: settings.dollars.topUp.frenquency, deep: true, }), ), }), ); 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: dom.createResetableInput( 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: dom.createResetableInput( 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: dom.createResetableInput( utils.dom.createSelect({ id: "investment-frequency", list: frequencies.list, signal: settings.bitcoin.investment.frequency, deep: true, }), ), }), ); 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: dom.createResetableInput( 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: dom.createResetableInput( 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: dom.createResetableInput( 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: dom.createResetableInput( 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