// @ts-check /** * @import { FilePath, PartialPreset, PartialPresetFolder, PartialPresetTree, Preset, PresetFolder, Series, PriceSeriesType, ResourceDataset, Scale, SerializedPresetsHistory, TimeRange, Unit, Marker, Weighted, DatasetPath, OHLC, FetchedJSON, DatasetValue, FetchedResult, AnyDatasetPath, SeriesBlueprint, BaselineSpecificSeriesBlueprint, CandlestickSpecificSeriesBlueprint, LineSpecificSeriesBlueprint, SpecificSeriesBlueprintWithChart, Signal, Color, SettingsTheme } from "./types/self" * @import * as _ from "./libraries/ufuzzy/types" * @import { DeepPartial, ChartOptions, IChartApi, IHorzScaleBehavior, WhitespaceData, SingleValueData, ISeriesApi, Time, LogicalRange, SeriesMarker, CandlestickData, SeriesType, BaselineStyleOptions, SeriesOptionsCommon } from "./libraries/lightweight-charts/types" * @import { DatePath, HeightPath } from "./types/paths"; * @import { SignalOptions, untrack as Untrack } from "./libraries/solid-signals/types/core.js" * @import { getOwner as GetOwner, onCleanup as OnCleanup, Owner } from "./libraries/solid-signals/types/owner.js" * @import { createSignal as CreateSignal, createEffect as CreateEffect, Accessor, Setter, createMemo as CreateMemo, createRoot as CreateRoot, runWithOwner as RunWithOwner } from "./libraries/solid-signals/types/signals.js"; */ import { createSignal as _createSignal, createEffect as _createEffect, createMemo as _createMemo, createRoot as _createRoot, untrack as _untrack, getOwner as _getOwner, runWithOwner as _runWithOwner, onCleanup as _onCleanup, } from "./libraries/solid-signals/script.js"; const createSolidSignal = /** @type {CreateSignal} */ (_createSignal); const createEffect = /** @type {CreateEffect} */ (_createEffect); const createMemo = /** @type {CreateMemo} */ (_createMemo); const createRoot = /** @type {CreateRoot} */ (_createRoot); const untrack = /** @type {Untrack} */ (_untrack); const getOwner = /** @type {GetOwner} */ (_getOwner); const runWithOwner = /** @type {RunWithOwner} */ (_runWithOwner); const onCleanup = /** @type {OnCleanup} */ (_onCleanup); /** * @template T * @param {T} initialValue * @param {SignalOptions} [options] * @returns {Signal} */ function createSignal(initialValue, options) { const [get, set] = createSolidSignal(initialValue, options); // @ts-ignore get.set = set; // @ts-ignore return get; } /** * @param {string} id * @returns {HTMLElement} */ function getElementById(id) { const element = window.document.getElementById(id); if (!element) throw `Element with id = "${id}" should exist`; return element; } /** * @param {HTMLElement} parent * @param {HTMLElement} child * @param {number} index */ function insertElementAtIndex(parent, child, index) { if (!index) index = 0; if (index >= parent.children.length) { parent.appendChild(child); } else { parent.insertBefore(child, parent.children[index]); } } /** * @param {string} s * @returns {string} */ function stringToId(s) { return s.replace(/\W/g, " ").trim().replace(/ +/g, "-").toLowerCase(); } /** * @param {VoidFunction} callback * @param {number} [timeout = 1] */ function runWhenIdle(callback, timeout = 1) { if ("requestIdleCallback" in window) { requestIdleCallback(callback); } else { setTimeout(callback, timeout); } } /** * @param {Date} date * @returns {string} */ function dateToString(date) { return date.toJSON().split("T")[0]; } /** * @param {Date} oldest * @param {Date} youngest * @returns {number} */ function getNumberOfDaysBetweenTwoDates(oldest, youngest) { return Math.round( Math.abs((youngest.getTime() - oldest.getTime()) / ONE_DAY_IN_MS) ); } /** * * @template {(...args: any[]) => any} F * @param {F} callback * @param {number} [wait=250] */ function debounce(callback, wait = 250) { /** @type {number | undefined} */ let timeoutId; /** @type {Parameters} */ let latestArgs; return (/** @type {Parameters} */ ...args) => { latestArgs = args; if (!timeoutId) { timeoutId = window.setTimeout(async () => { await callback(...latestArgs); timeoutId = undefined; }, wait); } }; } const urlParamsHelpers = { whitelist: ["from", "to"], /** * @param {Object} args * @param {URLSearchParams} [args.urlParams] * @param {string} [args.pathname] */ replaceHistory({ urlParams, pathname }) { urlParams ||= new URLSearchParams(window.location.search); pathname ||= window.location.pathname; window.history.replaceState( null, "", `${pathname}?${urlParams.toString()}` ); }, /** * @param {string} [pathname] */ reset(pathname) { const urlParams = new URLSearchParams(); [...new URLSearchParams(window.location.search).entries()] .filter(([key, _]) => this.whitelist.includes(key)) .forEach(([key, value]) => { urlParams.set(key, value); }); this.replaceHistory({ urlParams, pathname }); }, /** * @param {string} key * @param {string | boolean | undefined} value */ write(key, value) { const urlParams = new URLSearchParams(window.location.search); if (value !== undefined) { urlParams.set(key, String(value)); } else { urlParams.delete(key); } this.replaceHistory({ urlParams }); }, /** * @param {string} key */ remove(key) { this.write(key, undefined); }, /** * * @param {string} key * @returns {boolean | null} */ readBool(key) { const urlParams = new URLSearchParams(window.location.search); const parameter = urlParams.get(key); if (parameter) { return utils.isSerializedBooleanTrue(parameter); } return null; }, }; const localeStorageHelpers = { /** * @param {string} key */ readBool(key) { const saved = localStorage.getItem(key); if (saved) { return utils.isSerializedBooleanTrue(saved); } return null; }, /** * @param {string} key * @param {string | boolean | undefined} value */ write(key, value) { value !== undefined && value !== null ? localStorage.setItem(key, String(value)) : localStorage.removeItem(key); }, /** * @param {string} key */ remove(key) { this.write(key, undefined); }, }; const utils = { /** * @param {string} serialized * @returns {boolean} */ isSerializedBooleanTrue(serialized) { return serialized === "true" || serialized === "1"; }, }; const dom = { head: window.document.getElementsByTagName("head")[0], /** * @param {string} name */ queryOrCreateMetaElement(name) { let meta = /** @type {HTMLMetaElement | null} */ ( window.document.querySelector(`meta[name="${name}"]`) ); if (!meta) { meta = window.document.createElement("meta"); meta.name = name; this.head.appendChild(meta); } return meta; }, }; const env = (function initEnv() { const standalone = "standalone" in window.navigator && !!window.navigator.standalone; const userAgent = navigator.userAgent.toLowerCase(); const isChrome = userAgent.includes("chrome"); const safari = userAgent.includes("safari"); const safariOnly = safari && !isChrome; const macOS = userAgent.includes("mac os"); const iphone = userAgent.includes("iphone"); const ipad = userAgent.includes("ipad"); return { standalone, userAgent, isChrome, safari, safariOnly, macOS, iphone, ipad, }; })(); const ONE_SECOND_IN_MS = 1_000; const FIVE_SECOND_IN_MS = 5 * ONE_SECOND_IN_MS; const TEN_SECOND_IN_MS = 2 * FIVE_SECOND_IN_MS; const ONE_MINUTE_IN_MS = 6 * TEN_SECOND_IN_MS; const FIVE_MINUTES_IN_MS = 5 * ONE_MINUTE_IN_MS; const TEN_MINUTES_IN_MS = 2 * FIVE_MINUTES_IN_MS; const ONE_HOUR_IN_MS = 6 * TEN_MINUTES_IN_MS; const ONE_DAY_IN_MS = 24 * ONE_HOUR_IN_MS; const mainFrames = getElementById("frames"); const chartFrameSelectorLabelId = `selected-frame-selector-label`; const chartLabel = getElementById(chartFrameSelectorLabelId); const foldersLabel = getElementById(`folders-frame-selector-label`); const searchLabel = getElementById(`search-frame-selector-label`); const searchFrame = getElementById("search-frame"); const foldersFrame = getElementById("folders-frame"); const selectedFrame = getElementById("selected-frame"); const historyList = getElementById("history-list"); const searchInput = /** @type {HTMLInputElement} */ ( getElementById("search-input") ); const searchSmall = getElementById("search-small"); const searchResults = getElementById("search-results"); const presetTitle = getElementById("preset-title"); const presetDescription = getElementById("preset-description"); const foldersFilterAllCount = getElementById("folders-filter-all-count"); const foldersFilterFavoritesCount = getElementById( "folders-filter-favorites-count" ); const foldersFilterNewCount = getElementById("folders-filter-new-count"); const chartListElement = getElementById("chart-list"); const legendElement = getElementById("legend"); const bodyStyle = getComputedStyle(window.document.documentElement); const buttonFavorite = getElementById("button-favorite"); const buttonShare = getElementById("button-share"); const timeScaleDateButtons = getElementById("timescale-date-buttons"); const timeScaleHeightButtons = getElementById("timescale-height-buttons"); const dark = createSignal(true); function initFrames() { const localStorageKey = "checked-frame-selector-label"; let selectedFrameLabel = localStorage.getItem(localStorageKey); const fieldset = window.document.getElementById("frame-selectors"); if (!fieldset) throw "Fieldset should exist"; const children = Array.from(fieldset.children); /** @type {HTMLElement | undefined} */ let focusedSection = undefined; for (let i = 0; i < children.length; i++) { const element = children[i]; switch (element.tagName) { case "LABEL": { element.addEventListener("click", (event) => { const id = element.id; event.stopPropagation(); event.preventDefault(); const forId = element.getAttribute("for") || ""; const input = /** @type {HTMLInputElement | undefined} */ ( window.document.getElementById(forId) ); if (!input) throw "Shouldn't be possible"; selectedFrameLabel = id; localStorage.setItem(localStorageKey, id); input.checked = true; const sectionId = element.id.split("-").splice(0, 2).join("-"); const section = window.document.getElementById(sectionId); if (!section) { console.log(sectionId, section); throw "Section should exist"; } if (section === focusedSection) { return; } section.hidden = false; if (focusedSection?.parentElement === mainFrames) { focusedSection.hidden = true; } focusedSection = section; }); break; } } } if (selectedFrameLabel) { const frameLabel = window.document.getElementById(selectedFrameLabel); if (!frameLabel) throw "Frame should exist"; frameLabel.click(); } else { chartLabel.click(); } // When going from mobile view to desktop view, if selected frame was open, go to the folders frame new IntersectionObserver((entries) => { for (let i = 0; i < entries.length; i++) { if ( !entries[i].isIntersecting && entries[i].target === chartLabel && selectedFrameLabel === chartFrameSelectorLabelId ) { foldersLabel.click(); } } }).observe(chartLabel); window.document.addEventListener("keydown", (event) => { switch (event.key) { case "Escape": { event.stopPropagation(); event.preventDefault(); foldersLabel.click(); break; } case "/": { if (window.document.activeElement === searchInput) { return; } event.stopPropagation(); event.preventDefault(); searchLabel.click(); searchInput.focus(); break; } } }); } initFrames(); function createColors() { function lightRed() { const tailwindRed300 = "#fca5a5"; const tailwindRed800 = "#991b1b"; return dark() ? tailwindRed300 : tailwindRed800; } function red() { return "#e63636"; // 550 } function darkRed() { const tailwindRed900 = "#7f1d1d"; const tailwindRed100 = "#fee2e2"; return dark() ? tailwindRed900 : tailwindRed100; } function orange() { return bodyStyle.getPropertyValue("--orange"); // 550 } function darkOrange() { const tailwindOrange900 = "#7c2d12"; const tailwindOrange100 = "#ffedd5"; return dark() ? tailwindOrange900 : tailwindOrange100; } function amber() { return "#e78a05"; // 550 } function yellow() { return "#db9e03"; // 550 } function lime() { return "#74b713"; // 550 } function green() { return "#1cb454"; } function darkGreen() { const tailwindGreen900 = "#14532d"; const tailwindGreen100 = "#dcfce7"; return dark() ? tailwindGreen900 : tailwindGreen100; } function emerald() { return "#0ba775"; } function darkEmerald() { const tailwindEmerald900 = "#064e3b"; const tailwindEmerald100 = "#d1fae5"; return dark() ? tailwindEmerald900 : tailwindEmerald100; } function teal() { return "#10a697"; // 550 } function cyan() { return "#06a3c3"; // 550 } function sky() { return "#0794d8"; // 550 } function blue() { return "#2f73f1"; // 550 } function indigo() { return "#5957eb"; } function violet() { return "#834cf2"; } function purple() { return "#9d45f0"; } function fuchsia() { return "#cc37e1"; } function pink() { return "#e53882"; } function rose() { return "#ea3053"; } function darkRose() { const tailwindRose900 = "#881337"; const tailwindRose100 = "#ffe4e6"; return dark() ? tailwindRose900 : tailwindRose100; } function darkWhite() { const _ = dark(); return bodyStyle.getPropertyValue("--off-color"); } function gray() { const _ = dark(); return bodyStyle.getPropertyValue("--border-color"); } function white() { const _ = dark(); return bodyStyle.getPropertyValue("--color"); } function black() { const _ = dark(); return bodyStyle.getPropertyValue("--background-color"); } return { white, black, darkWhite, gray, lightBitcoin: yellow, bitcoin: orange, darkBitcoin: darkOrange, lightDollars: lime, dollars: emerald, darkDollars: darkEmerald, _1d: lightRed, _1w: red, _8d: orange, _13d: amber, _21d: yellow, _1m: lime, _34d: green, _55d: emerald, _89d: teal, _144d: cyan, _6m: sky, _1y: blue, _2y: indigo, _200w: violet, _4y: purple, _10y: fuchsia, p2pk: lime, p2pkh: violet, p2sh: emerald, p2wpkh: cyan, p2wsh: pink, p2tr: blue, crab: red, fish: lime, humpback: violet, plankton: emerald, shark: cyan, shrimp: pink, whale: blue, megalodon: purple, realizedPrice: orange, oneMonthHolders: cyan, threeMonthsHolders: lime, sth: yellow, sixMonthsHolder: red, oneYearHolders: pink, twoYearsHolders: purple, lth: fuchsia, balancedPrice: yellow, cointimePrice: yellow, trueMarketMeanPrice: blue, vaultedPrice: green, cvdd: lime, terminalPrice: red, loss: red, darkLoss: darkRed, profit: green, darkProfit: darkGreen, thermoCap: green, investorCap: rose, realizedCap: orange, ethereum: indigo, usdt: emerald, usdc: blue, ust: red, busd: yellow, usdd: emerald, frax: gray, dai: amber, tusd: indigo, pyusd: blue, darkLiveliness: darkRose, liveliness: rose, vaultedness: green, activityToVaultednessRatio: violet, up_to_1d: lightRed, up_to_1w: red, up_to_1m: orange, up_to_2m: orange, up_to_3m: orange, up_to_4m: orange, up_to_5m: orange, up_to_6m: orange, up_to_1y: orange, up_to_2y: orange, up_to_3y: orange, up_to_4y: orange, up_to_5y: orange, up_to_7y: orange, up_to_10y: orange, up_to_15y: orange, from_10y_to_15y: purple, from_7y_to_10y: violet, from_5y_to_7y: indigo, from_3y_to_5y: sky, from_2y_to_3y: teal, from_1y_to_2y: green, from_6m_to_1y: lime, from_3m_to_6m: yellow, from_1m_to_3m: amber, from_1w_to_1m: orange, from_1d_to_1w: red, from_1y: green, from_2y: teal, from_4y: indigo, from_10y: violet, from_15y: fuchsia, coinblocksCreated: purple, coinblocksDestroyed: red, coinblocksStored: green, momentum: [green, yellow, red], momentumGreen: green, momentumYellow: yellow, momentumRed: red, probability0_1p: red, probability0_5p: orange, probability1p: yellow, year_2009: yellow, year_2010: yellow, year_2011: yellow, year_2012: yellow, year_2013: yellow, year_2014: yellow, year_2015: yellow, year_2016: yellow, year_2017: yellow, year_2018: yellow, year_2019: yellow, year_2020: yellow, year_2021: yellow, year_2022: yellow, year_2023: yellow, year_2024: yellow, }; } const colors = createColors(); function initEverythingRelatedToPresets() { /** @type {Signal} */ const selected = createSignal(/** @type {any} */ (undefined)); const selectedLocalStorageKey = `selected-id`; const savedSelectedId = localStorage.getItem(selectedLocalStorageKey); const firstTime = !savedSelectedId; /** * @returns {PartialPresetTree} */ function createPartialTree() { function initConsts() { const xth = /** @type {const} */ ([ { id: "sth", key: "sth", name: "Short Term Holders", legend: "STH", }, { id: "lth", key: "lth", name: "Long Term Holders", legend: "LTH", }, ]); const upTo = /** @type {const} */ ([ { id: "up-to-1d", key: "up_to_1d", name: "Up To 1 Day", legend: "1D" }, { id: "up-to-1w", key: "up_to_1w", name: "Up To 1 Week", legend: "1W" }, { id: "up-to-1m", key: "up_to_1m", name: "Up To 1 Month", legend: "1M", }, { id: "up-to-2m", key: "up_to_2m", name: "Up To 2 Months", legend: "2M", }, { id: "up-to-3m", key: "up_to_3m", name: "Up To 3 Months", legend: "3M", }, { id: "up-to-4m", key: "up_to_4m", name: "Up To 4 Months", legend: "4M", }, { id: "up-to-5m", key: "up_to_5m", name: "Up To 5 Months", legend: "5M", }, { id: "up-to-6m", key: "up_to_6m", name: "Up To 6 Months", legend: "6M", }, { id: "up-to-1y", key: "up_to_1y", name: "Up To 1 Year", legend: "1Y" }, { id: "up-to-2y", key: "up_to_2y", name: "Up To 2 Years", legend: "2Y", }, { id: "up-to-3y", key: "up_to_3y", name: "Up To 3 Years", legend: "3Y", }, { id: "up-to-5y", key: "up_to_5y", name: "Up To 5 Years", legend: "5Y", }, { id: "up-to-7y", key: "up_to_7y", name: "Up To 7 Years", legend: "7Y", }, { id: "up-to-10y", key: "up_to_10y", name: "Up To 10 Years", legend: "10Y", }, { id: "up-to-15y", key: "up_to_15y", name: "Up To 15 Years", legend: "15Y", }, ]); const fromXToY = /** @type {const} */ ([ { id: "from-1d-to-1w", key: "from_1d_to_1w", name: "From 1 Day To 1 Week", legend: "1D - 1W", }, { id: "from-1w-to-1m", key: "from_1w_to_1m", name: "From 1 Week To 1 Month", legend: "1W - 1M", }, { id: "from-1m-to-3m", key: "from_1m_to_3m", name: "From 1 Month To 3 Months", legend: "1M - 3M", }, { id: "from-3m-to-6m", key: "from_3m_to_6m", name: "From 3 Months To 6 Months", legend: "3M - 6M", }, { id: "from-6m-to-1y", key: "from_6m_to_1y", name: "From 6 Months To 1 Year", legend: "6M - 1Y", }, { id: "from-1y-to-2y", key: "from_1y_to_2y", name: "From 1 Year To 2 Years", legend: "1Y - 2Y", }, { id: "from-2y-to-3y", key: "from_2y_to_3y", name: "From 2 Years To 3 Years", legend: "2Y - 3Y", }, { id: "from-3y-to-5y", key: "from_3y_to_5y", name: "From 3 Years To 5 Years", legend: "3Y - 5Y", }, { id: "from-5y-to-7y", key: "from_5y_to_7y", name: "From 5 Years To 7 Years", legend: "5Y - 7Y", }, { id: "from-7y-to-10y", key: "from_7y_to_10y", name: "From 7 Years To 10 Years", legend: "7Y - 10Y", }, { id: "from-10y-to-15y", key: "from_10y_to_15y", name: "From 10 Years To 15 Years", legend: "10Y - 15Y", }, ]); const fromX = /** @type {const} */ ([ { id: "from-1y", key: "from_1y", name: "From 1 Year", legend: "1Y+", }, { id: "from-2y", key: "from_2y", name: "From 2 Years", legend: "2Y+", }, { id: "from-4y", key: "from_4y", name: "From 4 Years", legend: "4Y+", }, { id: "from-10y", key: "from_10y", name: "From 10 Years", legend: "10Y+", }, { id: "from-15y", key: "from_15y", name: "From 15 Years", legend: "15Y+", }, ]); const year = /** @type {const} */ ([ { id: "year-2009", key: "year_2009", name: "2009" }, { id: "year-2010", key: "year_2010", name: "2010" }, { id: "year-2011", key: "year_2011", name: "2011" }, { id: "year-2012", key: "year_2012", name: "2012" }, { id: "year-2013", key: "year_2013", name: "2013" }, { id: "year-2014", key: "year_2014", name: "2014" }, { id: "year-2015", key: "year_2015", name: "2015" }, { id: "year-2016", key: "year_2016", name: "2016" }, { id: "year-2017", key: "year_2017", name: "2017" }, { id: "year-2018", key: "year_2018", name: "2018" }, { id: "year-2019", key: "year_2019", name: "2019" }, { id: "year-2020", key: "year_2020", name: "2020" }, { id: "year-2021", key: "year_2021", name: "2021" }, { id: "year-2022", key: "year_2022", name: "2022" }, { id: "year-2023", key: "year_2023", name: "2023" }, { id: "year-2024", key: "year_2024", name: "2024" }, ]); const age = /** @type {const} */ ([ { key: "", id: "", name: "", }, ...xth, ...upTo, ...fromXToY, ...fromX, ...year, ]); const size = /** @type {const} */ ([ { key: "plankton", name: "Plankton", size: "1 sat to 0.1 BTC", }, { key: "shrimp", name: "Shrimp", size: "0.1 sat to 1 BTC", }, { key: "crab", name: "Crab", size: "1 BTC to 10 BTC" }, { key: "fish", name: "Fish", size: "10 BTC to 100 BTC" }, { key: "shark", name: "Shark", size: "100 BTC to 1000 BTC" }, { key: "whale", name: "Whale", size: "1000 BTC to 10 000 BTC" }, { key: "humpback", name: "Humpback", size: "10 000 BTC to 100 000 BTC", }, { key: "megalodon", name: "Megalodon", size: "More than 100 000 BTC" }, ]); const type = /** @type {const} */ ([ { key: "p2pk", name: "P2PK" }, { key: "p2pkh", name: "P2PKH" }, { key: "p2sh", name: "P2SH" }, { key: "p2wpkh", name: "P2WPKH" }, { key: "p2wsh", name: "P2WSH" }, { key: "p2tr", name: "P2TR" }, ]); const address = /** @type {const} */ ([...size, ...type]); const liquidities = /** @type {const} */ ([ { key: "illiquid", id: "illiquid", name: "Illiquid", }, { key: "liquid", id: "liquid", name: "Liquid" }, { key: "highly_liquid", id: "highly-liquid", name: "Highly Liquid", }, ]); const averages = /** @type {const} */ ([ { name: "1 Week", key: "1w", days: 7 }, { name: "8 Days", key: "8d", days: 8 }, { name: "13 Days", key: "13d", days: 13 }, { name: "21 Days", key: "21d", days: 21 }, { name: "1 Month", key: "1m", days: 30 }, { name: "34 Days", key: "34d", days: 34 }, { name: "55 Days", key: "55d", days: 55 }, { name: "89 Days", key: "89d", days: 89 }, { name: "144 Days", key: "144d", days: 144 }, { name: "1 Year", key: "1y", days: 365 }, { name: "2 Years", key: "2y", days: 2 * 365 }, { name: "200 Weeks", key: "200w", days: 200 * 7 }, { name: "4 Years", key: "4y", days: 4 * 365 }, ]); const totalReturns = /** @type {const} */ ([ { name: "1 Day", key: "1d" }, { name: "1 Month", key: "1m" }, { name: "6 Months", key: "6m" }, { name: "1 Year", key: "1y" }, { name: "2 Years", key: "2y" }, { name: "3 Years", key: "3y" }, { name: "4 Years", key: "4y" }, { name: "6 Years", key: "6y" }, { name: "8 Years", key: "8y" }, { name: "10 Years", key: "10y" }, ]); const compoundReturns = /** @type {const} */ ([ { name: "4 Years", key: "4y" }, ]); const percentiles = /** @type {const} */ ([ { key: "median_price_paid", id: "median-price-paid", name: "Median", title: "Median Paid", value: 50, }, { key: "95p_price_paid", id: "95p-price-paid", name: `95%`, title: `95th Percentile Paid`, value: 95, }, { key: "90p_price_paid", id: "90p-price-paid", name: `90%`, title: `90th Percentile Paid`, value: 90, }, { key: "85p_price_paid", id: "85p-price-paid", name: `85%`, title: `85th Percentile Paid`, value: 85, }, { key: "80p_price_paid", id: "80p-price-paid", name: `80%`, title: `80th Percentile Paid`, value: 80, }, { key: "75p_price_paid", id: "75p-price-paid", name: `75%`, title: `75th Percentile Paid`, value: 75, }, { key: "70p_price_paid", id: "70p-price-paid", name: `70%`, title: `70th Percentile Paid`, value: 70, }, { key: "65p_price_paid", id: "65p-price-paid", name: `65%`, title: `65th Percentile Paid`, value: 65, }, { key: "60p_price_paid", id: "60p-price-paid", name: `60%`, title: `60th Percentile Paid`, value: 60, }, { key: "55p_price_paid", id: "55p-price-paid", name: `55%`, title: `55th Percentile Paid`, value: 55, }, { key: "45p_price_paid", id: "45p-price-paid", name: `45%`, title: `45th Percentile Paid`, value: 45, }, { key: "40p_price_paid", id: "40p-price-paid", name: `40%`, title: `40th Percentile Paid`, value: 40, }, { key: "35p_price_paid", id: "35p-price-paid", name: `35%`, title: `35th Percentile Paid`, value: 35, }, { key: "30p_price_paid", id: "30p-price-paid", name: `30%`, title: `30th Percentile Paid`, value: 30, }, { key: "25p_price_paid", id: "25p-price-paid", name: `25%`, title: `25th Percentile Paid`, value: 25, }, { key: "20p_price_paid", id: "20p-price-paid", name: `20%`, title: `20th Percentile Paid`, value: 20, }, { key: "15p_price_paid", id: "15p-price-paid", name: `15%`, title: `15th Percentile Paid`, value: 15, }, { key: "10p_price_paid", id: "10p-price-paid", name: `10%`, title: `10th Percentile Paid`, value: 10, }, { key: "05p_price_paid", id: "05p-price-paid", name: `5%`, title: `5th Percentile Paid`, value: 5, }, ]); return { xth, upTo, fromX, fromXToY, year, age, type, size, address, liquidities, averages, totalReturns, compoundReturns, percentiles, }; } const consts = initConsts(); /** * @typedef {(typeof consts.age)[number]["id"]} AgeCohortId * @typedef {Exclude} AgeCohortIdSub * @typedef {(typeof consts.address)[number]["key"]} AddressCohortId * @typedef {(typeof consts.liquidities[number]["id"])} LiquidityId * @typedef {`${LiquidityId}-${AddressCohortId}`} AddressCohortIdSplitByLiquidity * @typedef {AgeCohortId | AddressCohortId} AnyCohortId * @typedef {AnyCohortId | AddressCohortIdSplitByLiquidity | LiquidityId} AnyPossibleCohortId * @typedef {'' | `${AgeCohortIdSub | AddressCohortId | AddressCohortIdSplitByLiquidity | LiquidityId}-`} AnyDatasetPrefix * @typedef {(typeof consts.averages)[number]["key"]} AverageName * @typedef {(typeof consts.totalReturns)[number]["key"]} TotalReturnKey * @typedef {(typeof consts.compoundReturns)[number]["key"]} CompoundReturnKey * @typedef {(typeof consts.percentiles)[number]["id"]} PercentileId */ /** * @param {AnyPossibleCohortId} datasetId * @returns {AnyDatasetPrefix} */ function datasetIdToPrefix(datasetId) { return datasetId ? /** @type {const} */ (`${datasetId}-`) : /** @type {const} */ (""); } /** * * @param {Object} args * @param {Scale} args.scale * @param {string} args.title * @param {Color} args.color * @param {Unit} args.unit * @param {AnyDatasetPath} [args.keySum] * @param {AnyDatasetPath} [args.keyAverage] * @param {AnyDatasetPath} [args.keyMax] * @param {AnyDatasetPath} [args.key90p] * @param {AnyDatasetPath} [args.key75p] * @param {AnyDatasetPath} [args.keyMedian] * @param {AnyDatasetPath} [args.key25p] * @param {AnyDatasetPath} [args.key10p] * @param {AnyDatasetPath} [args.keyMin] * @returns {PartialPreset[]} */ function createRecapPresets({ scale, unit, title, keyAverage, color, keySum, keyMax, key90p, key75p, keyMedian, key25p, key10p, keyMin, }) { return [ ...(keySum ? [ { scale, icon: "➕", name: "Daily Sum", title: `${title} Daily Sum`, description: "", unit, bottom: [ { title: "Sum", color, datasetPath: keySum, }, ], }, ] : []), ...(keyAverage ? [ { scale, icon: "🌊", name: "Daily Average", title: `${title} Daily Average`, description: "", unit, bottom: [ { title: "Average", color, datasetPath: keyAverage, }, ], }, ] : []), ...(keyMax || key90p || key75p || keyMedian || key25p || key10p || keyMin ? [ { scale, icon: "%", name: "Daily Percentiles", title: `${title} Daily Percentiles`, description: "", unit, bottom: [ ...(keyMax ? [ { title: "Max", color, datasetPath: keyMax, }, ] : []), ...(key90p ? [ { title: "90%", color, datasetPath: key90p, }, ] : []), ...(key75p ? [ { title: "75%", color, datasetPath: key75p, }, ] : []), ...(keyMedian ? [ { title: "Median", color, datasetPath: keyMedian, }, ] : []), ...(key25p ? [ { title: "25%", color, datasetPath: key25p, }, ] : []), ...(key10p ? [ { title: "10%", color, datasetPath: key10p, }, ] : []), ...(keyMin ? [ { title: "Min", color, datasetPath: keyMin, }, ] : []), ], }, ] : []), ...(keyMax ? [ { scale, icon: "⬆️", name: "Daily Max", title: `${title} Daily Max`, description: "", unit, bottom: [ { title: "Max", color, datasetPath: keyMax, }, ], }, ] : []), ...(key90p ? [ { scale, icon: "9️⃣", name: "Daily 90th Percentile", title: `${title} Daily 90th Percentile`, description: "", unit, bottom: [ { title: "90%", color, datasetPath: key90p, }, ], }, ] : []), ...(key75p ? [ { scale, icon: "7️⃣", name: "Daily 75th Percentile", title: `${title} Size 75th Percentile`, description: "", unit, bottom: [ { title: "75%", color, datasetPath: key75p, }, ], }, ] : []), ...(keyMedian ? [ { scale, icon: "5️⃣", name: "Daily Median", title: `${title} Daily Median`, description: "", unit, bottom: [ { title: "Median", color, datasetPath: keyMedian, }, ], }, ] : []), ...(key25p ? [ { scale, icon: "2️⃣", name: "Daily 25th Percentile", title: `${title} Daily 25th Percentile`, description: "", unit, bottom: [ { title: "25%", color, datasetPath: key25p, }, ], }, ] : []), ...(key10p ? [ { scale, icon: "1️⃣", name: "Daily 10th Percentile", title: `${title} Daily 10th Percentile`, description: "", unit, bottom: [ { title: "10%", color, datasetPath: key10p, }, ], }, ] : []), ...(keyMin ? [ { scale, icon: "⬇️", name: "Daily Min", title: `${title} Daily Min`, description: "", unit, bottom: [ { title: "Min", color, datasetPath: keyMin, }, ], }, ] : []), ]; } /** * @param {Object} args * @param {Scale} args.scale * @param {Color} args.color * @param {AnyDatasetPath} args.valueDatasetPath * @param {AnyDatasetPath} args.ratioDatasetPath * @param {string} args.title * @returns {PartialPresetFolder} */ function createRatioFolder({ scale, color, valueDatasetPath, ratioDatasetPath, title, }) { return { name: "Ratio", tree: [ { scale, name: "Basic", icon: "➗", title: `Market Price To ${title} Ratio`, unit: "Ratio", description: "", top: [ { title: `SMA`, color, datasetPath: valueDatasetPath, }, ], bottom: [ { title: `Ratio`, type: "Baseline", datasetPath: ratioDatasetPath, options: { baseValue: { price: 1, }, }, }, { title: `Even`, color: colors.white, datasetPath: `${scale}-to-1`, options: { lineStyle: 3, lastValueVisible: false, }, }, ], }, { scale, name: "Averages", description: "", icon: "〰️", unit: "Ratio", title: `Market Price To ${title} Ratio Averages`, top: [ { title: `SMA`, color, datasetPath: valueDatasetPath, }, ], bottom: [ { title: `1Y`, color: colors._1y, datasetPath: /** @type {any} */ (`${ratioDatasetPath}-1y-sma`), }, { title: `1M`, color: colors._1m, datasetPath: `${ratioDatasetPath}-1m-sma`, }, { title: `1W`, color: colors._1w, datasetPath: `${ratioDatasetPath}-1w-sma`, }, { title: `Raw`, color: colors.white, datasetPath: ratioDatasetPath, }, { title: `Even`, color: colors.gray, datasetPath: `${scale}-to-1`, options: { lineStyle: 3, lastValueVisible: false, }, }, ], }, { scale, name: "Momentum Oscillator", title: `Market Price To ${title} Ratio 1Y SMA Momentum Oscillator`, description: "", unit: "Ratio", icon: "🔀", top: [ { title: `SMA`, color, datasetPath: valueDatasetPath, }, ], bottom: [ { title: `Momentum`, type: "Baseline", datasetPath: /** @type {any} */ ( `${ratioDatasetPath}-1y-sma-momentum-oscillator` ), }, { title: `Base`, color: colors.white, datasetPath: `${scale}-to-0`, options: { lineStyle: 3, lastValueVisible: false, }, }, ], }, { scale, name: "Top Percentiles", icon: "✈️", title: `Market Price To ${title} Ratio Top Percentiles`, description: "", unit: "Ratio", top: [ { title: `SMA`, color, datasetPath: valueDatasetPath, }, ], bottom: [ { title: `99.9%`, color: colors.probability0_1p, datasetPath: /** @type {any} */ (`${ratioDatasetPath}-99-9p`), }, { title: `99.5%`, color: colors.probability0_5p, datasetPath: `${ratioDatasetPath}-99-5p`, }, { title: `99%`, color: colors.probability1p, datasetPath: `${ratioDatasetPath}-99p`, }, { title: `Raw`, color: colors.white, datasetPath: ratioDatasetPath, }, ], }, { scale, name: "Bottom Percentiles", icon: "🤿", title: `Market Price To ${title} Ratio Bottom Percentiles`, description: "", unit: "Ratio", top: [ { title: `SMA`, color, datasetPath: valueDatasetPath, }, ], bottom: [ { title: `1%`, color: colors.probability1p, datasetPath: /** @type {any} */ (`${ratioDatasetPath}-1p`), }, { title: `0.5%`, color: colors.probability0_5p, datasetPath: `${ratioDatasetPath}-0-5p`, }, { title: `0.1%`, color: colors.probability0_1p, datasetPath: `${ratioDatasetPath}-0-1p`, }, { title: `Raw`, color: colors.white, datasetPath: ratioDatasetPath, }, ], }, { scale, name: "Top Probabilities", icon: "🚀", title: `${title} Top Probabilities`, description: "", unit: "US Dollars", top: [ { title: `0.1%`, color: colors.probability0_1p, datasetPath: /** @type {any} */ (`${valueDatasetPath}-99-9p`), }, { title: `0.5%`, color: colors.probability0_5p, datasetPath: `${valueDatasetPath}-99-5p`, }, { title: `1%`, color: colors.probability1p, datasetPath: `${valueDatasetPath}-99p`, }, ], }, { scale, name: "Bottom Probabilities", icon: "🚇", title: `${title} Bottom Probabilities`, description: "", unit: "US Dollars", top: [ { title: `0.1%`, color: colors.probability0_1p, datasetPath: `${valueDatasetPath}-0-1p`, }, { title: `0.5%`, color: colors.probability0_5p, datasetPath: `${valueDatasetPath}-0-5p`, }, { title: `1%`, color: colors.probability1p, datasetPath: `${valueDatasetPath}-1p`, }, ], }, ], }; } /** * @param {Scale} scale * @returns {PartialPresetFolder} */ function createMarketPresets(scale) { /** * @param {Scale} scale * @returns {PartialPresetFolder} */ function createAveragesPresets(scale) { return { name: "Averages", tree: [ { scale, icon: "~", name: "All", title: "All Moving Averages", description: "", unit: "US Dollars", top: consts.averages.map((average) => ({ title: average.key.toUpperCase(), color: colors[`_${average.key}`], datasetPath: `${scale}-to-price-${average.key}-sma`, })), }, ...consts.averages.map(({ name, key }) => createAveragePresetFolder({ scale, color: colors[`_${key}`], name, title: `${name} Market Price Moving Average`, key, }) ), ], }; } /** * * @param {Object} args * @param {Scale} args.scale * @param {Color} args.color * @param {string} args.name * @param {string} args.title * @param {AverageName} args.key * @returns {PartialPresetFolder} */ function createAveragePresetFolder({ scale, color, name, title, key }) { return { name, tree: [ { scale, name: "Average", title, description: "", unit: "US Dollars", icon: "~", top: [ { title: `SMA`, color, datasetPath: `${scale}-to-price-${key}-sma`, }, ], }, createRatioFolder({ scale, color, ratioDatasetPath: `${scale}-to-market-price-to-price-${key}-sma-ratio`, valueDatasetPath: `${scale}-to-price-${key}-sma`, title, }), ], }; } /** * @returns {PartialPresetFolder} */ function createReturnsPresets() { return { name: "Returns", tree: [ { name: "Total", tree: [ ...consts.totalReturns.map(({ name, key }) => createReturnsPreset({ scale: "date", name, title: `${name} Total`, key: `${key}-total`, }) ), ], }, { name: "Compound", tree: [ ...consts.compoundReturns.map(({ name, key }) => createReturnsPreset({ scale: "date", name, title: `${name} Compound`, key: `${key}-compound`, }) ), ], }, ], }; } /** * @param {Object} args * @param {Scale} args.scale * @param {string} args.name * @param {string} args.title * @param {`${TotalReturnKey}-total` | `${CompoundReturnKey}-compound`} args.key * @returns {PartialPreset} */ function createReturnsPreset({ scale, name, title, key }) { return { scale, name, description: "", icon: "🧾", title: `${title} Return`, unit: "Percentage", bottom: [ { title: `Return`, type: "Baseline", datasetPath: `date-to-price-${key}-return`, }, ], }; } /** * @returns {PartialPresetFolder} */ function createIndicatorsPresets() { return { name: "Indicators", tree: [], }; } return { name: "Market", tree: [ { scale, icon: "💵", name: "Price", title: "Market Price", description: "", unit: "US Dollars", }, { scale, icon: "♾️", name: "Capitalization", title: "Market Capitalization", description: "", unit: "US Dollars", bottom: [ { title: "Capitalization", datasetPath: `${scale}-to-market-cap`, color: colors.bitcoin, }, ], }, createAveragesPresets(scale), ...(scale === "date" ? [createReturnsPresets(), createIndicatorsPresets()] : []), ], }; } /** * @param {Scale} scale * @returns {PartialPresetFolder} */ function createBlocksPresets(scale) { return { name: "Blocks", tree: [ ...(scale === "date" ? /** @type {PartialPresetTree} */ ([ { scale, icon: "🧱", name: "Height", title: "Block Height", description: "", unit: "Height", bottom: [ { title: "Height", color: colors.bitcoin, datasetPath: `date-to-last-height`, }, ], }, { scale, name: "Mined", tree: [ { scale, icon: "D", name: "Daily Sum", title: "Daily Sum Of Blocks Mined", description: "", unit: "Count", bottom: [ { title: "Target", color: colors.white, datasetPath: `date-to-blocks-mined-1d-target`, options: { lineStyle: 3, }, }, { title: "1W Avg.", color: colors.momentumYellow, datasetPath: `date-to-blocks-mined-1w-sma`, defaultActive: false, }, { title: "1M Avg.", color: colors.bitcoin, datasetPath: `date-to-blocks-mined-1m-sma`, }, { title: "Mined", color: colors.darkBitcoin, datasetPath: `date-to-blocks-mined`, }, ], }, { scale, icon: "W", name: "Weekly Sum", title: "Weekly Sum Of Blocks Mined", description: "", unit: "Count", bottom: [ { title: "Target", color: colors.white, datasetPath: `date-to-blocks-mined-1w-target`, options: { lineStyle: 3, }, }, { title: "Sum Mined", color: colors.bitcoin, datasetPath: `date-to-blocks-mined-1w-sum`, }, ], }, { scale, icon: "M", name: "Monthly Sum", title: "Monthly Sum Of Blocks Mined", description: "", unit: "Count", bottom: [ { title: "Target", color: colors.white, datasetPath: `date-to-blocks-mined-1m-target`, options: { lineStyle: 3, }, }, { title: "Sum Mined", color: colors.bitcoin, datasetPath: `date-to-blocks-mined-1m-sum`, }, ], }, { scale, icon: "Y", name: "Yearly Sum", title: "Yearly Sum Of Blocks Mined", description: "", unit: "Count", bottom: [ { title: "Target", color: colors.white, datasetPath: `date-to-blocks-mined-1y-target`, options: { lineStyle: 3, }, }, { title: "Sum Mined", color: colors.bitcoin, datasetPath: `date-to-blocks-mined-1y-sum`, }, ], }, { scale, icon: "🧱", name: "Total", title: "Total Blocks Mined", description: "", unit: "Count", bottom: [ { title: "Mined", color: colors.bitcoin, datasetPath: `date-to-total-blocks-mined`, }, ], }, ], }, { scale, name: "Size", tree: createRecapPresets({ scale, title: "Block Size", color: colors.darkWhite, unit: "Megabytes", keySum: "date-to-block-size-1d-sum", keyAverage: "date-to-block-size-1d-average", keyMax: "date-to-block-size-1d-max", key90p: "date-to-block-size-1d-90p", key75p: "date-to-block-size-1d-75p", keyMedian: "date-to-block-size-1d-median", key25p: "date-to-block-size-1d-25p", key10p: "date-to-block-size-1d-10p", keyMin: "date-to-block-size-1d-min", }), }, { scale, name: "Weight", tree: createRecapPresets({ scale, title: "Block Weight", color: colors.darkWhite, unit: "Weight", keyAverage: "date-to-block-weight-1d-average", keyMax: "date-to-block-weight-1d-max", key90p: "date-to-block-weight-1d-90p", key75p: "date-to-block-weight-1d-75p", keyMedian: "date-to-block-weight-1d-median", key25p: "date-to-block-weight-1d-25p", key10p: "date-to-block-weight-1d-10p", keyMin: "date-to-block-weight-1d-min", }), }, { scale, name: "VBytes", tree: createRecapPresets({ scale, title: "Block VBytes", color: colors.darkWhite, unit: "Virtual Bytes", keyAverage: "date-to-block-vbytes-1d-average", keyMax: "date-to-block-vbytes-1d-max", key90p: "date-to-block-vbytes-1d-90p", key75p: "date-to-block-vbytes-1d-75p", keyMedian: "date-to-block-vbytes-1d-median", key25p: "date-to-block-vbytes-1d-25p", key10p: "date-to-block-vbytes-1d-10p", keyMin: "date-to-block-vbytes-1d-min", }), }, { scale, name: "Interval", tree: createRecapPresets({ scale, title: "Block Interval", color: colors.darkWhite, unit: "Seconds", keyAverage: "date-to-block-interval-1d-average", keyMax: "date-to-block-interval-1d-max", key90p: "date-to-block-interval-1d-90p", key75p: "date-to-block-interval-1d-75p", keyMedian: "date-to-block-interval-1d-median", key25p: "date-to-block-interval-1d-25p", key10p: "date-to-block-interval-1d-10p", keyMin: "date-to-block-interval-1d-min", }), }, ]) : /** @type {PartialPresetTree} */ ([ { scale, icon: "📏", name: "Size", title: "Block Size", description: "", unit: "Megabytes", bottom: [ { title: "Size", color: colors.darkWhite, datasetPath: `height-to-block-size`, }, ], }, { scale, icon: "🏋️", name: "Weight", title: "Block Weight", description: "", unit: "Weight", bottom: [ { title: "Weight", color: colors.darkWhite, datasetPath: `height-to-block-weight`, }, ], }, { scale, icon: "👾", name: "VBytes", title: "Block VBytes", description: "", unit: "Virtual Bytes", bottom: [ { title: "VBytes", color: colors.darkWhite, datasetPath: `height-to-block-vbytes`, }, ], }, { scale, icon: "⏰", name: "Interval", title: "Block Interval", description: "", unit: "Seconds", bottom: [ { title: "Interval", color: colors.darkWhite, datasetPath: `height-to-block-interval`, }, ], }, ])), { scale, icon: "📏", name: "Cumulative Size", title: "Cumulative Block Size", description: "", unit: "Megabytes", bottom: [ { title: "Size", color: colors.darkWhite, datasetPath: `${scale}-to-cumulative-block-size`, }, ], }, ], }; } /** * @param {Scale} scale * @returns {PartialPresetFolder} */ function createMinersPresets(scale) { return { name: "Miners", tree: [ { name: "Coinbases", tree: [ ...(scale === "date" ? /** @type {PartialPresetTree} */ ([ { name: "Last", tree: [ { scale, icon: "🍊", name: "In Bitcoin", title: "Last Coinbase In Bitcoin", description: "", unit: "US Dollars", bottom: [ { title: "Last", color: colors.bitcoin, datasetPath: `${scale}-to-last-coinbase`, }, ], }, { scale, icon: "💵", name: "In Dollars", title: "Last Coinbase In Dollars", description: "", unit: "US Dollars", bottom: [ { title: "Last", color: colors.dollars, datasetPath: `${scale}-to-last-coinbase-in-dollars`, }, ], }, ], }, { scale, name: "Daily Sum", tree: [ { scale, icon: "🍊", name: "In Bitcoin", title: "Daily Sum Of Coinbases In Bitcoin", description: "", unit: "Bitcoin", bottom: [ { title: "Sum", color: colors.bitcoin, datasetPath: `${scale}-to-coinbase`, }, ], }, { scale, icon: "💵", name: "In Dollars", title: "Daily Sum Of Coinbases In Dollars", description: "", unit: "US Dollars", bottom: [ { title: "Sum", color: colors.dollars, datasetPath: `${scale}-to-coinbase-in-dollars`, }, ], }, ], }, { scale, name: "Yearly Sum", tree: [ { scale, icon: "🍊", name: "In Bitcoin", title: "Yearly Sum Of Coinbases In Bitcoin", description: "", unit: "Bitcoin", bottom: [ { title: "Sum", color: colors.bitcoin, datasetPath: `${scale}-to-coinbase-1y-sum`, }, ], }, { scale, icon: "💵", name: "In Dollars", title: "Yearly Sum Of Coinbases In Dollars", description: "", unit: "US Dollars", bottom: [ { title: "Sum", color: colors.dollars, datasetPath: `${scale}-to-coinbase-in-dollars-1y-sum`, }, ], }, ], }, { scale, name: "Cumulative", tree: [ { scale, icon: "🍊", name: "In Bitcoin", title: "Cumulative Coinbases In Bitcoin", description: "", unit: "Bitcoin", bottom: [ { title: "Coinbases", color: colors.bitcoin, datasetPath: `${scale}-to-cumulative-coinbase`, }, ], }, { scale, icon: "💵", name: "In Dollars", title: "Cumulative Coinbases In Dollars", description: "", unit: "US Dollars", bottom: [ { title: "Coinbases", color: colors.dollars, datasetPath: `${scale}-to-cumulative-coinbase-in-dollars`, }, ], }, ], }, ]) : []), ], }, { name: "Subsidies", tree: [ ...(scale === "date" ? /** @type {PartialPresetTree} */ ([ { name: "Last", tree: [ { scale, icon: "🍊", name: "In Bitcoin", title: "Last Subsidy In Bitcoin", description: "", unit: "Bitcoin", bottom: [ { title: "Last", color: colors.bitcoin, datasetPath: `${scale}-to-last-subsidy`, }, ], }, { scale, icon: "💵", name: "In Dollars", title: "Last Subsidy In Dollars", description: "", unit: "US Dollars", bottom: [ { title: "Last", color: colors.dollars, datasetPath: `${scale}-to-last-subsidy-in-dollars`, }, ], }, ], }, { scale, name: "Daily Sum", tree: [ { scale, icon: "🍊", name: "In Bitcoin", title: "Daily Sum Of Subsidies In Bitcoin", description: "", unit: "Bitcoin", bottom: [ { title: "Sum", color: colors.bitcoin, datasetPath: `${scale}-to-subsidy`, }, ], }, { scale, icon: "💵", name: "In Dollars", title: "Daily Sum Of Subsidies In Dollars", description: "", unit: "US Dollars", bottom: [ { title: "Sum", color: colors.dollars, datasetPath: `${scale}-to-subsidy-in-dollars`, }, ], }, ], }, { scale, name: "Yearly Sum", tree: [ { scale, icon: "🍊", name: "In Bitcoin", title: "Yearly Sum Of Subsidies In Bitcoin", description: "", unit: "Bitcoin", bottom: [ { title: "Sum", color: colors.bitcoin, datasetPath: `${scale}-to-subsidy-1y-sum`, }, ], }, { scale, icon: "💵", name: "In Dollars", title: "Yearly Sum Of Subsidies In Dollars", description: "", unit: "US Dollars", bottom: [ { title: "Sum", color: colors.dollars, datasetPath: `${scale}-to-subsidy-in-dollars-1y-sum`, }, ], }, ], }, { scale, name: "Cumulative", tree: [ { scale, icon: "🍊", name: "In Bitcoin", title: "Cumulative Subsidies In Bitcoin", description: "", unit: "Bitcoin", bottom: [ { title: "Subsidies", color: colors.bitcoin, datasetPath: `${scale}-to-cumulative-subsidy`, }, ], }, { scale, icon: "💵", name: "In Dollars", title: "Cumulative Subsidies In Dollars", description: "", unit: "US Dollars", bottom: [ { title: "Subsidies", color: colors.dollars, datasetPath: `${scale}-to-cumulative-subsidy-in-dollars`, }, ], }, ], }, ]) : []), ], }, { name: "Fees", tree: [ ...(scale === "date" ? /** @type {PartialPresetTree} */ ([ { name: "Last", tree: [ { scale, icon: "🍊", name: "In Bitcoin", title: "Last Fees In Bitcoin", description: "", unit: "Bitcoin", bottom: [ { title: "Last", color: colors.bitcoin, datasetPath: `${scale}-to-last-fees`, }, ], }, { scale, icon: "💵", name: "In Dollars", title: "Last Fees In Dollars", description: "", unit: "US Dollars", bottom: [ { title: "Last", color: colors.dollars, datasetPath: `${scale}-to-last-fees-in-dollars`, }, ], }, ], }, { scale, name: "Daily Sum", tree: [ { scale, icon: "🍊", name: "In Bitcoin", title: "Daily Sum Of Fees In Bitcoin", description: "", unit: "Bitcoin", bottom: [ { title: "Sum", color: colors.bitcoin, datasetPath: `${scale}-to-fees`, }, ], }, { scale, icon: "💵", name: "In Dollars", title: "Daily Sum Of Fees In Dollars", description: "", unit: "US Dollars", bottom: [ { title: "Sum", color: colors.dollars, datasetPath: `${scale}-to-fees-in-dollars`, }, ], }, ], }, { scale, name: "Yearly Sum", tree: [ { scale, icon: "🍊", name: "In Bitcoin", title: "Yearly Sum Of Fees In Bitcoin", description: "", unit: "Bitcoin", bottom: [ { title: "Sum", color: colors.bitcoin, datasetPath: `${scale}-to-fees-1y-sum`, }, ], }, { scale, icon: "💵", name: "In Dollars", title: "Yearly Sum Of Fees In Dollars", description: "", unit: "US Dollars", bottom: [ { title: "Sum", color: colors.dollars, datasetPath: `${scale}-to-fees-in-dollars-1y-sum`, }, ], }, ], }, { scale, name: "Cumulative", tree: [ { scale, icon: "🍊", name: "In Bitcoin", title: "Cumulative Fees In Bitcoin", description: "", unit: "Bitcoin", bottom: [ { title: "Fees", color: colors.bitcoin, datasetPath: `${scale}-to-cumulative-fees`, }, ], }, { scale, icon: "💵", name: "In Dollars", title: "Cumulative Fees In Dollars", description: "", unit: "US Dollars", bottom: [ { title: "Fees", color: colors.dollars, datasetPath: `${scale}-to-cumulative-fees-in-dollars`, }, ], }, ], }, ]) : []), ], }, { scale, icon: "⚔️", name: "Subsidy V. Fees", title: "Subsidy V. Fees", description: "", unit: "Percentage", bottom: [ { title: "Subsidy", color: colors.bitcoin, datasetPath: `${scale}-to-subsidy-to-coinbase-ratio`, }, { title: "Fees", color: colors.darkBitcoin, datasetPath: `${scale}-to-fees-to-coinbase-ratio`, }, ], }, ...(scale === "date" ? /** @type {PartialPresetTree} */ ([ { scale, icon: "🧮", name: "Puell Multiple", title: "Puell Multiple", description: "", unit: "", bottom: [ { title: "Multiple", color: colors.bitcoin, datasetPath: `date-to-puell-multiple`, }, ], }, { scale, icon: "⛏️", name: "Hash Rate", title: "Hash Rate", description: "", unit: "ExaHash / Second", bottom: [ { title: "1M SMA", color: colors.momentumYellow, datasetPath: `date-to-hash-rate-1m-sma`, }, { title: "1W SMA", color: colors.bitcoin, datasetPath: `date-to-hash-rate-1w-sma`, }, { title: "Rate", color: colors.darkBitcoin, datasetPath: `date-to-hash-rate`, }, ], }, { scale, icon: "🎗️", name: "Hash Ribbon", title: "Hash Ribbon", description: "", unit: "ExaHash / Second", bottom: [ { title: "1M SMA", color: colors.profit, datasetPath: `date-to-hash-rate-1m-sma`, }, { title: "2M SMA", color: colors.loss, datasetPath: `date-to-hash-rate-2m-sma`, }, ], }, { scale, icon: "🏷️", name: "Hash Price", title: "Hash Price", description: "", unit: "Dollars / (PetaHash / Second)", bottom: [ { title: "Price", color: colors.dollars, datasetPath: `date-to-hash-price`, }, ], }, ]) : []), { scale, icon: "🏋️", name: "Difficulty", title: "Difficulty", description: "", unit: "", bottom: [ { title: "Difficulty", color: colors.bitcoin, datasetPath: `${scale}-to-difficulty`, }, ], }, ...(scale === "date" ? /** @type {PartialPresetTree} */ ([ { scale, icon: "📊", name: "Difficulty Adjustment", title: "Difficulty Adjustment", description: "", unit: "Percentage", bottom: [ { title: "Adjustment", type: "Baseline", datasetPath: `${scale}-to-difficulty-adjustment`, }, ], }, ]) : []), { scale, icon: "🏭", name: "Annualized Issuance", title: "Annualized Issuance", description: "", unit: "Bitcoin", bottom: [ { title: "Issuance", color: colors.bitcoin, datasetPath: `${scale}-to-annualized-issuance`, }, ], }, { scale, icon: "🏗️", name: "Yearly Inflation Rate", title: "Yearly Inflation Rate", description: "", unit: "Percentage", bottom: [ { title: "Rate", color: colors.bitcoin, datasetPath: `${scale}-to-yearly-inflation-rate`, }, ], }, // For scale === "height" // block_size, // block_weight, // block_vbytes, // block_interval, ], }; } /** * @param {Scale} scale * @returns {PartialPresetFolder} */ function createTransactionsPresets(scale) { return { name: "Transactions", tree: [ { scale, icon: "🖐️", name: "Count", title: "Transaction Count", description: "", unit: "Count", bottom: [ { title: "1M SMA", color: colors.momentumYellow, datasetPath: `${scale}-to-transaction-count-1m-sma`, }, { title: "1W SMA", color: colors.bitcoin, datasetPath: `${scale}-to-transaction-count-1w-sma`, }, { title: "Raw", color: colors.darkBitcoin, datasetPath: `${scale}-to-transaction-count`, }, ], }, { name: "Volume", tree: [ { scale, icon: "🍊", name: "In Bitcoin", title: "Transaction Volume", description: "", unit: "Bitcoin", bottom: [ { title: "1M SMA", color: colors.momentumYellow, datasetPath: `${scale}-to-transaction-volume-1m-sma`, }, { title: "1W SMA", color: colors.bitcoin, datasetPath: `${scale}-to-transaction-volume-1w-sma`, }, { title: "Raw", color: colors.darkBitcoin, datasetPath: `${scale}-to-transaction-volume`, }, ], }, { scale, icon: "💵", name: "In Dollars", title: "Transaction Volume In Dollars", description: "", unit: "US Dollars", bottom: [ { title: "1M SMA", color: colors.lightDollars, datasetPath: `${scale}-to-transaction-volume-in-dollars-1m-sma`, }, { title: "1W SMA", color: colors.dollars, datasetPath: `${scale}-to-transaction-volume-in-dollars-1w-sma`, }, { title: "Raw", color: colors.darkDollars, datasetPath: `${scale}-to-transaction-volume-in-dollars`, }, ], }, ], }, { name: "Annualized Volume", tree: [ { scale, icon: "🍊", name: "In Bitcoin", title: "Annualized Transaction Volume", description: "", unit: "Bitcoin", bottom: [ { title: "Volume", color: colors.bitcoin, datasetPath: `${scale}-to-annualized-transaction-volume`, }, ], }, { scale, icon: "💵", name: "In Dollars", title: "Annualized Transaction Volume In Dollars", description: "", unit: "US Dollars", bottom: [ { title: "Volume", color: colors.dollars, datasetPath: `${scale}-to-annualized-transaction-volume-in-dollars`, }, ], }, ], }, { scale, icon: "💨", name: "Velocity", title: "Transactions Velocity", description: "", unit: "", bottom: [ { title: "Transactions Velocity", color: colors.bitcoin, datasetPath: `${scale}-to-transaction-velocity`, }, ], }, { scale, icon: "⏰", name: "Per Second", title: "Transactions Per Second", description: "", unit: "Transactions", bottom: [ { title: "1M SMA", color: colors.lightBitcoin, datasetPath: `${scale}-to-transactions-per-second-1m-sma`, }, { title: "1W SMA", color: colors.bitcoin, datasetPath: `${scale}-to-transactions-per-second-1w-sma`, }, { title: "Raw", color: colors.darkBitcoin, datasetPath: `${scale}-to-transactions-per-second`, }, ], }, ], }; } /** * @param {Object} args * @param {Scale} args.scale * @param {AnyPossibleCohortId} args.datasetId * @param {string} args.title * @param {Color} args.color * @returns {PartialPresetFolder} */ function createCohortPresetUTXOFolder({ scale, color, datasetId, title }) { const datasetPrefix = datasetIdToPrefix(datasetId); return { name: "UTXOs", tree: [ { scale, name: `Count`, title: `${title} Unspent Transaction Outputs Count`, description: "", unit: "Count", icon: "🎫", bottom: [ { title: "Count", color, datasetPath: `${scale}-to-${datasetPrefix}utxo-count`, }, ], }, ], }; } /** * @param {Object} args * @param {Scale} args.scale * @param {AnyPossibleCohortId} args.datasetId * @param {string} args.title * @param {Color} args.color * @returns {PartialPresetFolder} */ function createCohortPresetRealizedFolder({ scale, color, datasetId, title, }) { const datasetPrefix = datasetIdToPrefix(datasetId); return { name: "Realized", tree: [ { scale, name: `Price`, title: `${title} Realized Price`, description: "", unit: "US Dollars", icon: "🏷️", top: [ { title: "Realized Price", color, datasetPath: `${scale}-to-${datasetPrefix}realized-price`, }, ], }, createRatioFolder({ scale, color, ratioDatasetPath: `${scale}-to-market-price-to-${datasetPrefix}realized-price-ratio`, valueDatasetPath: `${scale}-to-${datasetPrefix}realized-price`, title: `${title} Realized Price`, }), { scale, name: `Capitalization`, title: `${title} Realized Capitalization`, description: "", unit: "US Dollars", icon: "💰", bottom: [ { title: `${name} Realized Cap.`, color, datasetPath: `${scale}-to-${datasetPrefix}realized-cap`, }, ...(datasetId ? /** @type {const} */ ([ { title: "Realized Cap.", color: colors.bitcoin, datasetPath: `${scale}-to-realized-cap`, defaultActive: false, }, ]) : []), ], }, { scale, name: `Capitalization 1M Net Change`, title: `${title} Realized Capitalization 1 Month Net Change`, description: "", unit: "US Dollars", icon: "🔀", bottom: [ { title: `Net Change`, type: "Baseline", datasetPath: `${scale}-to-${datasetPrefix}realized-cap-1m-net-change`, }, ], }, { scale, name: `Profit`, title: `${title} Realized Profit`, description: "", unit: "US Dollars", icon: "🎉", bottom: [ { title: "Realized Profit", datasetPath: `${scale}-to-${datasetPrefix}realized-profit`, color: colors.profit, }, ], }, { scale, name: "Loss", title: `${title} Realized Loss`, description: "", unit: "US Dollars", icon: "⚰️", bottom: [ { title: "Realized Loss", datasetPath: `${scale}-to-${datasetPrefix}realized-loss`, color: colors.loss, }, ], }, { scale, name: `PNL`, title: `${title} Realized Profit And Loss`, description: "", unit: "US Dollars", icon: "⚖️", bottom: [ { title: "Profit", color: colors.profit, datasetPath: `${scale}-to-${datasetPrefix}realized-profit`, type: "Baseline", }, { title: "Loss", color: colors.loss, datasetPath: `${scale}-to-${datasetPrefix}negative-realized-loss`, type: "Baseline", }, ], }, { scale, name: `Net PNL`, title: `${title} Net Realized Profit And Loss`, description: "", unit: "US Dollars", icon: "⚖️", bottom: [ { title: "Net PNL", type: "Baseline", datasetPath: `${scale}-to-${datasetPrefix}net-realized-profit-and-loss`, }, ], }, { scale, name: `Net PNL Relative To Market Cap`, title: `${title} Net Realized Profit And Loss Relative To Market Capitalization`, description: "", unit: "Percentage", icon: "➗", bottom: [ { title: "Net", type: "Baseline", datasetPath: `${scale}-to-${datasetPrefix}net-realized-profit-and-loss-to-market-cap-ratio`, }, ], }, { scale, name: `Cumulative Profit`, title: `${title} Cumulative Realized Profit`, description: "", unit: "US Dollars", icon: "🎊", bottom: [ { title: "Cumulative Realized Profit", color: colors.profit, datasetPath: `${scale}-to-${datasetPrefix}cumulative-realized-profit`, }, ], }, { scale, name: "Cumulative Loss", title: `${title} Cumulative Realized Loss`, description: "", unit: "US Dollars", icon: "☠️", bottom: [ { title: "Cumulative Realized Loss", color: colors.loss, datasetPath: `${scale}-to-${datasetPrefix}cumulative-realized-loss`, }, ], }, { scale, name: `Cumulative Net PNL`, title: `${title} Cumulative Net Realized Profit And Loss`, description: "", unit: "US Dollars", icon: "➕", bottom: [ { title: "Cumulative Net Realized PNL", type: "Baseline", datasetPath: `${scale}-to-${datasetPrefix}cumulative-net-realized-profit-and-loss`, }, ], }, { scale, name: `Cumulative Net PNL 30 Day Change`, title: `${title} Cumulative Net Realized Profit And Loss 30 Day Change`, description: "", unit: "US Dollars", icon: "🗓️", bottom: [ { title: "Cumulative Net Realized PNL 30d Change", datasetPath: `${scale}-to-${datasetPrefix}cumulative-net-realized-profit-and-loss-1m-net-change`, type: "Baseline", }, ], }, { scale, name: `Value Created`, title: `${title} Value Created`, description: "", unit: "US Dollars", icon: "➕", bottom: [ { title: "Value", color: colors.profit, datasetPath: `${scale}-to-${datasetPrefix}value-created`, }, ], }, { scale, name: `Value Destroyed`, title: `${title} Value Destroyed`, description: "", unit: "US Dollars", icon: "☄️", bottom: [ { title: "Value", color: colors.loss, datasetPath: `${scale}-to-${datasetPrefix}value-destroyed`, }, ], }, { scale, name: `Spent Output Profit Ratio - SOPR`, title: `${title} Spent Output Profit Ratio`, description: "", unit: "Percentage", icon: "➗", bottom: [ { title: "SOPR", datasetPath: `${scale}-to-${datasetPrefix}spent-output-profit-ratio`, type: "Baseline", options: { baseValue: { price: 1, }, }, }, ], }, ], }; } /** * @param {Object} args * @param {Scale} args.scale * @param {AnyPossibleCohortId} args.datasetId * @param {string} args.title * @param {Color} args.color * @returns {PartialPresetFolder} */ function createCohortPresetUnrealizedFolder({ scale, color, datasetId, title, }) { const datasetPrefix = datasetIdToPrefix(datasetId); return { name: "Unrealized", tree: [ { scale, name: `Profit`, title: `${title} Unrealized Profit`, description: "", unit: "US Dollars", icon: "🤑", bottom: [ { title: "Profit", datasetPath: `${scale}-to-${datasetPrefix}unrealized-profit`, color: colors.profit, }, ], }, { scale, name: "Loss", title: `${title} Unrealized Loss`, description: "", unit: "US Dollars", icon: "😭", bottom: [ { title: "Loss", datasetPath: `${scale}-to-${datasetPrefix}unrealized-loss`, color: colors.loss, }, ], }, { scale, name: `PNL`, title: `${title} Unrealized Profit And Loss`, description: "", unit: "US Dollars", icon: "🤔", bottom: [ { title: "Profit", color: colors.profit, datasetPath: `${scale}-to-${datasetPrefix}unrealized-profit`, type: "Baseline", }, { title: "Loss", color: colors.loss, datasetPath: `${scale}-to-${datasetPrefix}negative-unrealized-loss`, type: "Baseline", }, ], }, { scale, name: `Net PNL`, title: `${title} Net Unrealized Profit And Loss`, description: "", unit: "US Dollars", icon: "⚖️", bottom: [ { title: "Net Unrealized PNL", datasetPath: `${scale}-to-${datasetPrefix}net-unrealized-profit-and-loss`, type: "Baseline", }, ], }, { scale, name: `Net PNL Relative To Market Cap`, title: `${title} Net Unrealized Profit And Loss Relative To Total Market Capitalization`, description: "", unit: "Percentage", icon: "➗", bottom: [ { title: "Relative Net Unrealized PNL", datasetPath: `${scale}-to-${datasetPrefix}net-unrealized-profit-and-loss-to-market-cap-ratio`, type: "Baseline", }, ], }, ], }; } /** * @param {Object} args * @param {Scale} args.scale * @param {AnyPossibleCohortId} args.datasetId * @param {string} args.title * @param {Color} args.color * @returns {PartialPresetFolder} */ function createCohortPresetSupplyFolder({ scale, color, datasetId, title, }) { const datasetPrefix = datasetIdToPrefix(datasetId); return { name: "Supply", tree: [ { name: "Absolute", tree: [ { scale, name: "All", title: `${title} Profit And Loss`, icon: "❌", description: "", unit: "US Dollars", bottom: [ { title: "In Profit", color: colors.profit, datasetPath: `${scale}-to-${datasetPrefix}supply-in-profit`, }, { title: "In Loss", color: colors.loss, datasetPath: `${scale}-to-${datasetPrefix}supply-in-loss`, }, { title: "Total", color: colors.white, datasetPath: `${scale}-to-${datasetPrefix}supply`, }, { title: "Halved Total", color: colors.gray, datasetPath: `${scale}-to-${datasetPrefix}halved-supply`, options: { lineStyle: 4, }, }, ], }, { scale, name: `Total`, title: `${title} Total supply`, icon: "∑", description: "", unit: "Bitcoin", bottom: [ { title: "Supply", color, datasetPath: `${scale}-to-${datasetPrefix}supply`, }, ], }, { scale, name: "In Profit", title: `${title} Supply In Profit`, description: "", unit: "Bitcoin", icon: "📈", bottom: [ { title: "Supply", color: colors.profit, datasetPath: `${scale}-to-${datasetPrefix}supply-in-profit`, }, ], }, { scale, name: "In Loss", title: `${title} Supply In Loss`, description: "", unit: "Bitcoin", icon: "📉", bottom: [ { title: "Supply", color: colors.loss, datasetPath: `${scale}-to-${datasetPrefix}supply-in-loss`, }, ], }, ], }, { name: "Relative To Circulating", tree: [ { scale, name: "All", title: `${title} Profit And Loss Relative To Circulating Supply`, description: "", unit: "Percentage", icon: "🔀", bottom: [ { title: "In Profit", color: colors.profit, datasetPath: `${scale}-to-${datasetPrefix}supply-in-profit-to-circulating-supply-ratio`, }, { title: "In Loss", color: colors.loss, datasetPath: `${scale}-to-${datasetPrefix}supply-in-loss-to-circulating-supply-ratio`, }, { title: "100%", color: colors.white, datasetPath: `${scale}-to-${datasetPrefix}supply-to-circulating-supply-ratio`, }, { title: "50%", color: colors.gray, datasetPath: `${scale}-to-${datasetPrefix}halved-supply-to-circulating-supply-ratio`, options: { lineStyle: 4, }, }, ], }, { scale, name: `Total`, title: `${title} Total supply Relative To Circulating Supply`, description: "", unit: "Percentage", icon: "∑", bottom: [ { title: "Supply", color, datasetPath: `${scale}-to-${datasetPrefix}supply-to-circulating-supply-ratio`, }, ], }, { scale, name: "In Profit", title: `${title} Supply In Profit Relative To Circulating Supply`, description: "", unit: "Percentage", icon: "📈", bottom: [ { title: "Supply", color: colors.profit, datasetPath: `${scale}-to-${datasetPrefix}supply-in-profit-to-circulating-supply-ratio`, }, ], }, { scale, name: "In Loss", title: `${title} Supply In Loss Relative To Circulating Supply`, description: "", unit: "Percentage", icon: "📉", bottom: [ { title: "Supply", color: colors.loss, datasetPath: `${scale}-to-${datasetPrefix}supply-in-loss-to-circulating-supply-ratio`, }, ], }, ], }, { name: "Relative To Own", tree: [ { scale, name: "All", title: `${title} Supply In Profit And Loss Relative To Own Supply`, description: "", unit: "Percentage", icon: "🔀", bottom: [ { title: "In Profit", color: colors.profit, datasetPath: `${scale}-to-${datasetPrefix}supply-in-profit-to-own-supply-ratio`, }, { title: "In Loss", color: colors.loss, datasetPath: `${scale}-to-${datasetPrefix}supply-in-loss-to-own-supply-ratio`, }, { title: "100%", color: colors.white, datasetPath: `${scale}-to-100`, options: { lastValueVisible: false, }, }, { title: "50%", color: colors.gray, datasetPath: `${scale}-to-50`, options: { lineStyle: 4, lastValueVisible: false, }, }, ], }, { scale, name: "In Profit", title: `${title} Supply In Profit Relative To Own Supply`, description: "", unit: "Percentage", icon: "📈", bottom: [ { title: "Supply", color: colors.profit, datasetPath: `${scale}-to-${datasetPrefix}supply-in-profit-to-own-supply-ratio`, }, ], }, { scale, name: "In Loss", title: `${title} Supply In Loss Relative To Own Supply`, description: "", unit: "Percentage", icon: "📉", bottom: [ { title: "Supply", color: colors.loss, datasetPath: `${scale}-to-${datasetPrefix}supply-in-loss-to-own-supply-ratio`, }, ], }, ], }, // createMomentumPresetFolder({ // datasets: datasets[scale], // scale, // id: `${scale}-${id}-supply-in-profit-and-loss-percentage-self`, // title: `${title} Supply In Profit And Loss (% Self)`, // datasetId: `${datasetId}SupplyPNL%Self`, // }), ], }; } /** * @param {Object} args * @param {Scale} args.scale * @param {AnyPossibleCohortId} args.datasetId * @param {string} args.title * @param {Color} args.color * @returns {PartialPresetFolder} */ function createCohortPresetPricesPaidFolder({ scale, color, datasetId, title, }) { /** * @param {Object} args * @param {Scale} args.scale * @param {AnyPossibleCohortId} args.cohortId * @param {PercentileId} args.id * @returns {AnyDatasetPath} */ function generatePath({ scale, cohortId, id }) { const datasetPrefix = datasetIdToPrefix(cohortId); return /** @type {const} */ (`${scale}-to-${datasetPrefix}${id}`); } return { name: "Prices Paid", tree: [ { scale, name: `Average`, title: `${title} Average Price Paid - Realized Price`, description: "", unit: "US Dollars", icon: "~", top: [ { title: "Average", color, datasetPath: `${scale}-to-${datasetIdToPrefix( datasetId )}realized-price`, }, ], }, { scale, name: `Deciles`, title: `${title} deciles`, icon: "🌗", description: "", unit: "US Dollars", top: consts.percentiles .filter(({ value }) => Number(value) % 10 === 0) .map(({ name, id }) => { const datasetPath = generatePath({ scale, cohortId: datasetId, id, }); return { datasetPath, color, title: name, }; }), }, ...consts.percentiles.map((percentile) => { /** @type {PartialPreset} */ const preset = { scale, name: percentile.name, title: `${title} ${percentile.title}`, description: "", unit: "US Dollars", icon: "🌓", top: [ { title: percentile.name, color, datasetPath: generatePath({ scale, cohortId: datasetId, id: percentile.id, }), }, ], }; return preset; }), ], }; } /** * @param {Object} args * @param {string} args.name * @param {Scale} args.scale * @param {AnyPossibleCohortId} args.datasetId * @param {string} args.title * @param {Color} args.color * @returns {PartialPresetTree} */ function createCohortPresetList({ name, scale, color, datasetId, title }) { return [ createCohortPresetUTXOFolder({ color, datasetId, scale, title, }), createCohortPresetRealizedFolder({ color, datasetId, scale, title, }), createCohortPresetUnrealizedFolder({ color, datasetId, scale, title, }), createCohortPresetSupplyFolder({ color, datasetId, scale, title, }), createCohortPresetPricesPaidFolder({ color, datasetId, scale, title, }), ]; } /** * @param {Object} args * @param {Scale} args.scale * @param {string} args.name * @param {AddressCohortId | ""} args.datasetId * @param {Color} args.color * @returns {PartialPresetFolder} */ function createLiquidityFolder({ scale, color, name, datasetId }) { return { name: `Split By Liquidity`, tree: consts.liquidities.map((liquidity) => { /** @type {PartialPresetFolder} */ const folder = { name: liquidity.name, tree: createCohortPresetList({ title: `${liquidity.name} ${name}`, name: `${liquidity.name} ${name}`, scale, color, datasetId: !datasetId ? liquidity.id : `${liquidity.id}-${datasetId}`, }), }; return folder; }), }; } /** * @param {Object} args * @param {Scale} args.scale * @param {string} args.name * @param {AnyPossibleCohortId} args.datasetId * @param {string} args.title * @param {Color} args.color * @returns {PartialPresetFolder} */ function createCohortPresetFolder({ scale, color, name, datasetId, title, }) { return { name, tree: createCohortPresetList({ title, name, scale, color, datasetId: datasetId, }), }; } /** * @param {Scale} scale * @returns {PartialPresetFolder} */ function createHodlersPresets(scale) { return { name: "Hodlers", tree: [ { scale, name: `Hodl Supply`, title: `Hodl Supply`, description: "", icon: "🌊", unit: "Percentage", bottom: [ { title: `24h`, color: colors.up_to_1d, datasetPath: `${scale}-to-up-to-1d-supply-to-circulating-supply-ratio`, }, ...consts.fromXToY.map(({ key, id, name, legend }) => ({ title: legend, color: colors[key], datasetPath: /** @type {const} */ ( `${scale}-to-${id}-supply-to-circulating-supply-ratio` ), })), { title: `15y+`, color: colors.from_15y, datasetPath: `${scale}-to-from-15y-supply-to-circulating-supply-ratio`, }, ], }, ...consts.xth.map(({ key, id, name, legend }) => createCohortPresetFolder({ scale, color: colors[key], name: legend, datasetId: id, title: name, }) ), { name: "Up To X", tree: consts.upTo.map(({ key, id, name }) => createCohortPresetFolder({ scale, color: colors[key], name, datasetId: id, title: name, }) ), }, { name: "From X To Y", tree: consts.fromXToY.map(({ key, id, name }) => createCohortPresetFolder({ scale, color: colors[key], name, datasetId: id, title: name, }) ), }, { name: "From X", tree: consts.fromX.map(({ key, id, name }) => createCohortPresetFolder({ scale, color: colors[key], name, datasetId: id, title: name, }) ), }, { name: "Years", tree: consts.year.map(({ key, id, name }) => createCohortPresetFolder({ scale, color: colors[key], name, datasetId: id, title: name, }) ), }, ], }; } /** * @param {Object} args * @param {Scale} args.scale * @param {string} args.name * @param {AddressCohortId} args.datasetId * @param {Color} args.color * @returns {PartialPreset} */ function createAddressCountPreset({ scale, color, name, datasetId }) { return { scale, name: `Address Count`, title: `${name} Address Count`, description: "", unit: "Count", icon: "📕", bottom: [ { title: "Address Count", color, datasetPath: `${scale}-to-${datasetId}-address-count`, }, ], }; } /** * @param {Object} args * @param {Scale} args.scale * @param {string} args.name * @param {AddressCohortId} args.datasetId * @param {Color} args.color * @param { string} [args.filenameAddon] * @returns {PartialPresetFolder} */ function createAddressPresetFolder({ scale, color, name, filenameAddon, datasetId, }) { return { name: filenameAddon ? `${name} - ${filenameAddon}` : name, tree: [ createAddressCountPreset({ scale, name, datasetId, color }), ...createCohortPresetList({ title: name, scale, name, color, datasetId, }), createLiquidityFolder({ scale, name, datasetId, color, }), ], }; } /** * @param {Scale} scale * @returns {PartialPresetFolder} */ function createAddressesPresets(scale) { return { name: "Addresses", tree: [ { scale, name: `Total Non Empty Addresses`, title: `Total Non Empty Address`, description: "", unit: "Count", icon: "💳", bottom: [ { title: `Total Non Empty Address`, color: colors.bitcoin, datasetPath: `${scale}-to-address-count`, }, ], }, { scale, name: `New Addresses`, title: `New Addresses`, description: "", unit: "Count", icon: "🏡", bottom: [ { title: `New Addresses`, color: colors.bitcoin, datasetPath: `${scale}-to-new-addresses`, }, ], }, { scale, name: `Total Addresses Created`, title: `Total Addresses Created`, description: "", unit: "Count", icon: "🏠", bottom: [ { title: `Total Addresses Created`, color: colors.bitcoin, datasetPath: `${scale}-to-created-addresses`, }, ], }, { scale, name: `Total Empty Addresses`, title: `Total Empty Addresses`, description: "", unit: "Count", icon: "🗑️", bottom: [ { title: `Total Empty Addresses`, color: colors.darkWhite, datasetPath: `${scale}-to-empty-addresses`, }, ], }, { name: "By Size", tree: consts.size.map(({ key, name, size }) => createAddressPresetFolder({ scale, color: colors[key], name, filenameAddon: size, datasetId: key, }) ), }, { scale, name: "By Type", tree: consts.type.map(({ key, name }) => createAddressPresetFolder({ scale, color: colors[key], name, datasetId: key, }) ), }, ], }; } /** * @param {Scale} scale * @returns {PartialPresetFolder} */ function createCointimePresets(scale) { return { name: "Cointime Economics", tree: [ { name: "Prices", tree: [ { scale, icon: "🔀", name: "All", title: "All Cointime Prices", description: "", unit: "US Dollars", top: [ { title: "Vaulted Price", color: colors.vaultedness, datasetPath: `${scale}-to-vaulted-price`, }, { title: "Active Price", color: colors.liveliness, datasetPath: `${scale}-to-active-price`, }, { title: "True Market Mean", color: colors.trueMarketMeanPrice, datasetPath: `${scale}-to-true-market-mean`, }, { title: "Realized Price", color: colors.bitcoin, datasetPath: `${scale}-to-realized-price`, }, { title: "Cointime", color: colors.cointimePrice, datasetPath: `${scale}-to-cointime-price`, }, ], }, { name: "Active", tree: [ { scale, icon: "❤️", name: "Price", title: "Active Price", description: "", unit: "US Dollars", top: [ { title: "Active Price", color: colors.liveliness, datasetPath: `${scale}-to-active-price`, }, ], }, createRatioFolder({ color: colors.liveliness, ratioDatasetPath: `${scale}-to-market-price-to-active-price-ratio`, scale, title: "Active Price", valueDatasetPath: `${scale}-to-active-price`, }), ], }, { name: "Vaulted", tree: [ { scale, icon: "🏦", name: "Price", title: "Vaulted Price", description: "", unit: "US Dollars", top: [ { title: "Vaulted Price", color: colors.vaultedness, datasetPath: `${scale}-to-vaulted-price`, }, ], }, createRatioFolder({ color: colors.vaultedness, ratioDatasetPath: `${scale}-to-market-price-to-vaulted-price-ratio`, scale, title: "Vaulted Price", valueDatasetPath: `${scale}-to-vaulted-price`, }), ], }, { name: "True Market Mean", tree: [ { scale, icon: "〰️", name: "Price", title: "True Market Mean", description: "", unit: "US Dollars", top: [ { title: "True Market Mean", color: colors.trueMarketMeanPrice, datasetPath: `${scale}-to-true-market-mean`, }, ], }, createRatioFolder({ color: colors.liveliness, ratioDatasetPath: `${scale}-to-market-price-to-true-market-mean-ratio`, scale, title: "True Market Mean", valueDatasetPath: `${scale}-to-true-market-mean`, }), ], }, { name: "Cointime Price", tree: [ { scale, icon: "⏱️", name: "Price", title: "Cointime Price", description: "", unit: "US Dollars", top: [ { title: "Cointime", color: colors.cointimePrice, datasetPath: `${scale}-to-cointime-price`, }, ], }, createRatioFolder({ color: colors.cointimePrice, ratioDatasetPath: `${scale}-to-market-price-to-cointime-price-ratio`, scale, title: "Cointime", valueDatasetPath: `${scale}-to-cointime-price`, }), ], }, ], }, { name: "Capitalizations", tree: [ { scale, icon: "🔀", name: "All", title: "Cointime Capitalizations", description: "", unit: "US Dollars", bottom: [ { title: "Market Cap", color: colors.white, datasetPath: `${scale}-to-market-cap`, }, { title: "Realized Cap", color: colors.realizedCap, datasetPath: `${scale}-to-realized-cap`, }, { title: "Investor Cap", color: colors.investorCap, datasetPath: `${scale}-to-investor-cap`, }, { title: "Thermo Cap", color: colors.thermoCap, datasetPath: `${scale}-to-thermo-cap`, }, ], }, { scale, icon: "⛏️", name: "Thermo Cap", title: "Thermo Cap", description: "", unit: "US Dollars", bottom: [ { title: "Thermo Cap", color: colors.thermoCap, datasetPath: `${scale}-to-thermo-cap`, }, ], }, { scale, icon: "🧑‍💼", name: "Investor Cap", title: "Investor Cap", description: "", unit: "US Dollars", bottom: [ { title: "Investor Cap", color: colors.investorCap, datasetPath: `${scale}-to-investor-cap`, }, ], }, { scale, icon: "➗", name: "Thermo Cap To Investor Cap Ratio", title: "Thermo Cap To Investor Cap Ratio", description: "", unit: "Percentage", bottom: [ { title: "Ratio", color: colors.bitcoin, datasetPath: `${scale}-to-thermo-cap-to-investor-cap-ratio`, }, ], }, ], }, { name: "Coinblocks", tree: [ { scale, icon: "🧱", name: "All", title: "All Coinblocks", description: "", unit: "Coinblocks", bottom: [ { title: "Coinblocks Created", color: colors.coinblocksCreated, datasetPath: `${scale}-to-coinblocks-created`, }, { title: "Coinblocks Destroyed", color: colors.coinblocksDestroyed, datasetPath: `${scale}-to-coinblocks-destroyed`, }, { title: "Coinblocks Stored", color: colors.coinblocksStored, datasetPath: `${scale}-to-coinblocks-stored`, }, ], }, { scale, icon: "🧊", name: "Created", title: "Coinblocks Created", description: "", unit: "Coinblocks", bottom: [ { title: "Coinblocks Created", color: colors.coinblocksCreated, datasetPath: `${scale}-to-coinblocks-created`, }, ], }, { scale, icon: "⛓️‍💥", name: "Destroyed", title: "Coinblocks Destroyed", description: "", unit: "Coinblocks", bottom: [ { title: "Coinblocks Destroyed", color: colors.coinblocksDestroyed, datasetPath: `${scale}-to-coinblocks-destroyed`, }, ], }, { scale, icon: "🗄️", name: "Stored", title: "Coinblocks Stored", description: "", unit: "Coinblocks", bottom: [ { title: "Coinblocks Stored", color: colors.coinblocksStored, datasetPath: `${scale}-to-coinblocks-stored`, }, ], }, ], }, { name: "Cumulative Coinblocks", tree: [ { scale, icon: "🔀", name: "All", title: "All Cumulative Coinblocks", description: "", unit: "Coinblocks", bottom: [ { title: "Cumulative Coinblocks Created", color: colors.coinblocksCreated, datasetPath: `${scale}-to-cumulative-coinblocks-created`, }, { title: "Cumulative Coinblocks Destroyed", color: colors.coinblocksDestroyed, datasetPath: `${scale}-to-cumulative-coinblocks-destroyed`, }, { title: "Cumulative Coinblocks Stored", color: colors.coinblocksStored, datasetPath: `${scale}-to-cumulative-coinblocks-stored`, }, ], }, { scale, icon: "🧊", name: "Created", title: "Cumulative Coinblocks Created", description: "", unit: "Coinblocks", bottom: [ { title: "Cumulative Coinblocks Created", color: colors.coinblocksCreated, datasetPath: `${scale}-to-cumulative-coinblocks-created`, }, ], }, { scale, icon: "⛓️‍💥", name: "Destroyed", title: "Cumulative Coinblocks Destroyed", description: "", unit: "Coinblocks", bottom: [ { title: "Cumulative Coinblocks Destroyed", color: colors.coinblocksDestroyed, datasetPath: `${scale}-to-cumulative-coinblocks-destroyed`, }, ], }, { scale, icon: "🗄️", name: "Stored", title: "Cumulative Coinblocks Stored", description: "", unit: "Coinblocks", bottom: [ { title: "Cumulative Coinblocks Stored", color: colors.coinblocksStored, datasetPath: `${scale}-to-cumulative-coinblocks-stored`, }, ], }, ], }, { name: "Liveliness & Vaultedness", tree: [ { scale, icon: "❤️", name: "Liveliness - Activity", title: "Liveliness (Activity)", description: "", unit: "", bottom: [ { title: "Liveliness", color: colors.liveliness, datasetPath: `${scale}-to-liveliness`, }, ], }, { scale, icon: "🏦", name: "Vaultedness", title: "Vaultedness", description: "", unit: "", bottom: [ { title: "Vaultedness", color: colors.vaultedness, datasetPath: `${scale}-to-vaultedness`, }, ], }, { scale, icon: "⚔️", name: "Versus", title: "Liveliness V. Vaultedness", description: "", unit: "", bottom: [ { title: "Liveliness", color: colors.liveliness, datasetPath: `${scale}-to-liveliness`, }, { title: "Vaultedness", color: colors.vaultedness, datasetPath: `${scale}-to-vaultedness`, }, ], }, { scale, icon: "➗", name: "Activity To Vaultedness Ratio", title: "Activity To Vaultedness Ratio", description: "", unit: "Percentage", bottom: [ { title: "Activity To Vaultedness Ratio", color: colors.activityToVaultednessRatio, datasetPath: `${scale}-to-activity-to-vaultedness-ratio`, }, ], }, { scale, icon: "❤️", name: "Concurrent Liveliness - Supply Adjusted Coindays Destroyed", title: "Concurrent Liveliness - Supply Adjusted Coindays Destroyed", description: "", unit: "", bottom: [ { title: "Concurrent Liveliness 14d Median", color: colors.liveliness, datasetPath: `${scale}-to-concurrent-liveliness-2w-median`, }, { title: "Concurrent Liveliness", color: colors.darkLiveliness, datasetPath: `${scale}-to-concurrent-liveliness`, }, ], }, { scale, icon: "📊", name: "Liveliness Incremental Change", title: "Liveliness Incremental Change", description: "", unit: "", bottom: [ { title: "Liveliness Incremental Change", color: colors.darkLiveliness, type: "Baseline", datasetPath: `${scale}-to-liveliness-net-change`, }, { title: "Liveliness Incremental Change 14 Day Median", color: colors.liveliness, type: "Baseline", datasetPath: `${scale}-to-liveliness-net-change-2w-median`, }, ], }, ], }, { name: "Supply", tree: [ { scale, icon: "🏦", name: "Vaulted", title: "Vaulted Supply", description: "", unit: "Bitcoin", bottom: [ { title: "Vaulted Supply", color: colors.vaultedness, datasetPath: `${scale}-to-vaulted-supply`, }, ], }, { scale, icon: "❤️", name: "Active", title: "Active Supply", description: "", unit: "Bitcoin", bottom: [ { title: "Active Supply", color: colors.liveliness, datasetPath: `${scale}-to-active-supply`, }, ], }, { scale, icon: "⚔️", name: "Vaulted V. Active", title: "Vaulted V. Active", description: "", unit: "Bitcoin", bottom: [ { title: "Circulating Supply", color: colors.coinblocksCreated, datasetPath: `${scale}-to-supply`, }, { title: "Vaulted Supply", color: colors.vaultedness, datasetPath: `${scale}-to-vaulted-supply`, }, { title: "Active Supply", color: colors.liveliness, datasetPath: `${scale}-to-active-supply`, }, ], }, // TODO: Fix, Bad data // { // id: 'asymptomatic-supply-regions', // icon: IconTablerDirections, // name: 'Asymptomatic Supply Regions', // title: 'Asymptomatic Supply Regions', // description: '', // applyPreset(params) { // return applyMultipleSeries({ // ...params, // priceScaleOptions: { // halved: true, // }, // list: [ // { // id: 'min-vaulted', // title: 'Min Vaulted Supply', // color: colors.vaultedness, // dataset: params.`/${scale}-to-dateToMinVaultedSupply, // }, // { // id: 'max-active', // title: 'Max Active Supply', // color: colors.liveliness, // dataset: params.`/${scale}-to-dateToMaxActiveSupply, // }, // ], // }) // }, // }, { scale, icon: "🏦", name: "Vaulted Net Change", title: "Vaulted Supply Net Change", description: "", unit: "Bitcoin", bottom: [ { title: "Vaulted Supply Net Change", color: colors.vaultedness, datasetPath: `${scale}-to-vaulted-supply-net-change`, }, ], }, { scale, icon: "❤️", name: "Active Net Change", title: "Active Supply Net Change", description: "", unit: "Bitcoin", bottom: [ { title: "Active Supply Net Change", color: colors.liveliness, datasetPath: `${scale}-to-active-supply-net-change`, }, ], }, { scale, icon: "⚔️", name: "Active VS. Vaulted 90D Net Change", title: "Active VS. Vaulted 90 Day Supply Net Change", description: "", unit: "Bitcoin", bottom: [ { title: "Active Supply Net Change", color: colors.liveliness, datasetPath: `${scale}-to-active-supply-3m-net-change`, type: "Baseline", }, { title: "Vaulted Supply Net Change", color: colors.vaultedPrice, type: "Baseline", datasetPath: `${scale}-to-vaulted-supply-3m-net-change`, }, ], }, // TODO: Fix, Bad data // { // id: 'vaulted-supply-annualized-net-change', // icon: IconTablerBuildingBank, // name: 'Vaulted Annualized Net Change', // title: 'Vaulted Supply Annualized Net Change', // description: '', // applyPreset(params) { // return applyMultipleSeries({ // ...params, // priceScaleOptions: { // halved: true, // }, // list: [ // { // id: 'vaulted-annualized-supply-net-change', // title: 'Vaulted Supply Annualized Net Change', // color: colors.vaultedness, // dataset: // `/${scale}-to-vaultedAnnualizedSupplyNetChange, // }, // ], // }) // }, // }, // TODO: Fix, Bad data // { // id: 'vaulting-rate', // icon: IconTablerBuildingBank, // name: 'Vaulting Rate', // title: 'Vaulting Rate', // description: '', // applyPreset(params) { // return applyMultipleSeries({ // ...params, // priceScaleOptions: { // halved: true, // }, // list: [ // { // id: 'vaulting-rate', // title: 'Vaulting Rate', // color: colors.vaultedness, // dataset: `${scale}-to-vaultingRate, // }, // { // id: 'nominal-inflation-rate', // title: 'Nominal Inflation Rate', // color: colors.orange, // dataset: params.`/${scale}-to-dateToYearlyInflationRate, // }, // ], // }) // }, // }, // TODO: Fix, Bad data // { // id: 'active-supply-net-change-decomposition', // icon: IconTablerArrowsCross, // name: 'Active Supply Net Change Decomposition (90D)', // title: 'Active Supply Net 90 Day Change Decomposition', // description: '', // applyPreset(params) { // return applyMultipleSeries({ // ...params, // priceScaleOptions: { // halved: true, // }, // list: [ // { // id: 'issuance-change', // title: 'Change From Issuance', // color: colors.emerald, // dataset: // params.params.datasets[scale] // [scale].activeSupplyChangeFromIssuance90dChange, // }, // { // id: 'transactions-change', // title: 'Change From Transactions', // color: colors.rose, // dataset: // params.params.datasets[scale] // [scale].activeSupplyChangeFromTransactions90dChange, // }, // // { // // id: 'active', // // title: 'Active Supply', // // color: colors.liveliness, // // dataset: `${scale}-to-activeSupply, // // }, // ], // }) // }, // }, { scale, icon: "📈", name: "In Profit", title: "Cointime Supply In Profit", description: "", unit: "Bitcoin", bottom: [ { title: "Circulating Supply", color: colors.coinblocksCreated, datasetPath: `${scale}-to-supply`, }, { title: "Vaulted Supply", color: colors.vaultedness, datasetPath: `${scale}-to-vaulted-supply`, }, { title: "Supply in profit", color: colors.bitcoin, datasetPath: `${scale}-to-supply-in-profit`, }, ], }, { scale, icon: "📉", name: "In Loss", title: "Cointime Supply In Loss", description: "", unit: "Bitcoin", bottom: [ { title: "Circulating Supply", color: colors.coinblocksCreated, datasetPath: `${scale}-to-supply`, }, { title: "Active Supply", color: colors.liveliness, datasetPath: `${scale}-to-active-supply`, }, { title: "Supply in Loss", color: colors.bitcoin, datasetPath: `${scale}-to-supply-in-loss`, }, ], }, ], }, { scale, icon: "🏭", name: "Cointime Yearly Inflation Rate", title: "Cointime-Adjusted Yearly Inflation Rate", description: "", unit: "Percentage", bottom: [ { title: "Cointime Adjusted", color: colors.coinblocksCreated, datasetPath: `${scale}-to-cointime-adjusted-yearly-inflation-rate`, }, { title: "Nominal", color: colors.bitcoin, datasetPath: `${scale}-to-yearly-inflation-rate`, }, ], }, { scale, icon: "💨", name: "Cointime Velocity", title: "Cointime-Adjusted Transactions Velocity", description: "", unit: "", bottom: [ { title: "Cointime Adjusted", color: colors.coinblocksCreated, datasetPath: `${scale}-to-cointime-adjusted-velocity`, }, { title: "Nominal", color: colors.bitcoin, datasetPath: `${scale}-to-transaction-velocity`, }, ], }, ], }; } return [ { name: "Dashboards", tree: [], }, { name: "Charts", tree: [ { name: "By Date", tree: [ createMarketPresets("date"), createBlocksPresets("date"), createMinersPresets("date"), createTransactionsPresets("date"), ...createCohortPresetList({ scale: "date", color: colors.bitcoin, datasetId: "", name: "", title: "", }), createLiquidityFolder({ scale: "date", color: colors.bitcoin, datasetId: "", name: "", }), createHodlersPresets("date"), createAddressesPresets("date"), createCointimePresets("date"), ], }, { name: "By Height", tree: [ createMarketPresets("height"), createBlocksPresets("height"), createMinersPresets("height"), createTransactionsPresets("height"), ...createCohortPresetList({ scale: "height", color: colors.bitcoin, datasetId: "", name: "", title: "", }), createLiquidityFolder({ scale: "height", color: colors.bitcoin, datasetId: "", name: "", }), createHodlersPresets("height"), createAddressesPresets("height"), createCointimePresets("height"), ], }, ], }, ]; } const partialTree = createPartialTree(); /** @type {string[]} */ const presetsIds = []; /** @type {Preset[]} */ const presetsList = []; /** @type {HTMLDetailsElement[]} */ const detailsList = []; /** @typedef {'all' | 'favorites' | 'new'} FoldersFilter */ const foldersFilterLocalStorageKey = "folders-filter"; const filter = createSignal( /** @type {FoldersFilter} */ ( localStorage.getItem(foldersFilterLocalStorageKey) || "all" ) ); const favoritesCount = createSignal(0); const newCount = createSignal(0); /** @param {Preset} preset */ function createCountersEffects(preset) { let firstFavoritesRun = true; createEffect(() => { if (preset.isFavorite()) { favoritesCount.set((c) => c + 1); } else if (!firstFavoritesRun) { favoritesCount.set((c) => c - 1); } firstFavoritesRun = false; }); let firstNewRun = true; createEffect(() => { if (!preset.visited()) { newCount.set((c) => c + 1); } else if (!firstNewRun) { newCount.set((c) => c - 1); } firstNewRun = false; }); } /** @param {Preset} preset */ function presetToVisitedLocalStorageKey(preset) { return `${preset.id}-visited`; } /** * @param {Preset} preset * @param {Series | SeriesBlueprint} series */ function presetAndSeriesToLocalStorageKey(preset, series) { return `${preset.id}-${stringToId(series.title)}`; } /** * @param {Object} args * @param {string} args.name * @param {string} args.inputName * @param {string} args.inputId * @param {string} args.inputValue * @param {string} args.labelTitle * @param {VoidFunction} args.onClick */ function createLabel({ inputId, inputName, inputValue, labelTitle, name, onClick, }) { const label = window.document.createElement("label"); const input = window.document.createElement("input"); input.type = "radio"; input.name = inputName; input.id = inputId; input.value = inputValue; label.append(input); label.id = `${inputId}-label`; // @ts-ignore label.for = inputId; label.title = labelTitle; const spanMain = window.document.createElement("span"); spanMain.classList.add("main"); label.append(spanMain); const spanName = window.document.createElement("span"); spanName.classList.add("name"); spanName.innerHTML = name; spanMain.append(spanName); label.addEventListener("click", (event) => { event.stopPropagation(); event.preventDefault(); onClick(); }); return { label, input, spanMain, spanName, }; } /** * @param {Object} args * @param {Preset} args.preset * @param {string} args.frame * @param {string} [args.name] * @param {string} [args.top] * @param {string} [args.id] * @param {Owner | null} [args.owner] */ function createPresetLabel({ preset, frame, name, top, id, owner }) { const { input, label, spanMain, spanName } = createLabel({ inputId: `${preset.id}-${frame}${id || ""}-selector`, inputValue: preset.id, inputName: `preset-${frame}${id || ""}`, labelTitle: preset.title, name: name || preset.name, onClick: () => selected.set(preset), }); if (top) { const small = window.document.createElement("small"); small.innerHTML = top; label.insertBefore(small, spanMain); } const spanEmoji = window.document.createElement("span"); spanEmoji.classList.add("emoji"); spanEmoji.innerHTML = preset.icon; spanMain.prepend(spanEmoji); /** @type {HTMLSpanElement | undefined} */ let spanNew; if (!preset.visited()) { spanNew = window.document.createElement("span"); spanNew.classList.add("new"); spanMain.append(spanNew); } function createFavoriteEffect() { // @ts-ignore createEffect((_wasFavorite) => { const wasFavorite = /** @type {boolean} */ (_wasFavorite); const isFavorite = preset.isFavorite(); if (!wasFavorite && isFavorite) { const iconFavorite = window.document.createElement("svg"); spanMain.append(iconFavorite); iconFavorite.outerHTML = ''; } else if (wasFavorite && !isFavorite) { spanMain.lastElementChild?.remove(); } return isFavorite; }, false); } function createUpdateEffect() { createEffect(() => { if (selected()?.id === preset.id) { input.checked = true; spanNew?.remove(); preset.visited.set(true); localStorage.setItem(presetToVisitedLocalStorageKey(preset), "1"); localStorage.setItem(selectedLocalStorageKey, preset.id); } else if (input.checked) { input.checked = false; } }); } if (owner !== undefined) { runWithOwner(owner, () => { createUpdateEffect(); createFavoriteEffect(); }); } else { createUpdateEffect(); createFavoriteEffect(); } return label; } /** * @param {Preset} preset */ function presetToFavoriteLocalStorageKey(preset) { return `${preset.id}-favorite`; } /** * @param {PartialPresetTree} partialTree * @param {HTMLElement} parent * @param {FilePath | undefined} path * @returns {Accessor} */ function processPartialTree(partialTree, parent, path = undefined) { const ul = window.document.createElement("ul"); parent.appendChild(ul); /** @type {Accessor[]} */ const listForSum = []; partialTree.forEach((anyPartial, partialIndex) => { const li = window.document.createElement("li"); ul.appendChild(li); if ("tree" in anyPartial) { const folderId = stringToId( `${(path || [])?.map(({ name }) => name).join(" ")} ${ anyPartial.name } folder` ); /** @type {Omit} */ const restFolder = { id: folderId, }; Object.assign(anyPartial, restFolder); presetsIds.push(restFolder.id); const details = window.document.createElement("details"); const folderOpenLocalStorageKey = `${folderId}-open`; details.open = !!localStorage.getItem(folderOpenLocalStorageKey); detailsList.push(details); li.appendChild(details); const summary = window.document.createElement("summary"); summary.id = folderId; const spanMarker = window.document.createElement("span"); spanMarker.classList.add("marker"); spanMarker.innerHTML = "●"; summary.append(spanMarker); const spanName = window.document.createElement("span"); spanName.classList.add("name"); spanName.innerHTML = anyPartial.name; summary.append(spanName); const smallCount = window.document.createElement("small"); smallCount.hidden = details.open; summary.append(smallCount); details.appendChild(summary); const thisPath = { name: anyPartial.name, id: restFolder.id, }; details.addEventListener("toggle", () => { const open = details.open; smallCount.hidden = open; if (open) { spanMarker.innerHTML = "○"; localStorage.setItem(folderOpenLocalStorageKey, "1"); } else { spanMarker.innerHTML = "●"; localStorage.removeItem(folderOpenLocalStorageKey); } }); const childPresetsCount = processPartialTree(anyPartial.tree, details, [ ...(path || []), thisPath, ]); listForSum.push(childPresetsCount); createEffect(() => { const count = childPresetsCount(); smallCount.innerHTML = count.toLocaleString(); if (!count) { li.hidden = true; } else { li.hidden = false; } }); } else { const id = `${anyPartial.scale}-to-${stringToId(anyPartial.title)}`; /** @type {Omit} */ const restPreset = { id, path: path || [], serializedPath: `/ ${[ ...(path || []).map(({ name }) => name), anyPartial.name, ].join(" / ")}`, isFavorite: createSignal(false), visited: createSignal(false), }; Object.assign(anyPartial, restPreset); const preset = /** @type {Preset} */ (anyPartial); if (!selected() && (firstTime || savedSelectedId === preset.id)) { selected.set(preset); } preset.isFavorite.set( !!localStorage.getItem(presetToFavoriteLocalStorageKey(preset)) ); preset.visited.set( !!localStorage.getItem(presetToVisitedLocalStorageKey(preset)) ); createCountersEffects(preset); presetsList.push(preset); presetsIds.push(preset.id); const label = createPresetLabel({ preset, frame: "folders", }); li.append(label); const inDom = createSignal(true); function createDomEffect() { createEffect(() => { switch (filter()) { case "all": { if (!inDom()) { insertElementAtIndex(ul, li, partialIndex); inDom.set(true); } break; } case "favorites": { if (preset.isFavorite()) { if (!inDom()) { insertElementAtIndex(ul, li, partialIndex); inDom.set(true); } } else if (inDom()) { inDom.set(false); ul.removeChild(li); } break; } case "new": { if (!preset.visited()) { if (!inDom()) { insertElementAtIndex(ul, li, partialIndex); inDom.set(true); } } else if (inDom()) { inDom.set(false); ul.removeChild(li); } break; } } }); } createDomEffect(); const memo = createMemo(() => (inDom() ? 1 : 0)); listForSum.push(memo); } }); return createMemo(() => listForSum.reduce((acc, s) => acc + s(), 0)); } const tree = window.document.createElement("div"); tree.classList.add("tree"); foldersFrame.append(tree); const allCount = processPartialTree(partialTree, tree); function checkUniqueIds() { if (presetsIds.length !== new Set(presetsIds).size) { /** @type {Map} */ const m = new Map(); presetsIds.forEach((id) => { m.set(id, (m.get(id) || 0) + 1); }); console.log( [...m.entries()] .filter(([_, value]) => value > 1) .map(([key, _]) => key) ); throw Error("ID duplicate"); } } checkUniqueIds(); function createCountersDomUpdateEffect() { foldersFilterAllCount.innerHTML = allCount().toLocaleString(); createEffect(() => { foldersFilterFavoritesCount.innerHTML = favoritesCount().toLocaleString(); }); createEffect(() => { foldersFilterNewCount.innerHTML = newCount().toLocaleString(); }); } createCountersDomUpdateEffect(); function initFilters() { const filterAllInput = /** @type {HTMLInputElement} */ ( getElementById("folders-filter-all") ); const filterFavoritesInput = /** @type {HTMLInputElement} */ ( getElementById("folders-filter-favorites") ); const filterNewInput = /** @type {HTMLInputElement} */ ( getElementById("folders-filter-new") ); filterAllInput.addEventListener("change", () => { filter.set("all"); }); filterFavoritesInput.addEventListener("change", () => { filter.set("favorites"); }); filterNewInput.addEventListener("change", () => { filter.set("new"); }); createEffect(() => { const f = filter(); localStorage.setItem(foldersFilterLocalStorageKey, f); switch (f) { case "all": { filterAllInput.checked = true; break; } case "favorites": { filterFavoritesInput.checked = true; break; } case "new": { filterNewInput.checked = true; break; } } }); } initFilters(); function initCloseAllButton() { getElementById("button-close-all-folders").addEventListener("click", () => { detailsList.forEach((details) => (details.open = false)); }); } initCloseAllButton(); function goToSelected() { filter.set("all"); if (!selected()) throw "Selected should be set by now"; const selectedId = selected().id; selected().path.forEach(({ id }) => { const summary = getElementById(id); const details = /** @type {HTMLDetailsElement | undefined} */ ( summary.parentElement ); if (!details) throw "Parent details should exist"; if (!details.open) { summary.click(); } }); setTimeout(() => { getElementById(`${selectedId}-folders-selector`).scrollIntoView({ behavior: "instant", block: "center", }); }, 0); } if (firstTime) { goToSelected(); } function createUpdateSelectedHeaderEffect() { createEffect(() => { const preset = selected(); presetTitle.innerHTML = preset.title; presetDescription.innerHTML = preset.serializedPath; }); } createUpdateSelectedHeaderEffect(); const LOCAL_STORAGE_TIME_RANGE_KEY = "chart-range"; const URL_PARAMS_TIME_RANGE_FROM_KEY = "from"; const URL_PARAMS_TIME_RANGE_TO_KEY = "to"; const HEIGHT_CHUNK_SIZE = 10_000; runWhenIdle(() => import("./libraries/lightweight-charts/script.js").then( ({ createChart: createClassicChart, createChartEx: createCustomChart, }) => { /** * @param {Scale} scale * @returns {string} */ function getVisibleTimeRangeLocalStorageKey(scale) { return `${LOCAL_STORAGE_TIME_RANGE_KEY}-${scale}`; } /** * @param {Scale} scale * @returns {TimeRange} */ function getInitialVisibleTimeRange(scale) { const urlParams = new URLSearchParams(window.location.search); const urlFrom = urlParams.get(URL_PARAMS_TIME_RANGE_FROM_KEY); const urlTo = urlParams.get(URL_PARAMS_TIME_RANGE_TO_KEY); if (urlFrom && urlTo) { if ( scale === "date" && urlFrom.includes("-") && urlTo.includes("-") ) { console.log({ from: new Date(urlFrom).toJSON().split("T")[0], to: new Date(urlTo).toJSON().split("T")[0], }); return { from: new Date(urlFrom).toJSON().split("T")[0], to: new Date(urlTo).toJSON().split("T")[0], }; } else if ( scale === "height" && (!urlFrom.includes("-") || !urlTo.includes("-")) ) { console.log({ from: Number(urlFrom), to: Number(urlTo), }); return { from: Number(urlFrom), to: Number(urlTo), }; } } function getSavedTimeRange() { return /** @type {TimeRange | null} */ ( JSON.parse( localStorage.getItem( getVisibleTimeRangeLocalStorageKey(scale) ) || "null" ) ); } const savedTimeRange = getSavedTimeRange(); console.log(savedTimeRange); if (savedTimeRange) { return savedTimeRange; } function getDefaultTimeRange() { switch (scale) { case "date": { const defaultTo = new Date(); const defaultFrom = new Date(); defaultFrom.setDate(defaultFrom.getUTCDate() - 6 * 30); return { from: defaultFrom.toJSON().split("T")[0], to: defaultTo.toJSON().split("T")[0], }; } case "height": { return { from: 850_000, to: 900_000, }; } } } return getDefaultTimeRange(); } /** * @param {IChartApi} chart */ function setInitialVisibleTimeRange(chart) { const range = visibleTimeRange(); if (range) { chart.timeScale().setVisibleRange(/** @type {any} */ (range)); // On small screen it doesn't it might not set it in time setTimeout(() => { try { chart.timeScale().setVisibleRange(/** @type {any} */ (range)); } catch {} }, 50); } } const scale = createMemo(() => selected().scale); const activeDatasets = createSignal( /** @type {Set>} */ (new Set()), { equals: false, } ); const visibleTimeRange = createSignal( getInitialVisibleTimeRange(scale()) ); const visibleDatasetIds = createSignal(/** @type {number[]} */ ([]), { equals: false, }); const lastVisibleDatasetIndex = createMemo(() => { const last = visibleDatasetIds().at(-1); return last !== undefined ? chunkIdToIndex(scale(), last) : undefined; }); const priceSeriesType = createSignal( /** @type {PriceSeriesType} */ ("Candlestick") ); function updateVisibleDatasetIds() { /** @type {number[]} */ let ids = []; const today = new Date(); const { from: rawFrom, to: rawTo } = visibleTimeRange(); if (typeof rawFrom === "string" && typeof rawTo === "string") { const from = new Date(rawFrom).getUTCFullYear(); const to = new Date(rawTo).getUTCFullYear(); ids = Array.from( { length: to - from + 1 }, (_, i) => i + from ).filter((year) => year >= 2009 && year <= today.getUTCFullYear()); } else { const from = Math.floor(Number(rawFrom) / HEIGHT_CHUNK_SIZE); const to = Math.floor(Number(rawTo) / HEIGHT_CHUNK_SIZE); const length = to - from + 1; ids = Array.from( { length }, (_, i) => (from + i) * HEIGHT_CHUNK_SIZE ); } const old = visibleDatasetIds(); if ( old.length !== ids.length || old.at(0) !== ids.at(0) || old.at(-1) !== ids.at(-1) ) { console.log("range:", ids); visibleDatasetIds.set(ids); } } updateVisibleDatasetIds(); const debouncedUpdateVisibleDatasetIds = debounce( updateVisibleDatasetIds, 100 ); function saveVisibleRange() { const range = visibleTimeRange(); urlParamsHelpers.write( URL_PARAMS_TIME_RANGE_FROM_KEY, String(range.from) ); urlParamsHelpers.write( URL_PARAMS_TIME_RANGE_TO_KEY, String(range.to) ); localStorage.setItem( getVisibleTimeRangeLocalStorageKey(scale()), JSON.stringify(range) ); } const debouncedSaveVisibleRange = debounce(saveVisibleRange, 250); function createFetchChunksOfVisibleDatasetsEffect() { createEffect(() => { const ids = visibleDatasetIds(); const datasets = Array.from(activeDatasets()); if (ids.length === 0 || datasets.length === 0) return; untrack(() => { console.log(ids, datasets); 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 {number} value */ function numberToShortUSLocale(value) { const absoluteValue = Math.abs(value); // value = absoluteValue; if (isNaN(value)) { return ""; // } else if (value === 0) { // return "0"; } else if (absoluteValue < 10) { return numberToUSLocale(value, 3); } else if (absoluteValue < 100) { return numberToUSLocale(value, 2); } else if (absoluteValue < 1_000) { return numberToUSLocale(value, 1); } else if (absoluteValue < 100_000) { return numberToUSLocale(value, 0); } else if (absoluteValue < 1_000_000) { return `${numberToUSLocale(value / 1_000, 1)}K`; } else if (absoluteValue >= 1_000_000_000_000_000_000) { return "Inf."; } const log = Math.floor(Math.log10(absoluteValue) - 6); const suffices = ["M", "B", "T", "Q"]; const letterIndex = Math.floor(log / 3); const letter = suffices[letterIndex]; const modulused = log % 3; if (modulused === 0) { return `${numberToUSLocale( value / (1_000_000 * 1_000 ** letterIndex), 3 )}${letter}`; } else if (modulused === 1) { return `${numberToUSLocale( value / (1_000_000 * 1_000 ** letterIndex), 2 )}${letter}`; } else { return `${numberToUSLocale( value / (1_000_000 * 1_000 ** letterIndex), 1 )}${letter}`; } } /** * @param {number} value * @param {number} [digits] * @param {Intl.NumberFormatOptions} [options] */ function numberToUSLocale(value, digits, options) { return value.toLocaleString("en-us", { ...options, minimumFractionDigits: digits, maximumFractionDigits: digits, }); } /** * @class * @implements {IHorzScaleBehavior} */ class HorzScaleBehaviorHeight { options() { return /** @type {any} */ (undefined); } setOptions() {} preprocessData() {} updateFormatter() {} createConverterToInternalObj() { /** @type {(p: any) => any} */ return (price) => price; } /** @param {any} item */ key(item) { return item; } /** @param {any} item */ cacheKey(item) { return item; } /** @param {any} item */ convertHorzItemToInternal(item) { return item; } /** @param {any} item */ formatHorzItem(item) { return item; } /** @param {any} tickMark */ formatTickmark(tickMark) { return tickMark.time.toLocaleString("en-us"); } /** @param {any} tickMarks */ maxTickMarkWeight(tickMarks) { return tickMarks.reduce(this.getMarkWithGreaterWeight, tickMarks[0]) .weight; } /** * @param {any} sortedTimePoints * @param {number} startIndex */ fillWeightsForPoints(sortedTimePoints, startIndex) { for ( let index = startIndex; index < sortedTimePoints.length; ++index ) { sortedTimePoints[index].timeWeight = this.computeHeightWeight( sortedTimePoints[index].time ); } } /** * @param {any} a * @param {any} b */ getMarkWithGreaterWeight(a, b) { return a.weight > b.weight ? a : b; } /** @param {number} value */ computeHeightWeight(value) { // if (value === Math.ceil(value / 1000000) * 1000000) { // return 12; // } if (value === Math.ceil(value / 100000) * 100000) { return 11; } if (value === Math.ceil(value / 10000) * 10000) { return 10; } if (value === Math.ceil(value / 1000) * 1000) { return 9; } if (value === Math.ceil(value / 100) * 100) { return 8; } if (value === Math.ceil(value / 50) * 50) { return 7; } if (value === Math.ceil(value / 25) * 25) { return 6; } if (value === Math.ceil(value / 10) * 10) { return 5; } if (value === Math.ceil(value / 5) * 5) { return 4; } if (value === Math.ceil(value)) { return 3; } if (value * 2 === Math.ceil(value * 2)) { return 1; } return 0; } } /** @arg {{scale: Scale, element: HTMLElement}} args */ function createChart({ scale, element }) { /** @satisfies {DeepPartial} */ const options = { autoSize: true, layout: { fontFamily: "Satoshi Chart", // fontSize: 13, background: { color: "transparent" }, attributionLogo: false, }, grid: { vertLines: { visible: false }, horzLines: { visible: false }, }, timeScale: { minBarSpacing: 0.05, shiftVisibleRangeOnNewBar: false, allowShiftVisibleRangeOnWhitespaceReplacement: false, }, handleScale: { axisDoubleClickReset: { time: false, }, }, crosshair: { mode: 0, }, localization: { priceFormatter: numberToShortUSLocale, locale: "en-us", }, }; /** @type {IChartApi} */ let chart; if (scale === "date") { chart = createClassicChart(element, options); } else { const horzScaleBehavior = new HorzScaleBehaviorHeight(); // @ts-ignore chart = createCustomChart(element, horzScaleBehavior, options); } chart.priceScale("right").applyOptions({ scaleMargins: { top: 0.075, bottom: 0.05, }, minimumWidth: 78, }); createEffect(() => { const { white, darkWhite } = colors; const color = white(); chart.applyOptions({ layout: { textColor: darkWhite(), }, rightPriceScale: { borderVisible: false, }, timeScale: { borderVisible: false, }, crosshair: { horzLine: { color: color, labelBackgroundColor: color, }, vertLine: { color: color, labelBackgroundColor: color, }, }, }); }); return chart; } function resetChartListElement() { while ( chartListElement.lastElementChild?.classList.contains( "chart-wrapper" ) ) { chartListElement.lastElementChild?.remove(); } } function initWhitespace() { const whitespaceStartDate = new Date("1970-01-01"); const whitespaceStartDateYear = whitespaceStartDate.getUTCFullYear(); const whitespaceStartDateMonth = whitespaceStartDate.getUTCMonth(); const whitespaceStartDateDate = whitespaceStartDate.getUTCDate(); const whitespaceEndDate = new Date("2141-01-01"); const whitespaceDateDataset = /** @type {(WhitespaceData | SingleValueData)[]} */ ( new Array( getNumberOfDaysBetweenTwoDates( whitespaceStartDate, whitespaceEndDate ) ) ); // Hack to be able to scroll freely // Setting them all to NaN is much slower for (let i = 0; i < whitespaceDateDataset.length; i++) { const date = new Date( whitespaceStartDateYear, whitespaceStartDateMonth, whitespaceStartDateDate + i ); const time = dateToString(date); if (i === whitespaceDateDataset.length - 1) { whitespaceDateDataset[i] = { time, value: NaN, }; } else { whitespaceDateDataset[i] = { time, }; } } const heightStart = -50_000; const whitespaceHeightDataset = /** @type {WhitespaceData[]} */ ( new Array((new Date().getUTCFullYear() - 2009 + 1) * 60_000) ); for (let i = 0; i < whitespaceHeightDataset.length; i++) { const height = heightStart + i; whitespaceHeightDataset[i] = { time: /** @type {Time} */ (height), }; } /** * @param {IChartApi} chart * @param {Scale} scale * @returns {ISeriesApi<'Line'>} */ function setWhitespace(chart, scale) { const whitespace = chart.addLineSeries(); if (scale === "date") { whitespace.setData(whitespaceDateDataset); } else { whitespace.setData(whitespaceHeightDataset); const time = whitespaceHeightDataset.length; whitespace.update({ time: /** @type {Time} */ (time), value: NaN, }); } return whitespace; } return { setWhitespace }; } const { setWhitespace } = initWhitespace(); /** * @param {HTMLElement} parent * @param {number} chartIndex * @param {Preset} preset */ function createChartDiv(parent, chartIndex, preset) { 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 unitField = window.document.createElement("fieldset"); const unitName = window.document.createElement("span"); unitName.innerHTML = chartIndex ? preset.unit : /** @satisfies {Unit} */ ("US Dollars"); unitField.append(unitName); const unitDash = window.document.createElement("span"); unitDash.innerHTML = "—"; chartWrapper.append(chartDiv); return chartDiv; } /** * @param {IChartApi} chart */ function subscribeVisibleTimeRangeChange(chart) { chart.timeScale().subscribeVisibleTimeRangeChange((range) => { if (!range) return; visibleTimeRange.set(range); debouncedUpdateVisibleDatasetIds(); debouncedSaveVisibleRange(); }); } /** * @param {{ chart: IChartApi; visibleLogicalRange?: LogicalRange; visibleTimeRange?: TimeRange }} args */ function updateVisiblePriceSeriesType({ chart, visibleLogicalRange, visibleTimeRange, }) { try { const width = chart.timeScale().width(); /** @type {number} */ let ratio; if (visibleLogicalRange) { ratio = (visibleLogicalRange.to - visibleLogicalRange.from) / width; } else if (visibleTimeRange) { if (scale() === "date") { const to = /** @type {Time} */ (visibleTimeRange.to); const from = /** @type {Time} */ (visibleTimeRange.from); ratio = getNumberOfDaysBetweenTwoDates( dateFromTime(from), dateFromTime(to) ) / width; } else { const to = /** @type {number} */ (visibleTimeRange.to); const from = /** @type {number} */ (visibleTimeRange.from); ratio = (to - from) / width; } } else { throw Error(); } if (ratio <= 0.5) { priceSeriesType.set("Candlestick"); } else { priceSeriesType.set("Line"); } } catch {} } /** @param {Time} time */ function dateFromTime(time) { return typeof time === "string" ? new Date(time) : // @ts-ignore new Date(time.year, time.month, time.day); } const debouncedUpdateVisiblePriceSeriesType = debounce( updateVisiblePriceSeriesType, 50 ); /** * @param {Scale} scale * @param {number} id */ function chunkIdToIndex(scale, id) { return scale === "date" ? id - 2009 : Math.floor(id / HEIGHT_CHUNK_SIZE); } function createDatasets() { /** @type {Map>} */ const date = new Map(); /** @type {Map>} */ const height = new Map(); const USE_LOCAL_URL = true; const LOCAL_URL = "/api"; const WEB_URL = "https://kibo.money/api"; const BACKUP_WEB_URL = "https://backup.kibo.money/api"; const datasetsOwner = getOwner(); /** * @template {Scale} S * @template {number | OHLC} [T=number] * @param {S} scale * @param {string} path */ function createResourceDataset(scale, path) { return /** @type {ResourceDataset} */ ( runWithOwner(datasetsOwner, () => { /** @typedef {DatasetValue} Value */ const baseURL = `${ USE_LOCAL_URL && location.hostname === "localhost" ? LOCAL_URL : WEB_URL }/${path}`; const backupURL = `${ USE_LOCAL_URL && location.hostname === "localhost" ? LOCAL_URL : BACKUP_WEB_URL }/${path}`; const fetchedJSONs = new Array( (new Date().getFullYear() - new Date("2009-01-01").getFullYear() + 2) * (scale === "date" ? 1 : 6) ) .fill(null) .map(() => { const json = createSignal( /** @type {FetchedJSON | null} */ (null) ); /** @type {FetchedResult} */ const fetchedResult = { at: null, json, loading: false, vec: createMemo(() => { const map = json()?.dataset.map; if (!map) { return null; } const chunkId = json()?.chunk.id; if (chunkId === undefined) { throw `ChunkId ${chunkId} is undefined`; } if (Array.isArray(map)) { const values = new Array(map.length); for (let i = 0; i < map.length; i++) { const value = map[i]; values[i] = /** @type {Value} */ ({ time: /** @type {Time} */ (chunkId + i), ...(typeof value !== "number" && value !== null ? { .../** @type {OHLC} */ (value), value: value.close, } : { value: value === null ? NaN : /** @type {number} */ (value), }), }); } return values; } else { return Object.entries(map).map( ([date, value]) => /** @type {Value} */ ({ time: date, ...(typeof value !== "number" && value !== null ? { .../** @type {OHLC} */ (value), value: value.close, } : { value: value === null ? NaN : /** @type {number} */ (value), }), }) ); } }), }; return fetchedResult; }); /** * @param {number} id */ async function _fetch(id) { const index = chunkIdToIndex(scale, id); if ( index < 0 || (scale === "date" && id > new Date().getUTCFullYear()) || (scale === "height" && id > 165 * 365 * (new Date().getUTCFullYear() - 2009)) ) { return; } const fetched = fetchedJSONs.at(index); if (scale === "height" && index > 0) { const length = fetchedJSONs.at(index - 1)?.vec()?.length; if (length !== undefined && length < HEIGHT_CHUNK_SIZE) { return; } } if (!fetched || fetched.loading) { return; } else if (fetched.at) { const diff = new Date().getTime() - fetched.at.getTime(); if ( diff < ONE_MINUTE_IN_MS || (index < fetchedJSONs.findLastIndex((json) => json.at) && diff < ONE_HOUR_IN_MS) ) { return; } } fetched.loading = true; /** @type {Cache | undefined} */ let cache; const urlWithQuery = `${baseURL}?chunk=${id}`; const backupUrlWithQuery = `${backupURL}?chunk=${id}`; if (!fetched.json()) { try { cache = await caches.open("resources"); const cachedResponse = await cache.match(urlWithQuery); if (cachedResponse) { /** @type {FetchedJSON | null} */ const json = await convertResponseToJSON( cachedResponse ); if (json) { console.log(`cache: ${path}?chunk=${id}`); fetched.json.set(() => json); } } } catch {} } if (!navigator.onLine) { fetched.loading = false; return; } /** @type {Response | undefined} */ let fetchedResponse; /** @type {RequestInit} */ const fetchConfig = { signal: AbortSignal.timeout(5000), }; try { fetchedResponse = await fetch(urlWithQuery, fetchConfig); if (!fetchedResponse.ok) { throw Error; } } catch { try { fetchedResponse = await fetch( backupUrlWithQuery, fetchConfig ); } catch { fetched.loading = false; return; } if (!fetchedResponse || !fetchedResponse.ok) { fetched.loading = false; return; } } const clonedResponse = fetchedResponse.clone(); /** @type {FetchedJSON | null} */ const json = await convertResponseToJSON(fetchedResponse); if (!json) { fetched.loading = false; return; } console.log(`fetch: ${path}?chunk=${id}`); const previousMap = fetched.json()?.dataset; const newMap = json.dataset.map; const previousLength = Object.keys(previousMap || []).length; const newLength = Object.keys(newMap).length; if (!newLength) { fetched.loading = false; return; } if (previousLength && previousLength === newLength) { const previousLastValue = Object.values( previousMap || [] ).at(-1); const newLastValue = Object.values(newMap).at(-1); if (newLastValue === null && previousLastValue === null) { fetched.at = new Date(); fetched.loading = false; return; } else if (typeof newLastValue === "number") { if (previousLastValue === newLastValue) { fetched.at = new Date(); fetched.loading = false; return; } } else { const previousLastOHLC = /** @type {OHLC} */ ( previousLastValue ); const newLastOHLC = /** @type {OHLC} */ (newLastValue); if ( previousLastOHLC.open === newLastOHLC.open && previousLastOHLC.high === newLastOHLC.high && previousLastOHLC.low === newLastOHLC.low && previousLastOHLC.close === newLastOHLC.close ) { fetched.loading = false; fetched.at = new Date(); return; } } } fetched.json.set(() => json); runWhenIdle(async function () { try { await cache?.put(urlWithQuery, clonedResponse); } catch (_) {} }); fetched.at = new Date(); fetched.loading = false; } /** @type {ResourceDataset} */ const resource = { scale, url: baseURL, fetch: _fetch, fetchedJSONs, // drop() { // dispose(); // fetchedJSONs.forEach((fetched) => { // fetched.at = null; // fetched.json.set(null); // }); // }, }; return resource; }) ); } /** * @template {Scale} S * @template {number | OHLC} T * @param {Response} response */ async function convertResponseToJSON(response) { try { return /** @type {FetchedJSON} */ (await response.json()); } catch (_) { return null; } } /** * @template {Scale} S * @param {S} scale * @param {DatasetPath} path * @returns {ResourceDataset} */ function getOrImport(scale, path) { if (scale === "date") { const found = date.get(/** @type {DatePath} */ (path)); if (found) return /** @type {ResourceDataset} */ (found); } else { const found = height.get(/** @type {HeightPath} */ (path)); if (found) return /** @type {ResourceDataset} */ (found); } /** @type {ResourceDataset} */ let dataset; if (path === `/${scale}-to-price`) { /** @type {ResourceDataset} */ dataset = createResourceDataset(scale, path); } else { /** @type {ResourceDataset} */ dataset = createResourceDataset(scale, path); } if (scale === "date") { date.set( /** @type {DatePath} */ (path), /** @type {any} */ (dataset) ); } else { height.set( /** @type {HeightPath} */ (path), /** @type {any} */ (dataset) ); } return dataset; } return { getOrImport, }; } const datasets = createDatasets(); /** @type {DeepPartial} */ const defaultSeriesOptions = { // @ts-ignore lineWidth: 1.5, priceLineVisible: false, baseLineVisible: false, baseLineColor: "", }; /** * @param {SpecificSeriesBlueprintWithChart} args */ function createBaseLineSeries({ chart, color, options, owner }) { const topLineColor = color || colors.profit; const bottomLineColor = color || colors.loss; function computeColors() { return { topLineColor: topLineColor(), bottomLineColor: bottomLineColor(), }; } const transparent = "transparent"; /** @type {DeepPartial} */ const seriesOptions = { priceScaleId: "right", ...defaultSeriesOptions, ...options, topFillColor1: transparent, topFillColor2: transparent, bottomFillColor1: transparent, bottomFillColor2: transparent, ...computeColors(), }; const series = chart.addBaselineSeries(seriesOptions); runWithOwner(owner, () => { createEffect(() => { series.applyOptions(computeColors()); }); }); return series; } /** * @param {SpecificSeriesBlueprintWithChart} args */ function createCandlesticksSeries({ chart, options, owner }) { function computeColors() { const upColor = colors.profit(); const downColor = colors.loss(); return { upColor, wickUpColor: upColor, downColor, wickDownColor: downColor, }; } const candlestickSeries = chart.addCandlestickSeries({ baseLineVisible: false, borderVisible: false, priceLineVisible: false, baseLineColor: "", borderColor: "", borderDownColor: "", borderUpColor: "", ...options, ...computeColors(), }); runWithOwner(owner, () => { createEffect(() => { candlestickSeries.applyOptions(computeColors()); }); }); return candlestickSeries; } /** * @param {SpecificSeriesBlueprintWithChart} args */ function createLineSeries({ chart, color, options, owner }) { function computeColors() { return { color: color(), }; } const series = chart.addLineSeries({ ...defaultSeriesOptions, ...options, ...computeColors(), }); runWithOwner(owner, () => { createEffect(() => { series.applyOptions(computeColors()); }); }); return series; } const hoveredLegend = createSignal( /** @type {{label: HTMLLabelElement, series: Series} | undefined} */ ( undefined ) ); const notHoveredLegendTransparency = "66"; /** * @param {Object} args * @param {Series} args.series * @param {Accessor} [args.disabled] * @param {string} [args.name] */ function createLegend({ series, disabled, name }) { const div = window.document.createElement("div"); createEffect(() => { div.hidden = disabled?.() ? true : false; }); legendElement.prepend(div); const { input, label, spanMain } = createLabel({ inputId: `legend-${series.title}`, inputName: `selected-${series.title}${name}`, inputValue: "value", labelTitle: "Click to toggle", name: series.title, onClick: () => { input.checked = !input.checked; series.active.set(input.checked); }, }); div.append(label); label.addEventListener("mouseover", () => { const hovered = hoveredLegend(); if (!hovered || hovered.label !== label) { hoveredLegend.set({ label, series }); } }); label.addEventListener("mouseleave", () => { hoveredLegend.set(undefined); }); createEffect(() => { input.checked = series.active(); }); function shouldHighlight() { const hovered = hoveredLegend(); return ( !hovered || (hovered.label === label && hovered.series.active()) || (hovered.label !== label && !hovered.series.active()) ); } const spanColors = window.document.createElement("span"); spanColors.classList.add("colors"); spanMain.prepend(spanColors); const colors = Array.isArray(series.color) ? series.color : [series.color]; colors.forEach((color) => { const spanColor = window.document.createElement("span"); spanColors.append(spanColor); createEffect(() => { const c = color(); if (shouldHighlight()) { spanColor.style.backgroundColor = c; } else { spanColor.style.backgroundColor = `${c}${notHoveredLegendTransparency}`; } }); }); function createHoverEffect() { const initialColors = /** @type {Record} */ ({}); const darkenedColors = /** @type {Record} */ ({}); // @ts-ignore createEffect((previouslyHovered) => { const hovered = hoveredLegend(); if (!hovered && !previouslyHovered) return hovered; const ids = visibleDatasetIds(); for (let i = 0; i < ids.length; i++) { const chunkId = ids[i]; const chunkIndex = chunkIdToIndex(scale(), chunkId); const chunk = series.chunks[chunkIndex]?.(); if (!chunk) return; if (hovered) { const seriesOptions = chunk.options(); if (!seriesOptions) return; initialColors[i] = {}; darkenedColors[i] = {}; Object.entries(seriesOptions).forEach(([k, v]) => { if (k.toLowerCase().includes("color") && v) { if (typeof v === "string" && !v.startsWith("#")) { return; } v = /** @type {string} */ (v).substring(0, 7); initialColors[i][k] = v; darkenedColors[i][ k ] = `${v}${notHoveredLegendTransparency}`; } else if (k === "lastValueVisible" && v) { initialColors[i][k] = true; darkenedColors[i][k] = false; } }); } if (shouldHighlight()) { chunk.applyOptions(initialColors[i]); } else { chunk.applyOptions(darkenedColors[i]); } } return hovered; }, undefined); } createHoverEffect(); const anchor = window.document.createElement("a"); anchor.href = series.dataset.url; anchor.innerHTML = ``; anchor.target = "_target"; anchor.rel = "noopener noreferrer"; div.append(anchor); } /** * @template {Scale} S * @param {Object} args * @param {ResourceDataset} args.dataset * @param {SeriesBlueprint} args.seriesBlueprint * @param {Preset} args.preset * @param {IChartApi} args.chart * @param {number} args.index * @param {Series[]} args.chartSeries * @param {Accessor} args.lastVisibleDatasetIndex * @param {VoidFunction} args.setMinMaxMarkersWhenIdle * @param {Accessor} [args.disabled] */ function createSeries({ chart, preset, index: seriesIndex, disabled: _disabled, lastVisibleDatasetIndex, setMinMaxMarkersWhenIdle, dataset, seriesBlueprint, chartSeries, }) { const { title, color, defaultActive, type, options } = seriesBlueprint; /** @type {Signal | undefined>[]} */ const chunks = new Array(dataset.fetchedJSONs.length); const id = stringToId(title); const storageId = presetAndSeriesToLocalStorageKey( preset, seriesBlueprint ); const active = createSignal( urlParamsHelpers.readBool(id) ?? localeStorageHelpers.readBool(storageId) ?? defaultActive ?? true ); const disabled = createMemo(_disabled || (() => false)); const visible = createMemo(() => active() && !disabled()); createEffect(() => { if (disabled()) { return; } const a = active(); if (a !== (defaultActive || true)) { urlParamsHelpers.write(id, a); localeStorageHelpers.write(storageId, a); } else { urlParamsHelpers.remove(id); localeStorageHelpers.remove(storageId); } }); /** @type {Series} */ const series = { active, chunks, color: color || [colors.profit, colors.loss], dataset, disabled, id, title, visible, }; chartSeries.push(series); const owner = getOwner(); dataset.fetchedJSONs.forEach((json, index) => { const chunk = createSignal( /** @type {ISeriesApi | undefined} */ (undefined) ); chunks[index] = chunk; createEffect(() => { const values = json.vec(); if (!values) return; if (seriesIndex > 0) { const previousSeriesChunk = chartSeries.at(seriesIndex - 1) ?.chunks[index]; const isPreviousSeriesOnChart = previousSeriesChunk?.(); if (!isPreviousSeriesOnChart) { return; } } untrack(() => { let s = chunk(); if (!s) { switch (type) { case "Baseline": { s = createBaseLineSeries({ chart, color, options, owner, }); break; } case "Candlestick": { s = createCandlesticksSeries({ chart, options, owner, }); break; } // case "Histogram": { // s = createHistogramSeries({ // chart, // options, // }); // break; // } default: case "Line": { s = createLineSeries({ chart, color, options, owner, }); break; } } // if (priceScaleOptions) { // s.priceScale().applyOptions(priceScaleOptions); // } chunk.set(s); } s.setData(values); setMinMaxMarkersWhenIdle(); }); }); createEffect(() => { const _chunk = chunk(); const currentVec = dataset.fetchedJSONs.at(index)?.vec(); const nextVec = dataset.fetchedJSONs.at(index + 1)?.vec(); if (_chunk && currentVec?.length && nextVec?.length) { _chunk.update(nextVec[0]); } }); const isChunkLastVisible = createMemo(() => { const last = lastVisibleDatasetIndex(); return last !== undefined && last === index; }); createEffect(() => { chunk()?.applyOptions({ lastValueVisible: series.visible() && isChunkLastVisible(), }); }); const shouldChunkBeVisible = createMemo(() => { if (visibleDatasetIds().length) { const start = chunkIdToIndex( scale(), /** @type {number} */ (visibleDatasetIds().at(0)) ); const end = chunkIdToIndex( scale(), /** @type {number} */ (visibleDatasetIds().at(-1)) ); if (index >= start && index <= end) { return true; } } return false; }); let wasChunkVisible = false; const chunkVisible = createMemo(() => { if (series.disabled()) { wasChunkVisible = false; } else { wasChunkVisible = wasChunkVisible || shouldChunkBeVisible(); } return wasChunkVisible; }); createEffect(() => { const visible = series.visible() && chunkVisible(); chunk()?.applyOptions({ visible, }); }); }); createLegend({ series, disabled, name: type }); return series; } /** * @param {Object} args * @param {PriceSeriesType} args.type * @param {VoidFunction} args.setMinMaxMarkersWhenIdle * @param {Preset} args.preset * @param {IChartApi} args.chart * @param {Series[]} args.chartSeries * @param {Accessor} args.lastVisibleDatasetIndex */ function createPriceSeries({ type, setMinMaxMarkersWhenIdle, preset, chart, chartSeries, lastVisibleDatasetIndex, }) { const s = scale(); /** @type {AnyDatasetPath} */ const datasetPath = `${s}-to-price`; const dataset = datasets.getOrImport(s, datasetPath); // Don't trigger reactivity by design activeDatasets().add(dataset); const title = "Price"; /** @type {SeriesBlueprint} */ let seriesBlueprint; if (type === "Candlestick") { seriesBlueprint = { datasetPath, title, type: "Candlestick", }; } else { seriesBlueprint = { datasetPath, title, color: colors.white, }; } const disabled = createMemo(() => priceSeriesType() !== type); const priceSeries = createSeries({ seriesBlueprint, dataset, preset, index: -1, chart, chartSeries, lastVisibleDatasetIndex, disabled, setMinMaxMarkersWhenIdle, }); // createEffect(() => { // const latest = webSockets.liveKrakenCandle.latest(); // if (!latest) return; // const index = chunkIdToIndex(s, latest.year); // const series = priceSeries.seriesList.at(index)?.(); // series?.update(latest); // }); return priceSeries; } function resetLegendElement() { legendElement.innerHTML = ""; } /** @type {Array<(IChartApi & {whitespace: ISeriesApi<"Line">})>} */ let charts = []; function applyPreset() { const preset = selected(); const scale = preset.scale; visibleTimeRange.set(getInitialVisibleTimeRange(scale)); activeDatasets.set((s) => { s.clear(); return s; }); const chartCount = 1 + (preset.bottom?.length ? 1 : 0); const blueprintCount = 1 + (preset.top?.length || 0) + (preset.bottom?.length || 0); const chartsBlueprints = [preset.top || [], preset.bottom].flatMap( (list) => (list ? [list] : []) ); resetLegendElement(); resetChartListElement(); /** @type {Series[]} */ const allSeries = []; charts = chartsBlueprints.map((seriesBlueprints, chartIndex) => { const chartDiv = createChartDiv( chartListElement, chartIndex, preset ); const chart = /** @type {IChartApi & {whitespace: ISeriesApi<"Line">}} */ ( createChart({ scale, element: chartDiv, }) ); chart.whitespace = setWhitespace(chart, scale); setInitialVisibleTimeRange(chart); /** @type {Series[]} */ const chartSeries = []; function setMinMaxMarkers() { try { const { from, to } = visibleTimeRange(); const dateFrom = new Date(String(from)); const dateTo = new Date(String(to)); /** @type {Marker | undefined} */ let max = undefined; /** @type {Marker | undefined} */ let min = undefined; const ids = visibleDatasetIds(); for (let i = 0; i < chartSeries.length; i++) { const { chunks, dataset } = chartSeries[i]; for (let j = 0; j < ids.length; j++) { const id = ids[j]; const chunkIndex = chunkIdToIndex(scale, id); const chunk = chunks.at(chunkIndex)?.(); if (!chunk || !chunk?.options().visible) continue; chunk.setMarkers([]); const isCandlestick = chunk.seriesType() === "Candlestick"; const vec = dataset.fetchedJSONs.at(chunkIndex)?.vec(); if (!vec) return; for (let k = 0; k < vec.length; k++) { const data = vec[k]; let number; if (scale === "date") { const date = dateFromTime(data.time); number = date.getTime(); if (date <= dateFrom || date >= dateTo) { continue; } } else { const height = data.time; number = /** @type {number} */ (height); if (height <= from || height >= to) { continue; } } // @ts-ignore const high = isCandlestick ? data["high"] : data.value; // @ts-ignore const low = isCandlestick ? data["low"] : data.value; if (!max || high > max.value) { max = { weight: number, time: data.time, value: high, seriesChunk: chunk, }; } if (!min || low < min.value) { min = { weight: number, time: data.time, value: low, seriesChunk: chunk, }; } } } } /** @type {(SeriesMarker