websites: default: add live price

This commit is contained in:
nym21
2025-06-12 18:10:24 +02:00
parent 37405384a2
commit 8a82bf5c50
6 changed files with 321 additions and 183 deletions

View File

@@ -1,6 +1,7 @@
// @ts-check
const keyPrefix = "chart";
const ONE_BTC_IN_SATS = 100_000_000;
/**
* @param {Object} args
@@ -34,7 +35,7 @@ export function init({
const { index, fieldset } = createIndexSelector({ signals, utils });
const TIMERANGE_LS_KEY = signals.createMemo(
() => `chart-timerange-${index()}`,
() => `chart-timerange-${index()}`
);
let firstRun = true;
@@ -89,7 +90,7 @@ export function init({
from.set(t.from);
to.set(t.to);
}
}),
})
);
elements.charts.append(fieldset);
@@ -128,6 +129,88 @@ export function init({
const seriesListTop = /** @type {Series[]} */ ([]);
const seriesListBottom = /** @type {Series[]} */ ([]);
/**
* @param {Object} params
* @param {ISeriesApi<any, number>} params.iseries
* @param {Unit} params.unit
* @param {Index} params.index
*/
function printLatest({ iseries, unit, index }) {
const _latest = webSockets.kraken1dCandle.latest();
if (!_latest) return;
const latest = { ..._latest };
if (unit === "Sats") {
latest.open = Math.floor(ONE_BTC_IN_SATS / latest.open);
latest.high = Math.floor(ONE_BTC_IN_SATS / latest.high);
latest.low = Math.floor(ONE_BTC_IN_SATS / latest.low);
latest.close = Math.floor(ONE_BTC_IN_SATS / latest.close);
latest.value = Math.floor(ONE_BTC_IN_SATS / latest.value);
}
const last_ = iseries.data().at(-1);
if (!last_) return;
const last = { ...last_ };
if ("close" in last) {
last.close = latest.close;
}
if ("value" in last) {
last.value = latest.value;
}
const date = new Date(latest.time * 1000);
switch (index) {
case /** @satisfies {Height} */ (5): {
if ("close" in last) {
last.low = Math.min(last.low, latest.close);
last.high = Math.max(last.high, latest.close);
}
iseries.update(last);
break;
}
case /** @satisfies {DateIndex} */ (0): {
iseries.update(latest);
break;
}
default: {
if (index === /** @satisfies {WeekIndex} */ (22)) {
date.setUTCDate(date.getUTCDate() - ((date.getUTCDay() + 6) % 7));
} else if (index === /** @satisfies {MonthIndex} */ (7)) {
date.setUTCDate(1);
} else if (index === /** @satisfies {QuarterIndex} */ (19)) {
const month = date.getUTCMonth();
date.setUTCMonth(month - (month % 3), 1);
} else if (index === /** @satisfies {YearIndex} */ (23)) {
date.setUTCMonth(0, 1);
} else if (index === /** @satisfies {DecadeIndex} */ (1)) {
date.setUTCFullYear(
Math.floor(date.getUTCFullYear() / 10) * 10,
0,
1
);
} else {
throw Error("Unsupported");
}
const time = date.valueOf() / 1000;
if (time === last.time) {
if ("close" in last) {
last.low = Math.min(last.low, latest.low);
last.high = Math.max(last.high, latest.high);
}
iseries.update(last);
} else {
latest.time = time;
iseries.update(latest);
}
}
}
}
signals.createEffect(selected, (option) => {
headingElement.innerHTML = option.title;
@@ -166,80 +249,92 @@ export function init({
signals.createEffect(index, (index) => {
signals.createEffect(
() => [topUnit(), topSeriesType()],
([topUnit, topSeriesType]) => {
() => ({
topUnit: topUnit(),
topSeriesType: topSeriesType(),
}),
({ topUnit, topSeriesType }) => {
/** @type {Series | undefined} */
let series;
switch (topUnit) {
case "USD": {
switch (topSeriesType) {
case "Candles": {
const series = chart.addCandlestickSeries({
series = chart.addCandlestickSeries({
vecId: "ohlc",
name: "Price",
unit: topUnit,
setDataCallback: printLatest,
order: 0,
});
seriesListTop[0]?.remove();
seriesListTop[0] = series;
break;
}
case "Line": {
const series = chart.addLineSeries({
series = chart.addLineSeries({
vecId: "close",
name: "Price",
unit: topUnit,
color: colors.default,
setDataCallback: printLatest,
options: {
priceLineVisible: true,
},
order: 0,
});
seriesListTop[0]?.remove();
seriesListTop[0] = series;
}
}
// signals.createEffect(webSockets.kraken1dCandle.latest, (latest) => {
// if (!latest) return;
// const last = /** @type { CandlestickData | undefined} */ (
// candles.data().at(-1)
// );
// if (!last) return;
// candles?.update({ ...last, close: latest.close });
// });
break;
}
case "Sats": {
switch (topSeriesType) {
case "Candles": {
const series = chart.addCandlestickSeries({
series = chart.addCandlestickSeries({
vecId: "ohlc-in-sats",
name: "Price",
unit: topUnit,
inverse: true,
setDataCallback: printLatest,
order: 0,
});
seriesListTop[0]?.remove();
seriesListTop[0] = series;
break;
}
case "Line": {
const series = chart.addLineSeries({
series = chart.addLineSeries({
vecId: "close-in-sats",
name: "Price",
unit: topUnit,
color: colors.default,
setDataCallback: printLatest,
options: {
priceLineVisible: true,
},
order: 0,
});
seriesListTop[0]?.remove();
seriesListTop[0] = series;
}
}
break;
}
}
},
if (!series) throw Error("Unreachable");
seriesListTop[0]?.remove();
seriesListTop[0] = series;
// setDataCallback insimport("./options").tead of hasData
signals.createEffect(
() => ({
latest: webSockets.kraken1dCandle.latest(),
hasData: series.hasData(),
}),
({ latest, hasData }) => {
if (!series || !latest || !hasData) return;
printLatest({ iseries: series.inner, unit: topUnit, index });
}
);
}
);
[
@@ -300,7 +395,7 @@ export function init({
blueprint.color?.() ?? blueprint.colors?.[1](),
},
order,
}),
})
);
break;
}
@@ -318,13 +413,13 @@ export function init({
paneIndex,
options: blueprint.options,
order,
}),
})
);
}
}
});
});
},
}
);
firstRun = false;
@@ -379,7 +474,7 @@ function createIndexSelector({ signals, utils }) {
case "decade":
return /** @satisfies {DecadeIndex} */ (1);
}
},
}
);
return { fieldset, index };

View File

@@ -1,7 +1,7 @@
// @ts-check
/**
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, ChartableIndex,CreatePriceLineOptions, CreatePriceLine } from "./options"
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, ChartableIndex,CreatePriceLineOptions, CreatePriceLine, SeriesType } from "./options"
* @import { Valued, SingleValueData, CandlestickData, ChartData, OHLCTuple, Series } from "../packages/lightweight-charts/wrapper"
* @import * as _ from "../packages/ufuzzy/v1.0.18/types"
* @import { createChart as CreateClassicChart, LineStyleOptions, DeepPartial, ChartOptions, IChartApi, IHorzScaleBehavior, WhitespaceData, ISeriesApi, Time, LineData, LogicalRange, BaselineStyleOptions, SeriesOptionsCommon, BaselineData, CandlestickStyleOptions } from "../packages/lightweight-charts/v5.0.7-treeshaked/types"
@@ -67,14 +67,14 @@ function initPackages() {
const imports = {
async signals() {
return import("../packages/solid-signals/wrapper.js").then((d) =>
d.default.then((d) => d),
d.default.then((d) => d)
);
},
async lightweightCharts() {
return window.document.fonts.ready.then(() =>
import("../packages/lightweight-charts/wrapper.js").then((d) =>
d.default.then((d) => d),
),
d.default.then((d) => d)
)
);
},
async leanQr() {
@@ -82,7 +82,7 @@ function initPackages() {
},
async ufuzzy() {
return import("../packages/ufuzzy/v1.0.18/script.js").then(
({ default: d }) => d,
({ default: d }) => d
);
},
};
@@ -580,7 +580,7 @@ function createUtils() {
window.history.pushState(
null,
"",
`${pathname}?${urlParams.toString()}`,
`${pathname}?${urlParams.toString()}`
);
} catch (_) {}
},
@@ -597,7 +597,7 @@ function createUtils() {
window.history.replaceState(
null,
"",
`${pathname}?${urlParams.toString()}`,
`${pathname}?${urlParams.toString()}`
);
} catch (_) {}
},
@@ -1120,8 +1120,8 @@ function createUtils() {
today.getUTCDate(),
0,
0,
0,
),
0
)
);
},
/**
@@ -1223,7 +1223,7 @@ function createUtils() {
*/
function getNumberOfDaysBetweenTwoDates(oldest, youngest) {
return Math.round(
Math.abs((youngest.getTime() - oldest.getTime()) / date.ONE_DAY_IN_MS),
Math.abs((youngest.getTime() - oldest.getTime()) / date.ONE_DAY_IN_MS)
);
}
@@ -1497,7 +1497,7 @@ function createVecsResources(signals, utils) {
const fetchedRecord = signals.createSignal(
/** @type {Map<string, {loading: boolean, at: Date | null, vec: Signal<T[] | null>}>} */ (
new Map()
),
)
);
return {
@@ -1545,7 +1545,7 @@ function createVecsResources(signals, utils) {
index,
id,
from,
to,
to
)
);
fetched.at = new Date();
@@ -1806,7 +1806,7 @@ function initWebSockets(signals, utils) {
window.document.addEventListener(
"visibilitychange",
reinitWebSocketIfDocumentNotHidden,
reinitWebSocketIfDocumentNotHidden
);
window.document.addEventListener("online", reinitWebSocket);
@@ -1815,7 +1815,7 @@ function initWebSockets(signals, utils) {
ws?.close();
window.document.removeEventListener(
"visibilitychange",
reinitWebSocketIfDocumentNotHidden,
reinitWebSocketIfDocumentNotHidden
);
window.document.removeEventListener("online", reinitWebSocket);
live.set(false);
@@ -1828,9 +1828,8 @@ function initWebSockets(signals, utils) {
/**
* @param {(candle: CandlestickData) => void} callback
* @param {number} interval
*/
function krakenCandleWebSocketCreator(callback, interval) {
function krakenCandleWebSocketCreator(callback) {
const ws = new WebSocket("wss://ws.kraken.com/v2");
ws.addEventListener("open", () => {
@@ -1842,7 +1841,7 @@ function initWebSockets(signals, utils) {
symbol: ["BTC/USD"],
interval: 1440,
},
}),
})
);
});
@@ -1853,14 +1852,10 @@ function initWebSockets(signals, utils) {
const { interval_begin, open, high, low, close } = result.data.at(-1);
const date = new Date(interval_begin);
const dateStr = utils.date.toString(date);
/** @type {CandlestickData} */
const candle = {
index: -1,
time: dateStr,
time: new Date(interval_begin).valueOf() / 1000,
open: Number(open),
high: Number(high),
low: Number(low),
@@ -1874,8 +1869,9 @@ function initWebSockets(signals, utils) {
return ws;
}
/** @type {ReturnType<typeof createWebsocket<CandlestickData>>} */
const kraken1dCandle = createWebsocket((callback) =>
krakenCandleWebSocketCreator(callback, 1440),
krakenCandleWebSocketCreator(callback)
);
kraken1dCandle.open();
@@ -1934,7 +1930,7 @@ function main() {
}
const frame = window.document.getElementById(
/** @type {string} */ (input.value),
/** @type {string} */ (input.value)
);
if (!frame) {
@@ -2032,23 +2028,23 @@ function main() {
function initDark() {
const preferredColorSchemeMatchMedia = window.matchMedia(
"(prefers-color-scheme: dark)",
"(prefers-color-scheme: dark)"
);
const dark = signals.createSignal(
preferredColorSchemeMatchMedia.matches,
preferredColorSchemeMatchMedia.matches
);
preferredColorSchemeMatchMedia.addEventListener(
"change",
({ matches }) => {
dark.set(matches);
},
}
);
return dark;
}
const dark = initDark();
const qrcode = signals.createSignal(
/** @type {string | null} */ (null),
/** @type {string | null} */ (null)
);
function createLastHeightResource() {
@@ -2059,7 +2055,7 @@ function main() {
lastHeight.set(h);
},
/** @satisfies {Height} */ (5),
"height",
"height"
);
}
fetchLastHeight();
@@ -2106,10 +2102,10 @@ function main() {
const owner = signals.getOwner();
const chartOption = signals.createSignal(
/** @type {ChartOption | null} */ (null),
/** @type {ChartOption | null} */ (null)
);
const simOption = signals.createSignal(
/** @type {SimulationOption | null} */ (null),
/** @type {SimulationOption | null} */ (null)
);
let previousElement = /** @type {HTMLElement | undefined} */ (
@@ -2156,10 +2152,10 @@ function main() {
webSockets,
vecsResources,
vecIdToIndexes,
}),
),
),
),
})
)
)
)
);
}
firstTimeLoadingChart = false;
@@ -2182,9 +2178,9 @@ function main() {
vecsResources,
option,
vecIdToIndexes,
}),
),
),
})
)
)
);
}
firstTimeLoadingTable = false;
@@ -2212,10 +2208,10 @@ function main() {
signals,
utils,
vecsResources,
}),
),
),
),
})
)
)
)
);
}
firstTimeLoadingSimulation = false;
@@ -2244,7 +2240,7 @@ function main() {
createMobileSwitchEffect();
utils.dom.onFirstIntersection(elements.aside, () =>
signals.runWithOwner(owner, initSelectedFrame),
signals.runWithOwner(owner, initSelectedFrame)
);
}
initSelected();
@@ -2322,7 +2318,7 @@ function main() {
if (indexes?.length) {
const maxIndex = Math.min(
(order || indexes).length - 1,
minIndex + RESULTS_PER_PAGE - 1,
minIndex + RESULTS_PER_PAGE - 1
);
list = Array(maxIndex - minIndex + 1);
@@ -2398,7 +2394,7 @@ function main() {
haystack,
needle,
undefined,
infoThresh,
infoThresh
);
if (!result?.[0]?.length || !result?.[1]) {
@@ -2406,7 +2402,7 @@ function main() {
haystack,
needle,
outOfOrder,
infoThresh,
infoThresh
);
}
@@ -2415,7 +2411,7 @@ function main() {
haystack,
needle,
outOfOrder,
infoThresh,
infoThresh
);
}
@@ -2424,7 +2420,7 @@ function main() {
haystack,
needle,
outOfOrder,
infoThresh,
infoThresh
);
}
@@ -2433,7 +2429,7 @@ function main() {
haystack,
needle,
undefined,
infoThresh,
infoThresh
);
}
@@ -2442,7 +2438,7 @@ function main() {
haystack,
needle,
outOfOrder,
infoThresh,
infoThresh
);
}
@@ -2525,7 +2521,7 @@ function main() {
shareDiv.hidden = false;
});
}),
})
);
}
initShare();
@@ -2547,7 +2543,7 @@ function main() {
localStorage.setItem(barWidthLocalStorageKey, String(width));
} else {
elements.main.style.width = elements.style.getPropertyValue(
"--default-main-width",
"--default-main-width"
);
localStorage.removeItem(barWidthLocalStorageKey);
}
@@ -2583,9 +2579,9 @@ function main() {
window.addEventListener("mouseleave", setResizeFalse);
}
initDesktopResizeBar();
}),
),
),
})
)
)
);
}
main();

View File

@@ -27,7 +27,7 @@
* @property {Color} [color]
* @property {[Color, Color]} [colors]
* @property {DeepPartial<BaselineStyleOptions & SeriesOptionsCommon & CreatePriceLineOptions>} [options]
* @property {Accessor<BaselineData[]>} [data]
* @property {Accessor<BaselineData<number>[]>} [data]
* @typedef {BaseSeriesBlueprint & BaselineSeriesBlueprintSpecific} BaselineSeriesBlueprint
*
* @typedef {Object} CandlestickSeriesBlueprintSpecific
@@ -41,7 +41,7 @@
* @property {"Line"} [type]
* @property {Color} [color]
* @property {DeepPartial<LineStyleOptions & SeriesOptionsCommon & CreatePriceLineOptions>} [options]
* @property {Accessor<LineData[]>} [data]
* @property {Accessor<LineData<number>[]>} [data]
* @typedef {BaseSeriesBlueprint & LineSeriesBlueprintSpecific} LineSeriesBlueprint
*
* @typedef {BaselineSeriesBlueprint | CandlestickSeriesBlueprint | LineSeriesBlueprint} AnySeriesBlueprint
@@ -1430,7 +1430,7 @@ function createPartialOptions(colors) {
key: `${fixKey(key)}realized-price`,
name,
color,
}),
})
),
}
: createPriceWithRatio({
@@ -1512,7 +1512,9 @@ function createPartialOptions(colors) {
}),
/** @satisfies {FetchedBaselineSeriesBlueprint} */ ({
type: "Baseline",
key: `${fixKey(key)}net-realized-profit-and-loss-relative-to-realized-cap`,
key: `${fixKey(
key
)}net-realized-profit-and-loss-relative-to-realized-cap`,
title: useGroupName ? name : "Net",
color: useGroupName ? color : undefined,
options: {
@@ -1581,7 +1583,9 @@ function createPartialOptions(colors) {
bottom: list.flatMap(({ color, name, key }) => [
/** @satisfies {FetchedBaselineSeriesBlueprint} */ ({
type: "Baseline",
key: `${fixKey(key)}adjusted-spent-output-profit-ratio`,
key: `${fixKey(
key
)}adjusted-spent-output-profit-ratio`,
title: useGroupName ? name : "asopr",
color: useGroupName ? color : undefined,
options: {
@@ -1603,7 +1607,7 @@ function createPartialOptions(colors) {
key: `${fixKey(key)}sell-side-risk-ratio`,
name: useGroupName ? name : "Risk",
color: color,
}),
})
),
},
],
@@ -1691,7 +1695,9 @@ function createPartialOptions(colors) {
}),
/** @satisfies {FetchedBaselineSeriesBlueprint} */ ({
type: "Baseline",
key: `${fixKey(key)}net-unrealized-profit-and-loss-relative-to-market-cap`,
key: `${fixKey(
key
)}net-unrealized-profit-and-loss-relative-to-market-cap`,
title: useGroupName ? name : "Net",
color: useGroupName ? color : undefined,
options: {
@@ -1868,7 +1874,7 @@ function createPartialOptions(colors) {
key: `${key}-sma`,
name: key,
color,
}),
})
),
},
...averages.map(({ key, name, color }) =>
@@ -1878,7 +1884,7 @@ function createPartialOptions(colors) {
title: `${name} Market Price Moving Average`,
legend: "average",
color,
}),
})
),
],
},
@@ -1969,7 +1975,7 @@ function createPartialOptions(colors) {
},
}),
],
}),
})
),
.../** @type {const} */ ([
{ name: "2 Year", key: "2y" },
@@ -2040,7 +2046,7 @@ function createPartialOptions(colors) {
},
}),
],
}),
})
),
],
},
@@ -2056,7 +2062,7 @@ function createPartialOptions(colors) {
name: `${year}`,
color,
defaultActive,
}),
})
),
},
...dcaClasses.map(
@@ -2083,7 +2089,7 @@ function createPartialOptions(colors) {
},
}),
],
}),
})
),
],
},
@@ -2144,10 +2150,10 @@ function createPartialOptions(colors) {
bottom: [
...createAverageSumCumulativeMinMaxPercentilesSeries("fee"),
...createAverageSumCumulativeMinMaxPercentilesSeries(
"fee-in-btc",
"fee-in-btc"
),
...createAverageSumCumulativeMinMaxPercentilesSeries(
"fee-in-usd",
"fee-in-usd"
),
],
},
@@ -2911,7 +2917,7 @@ export function initOptions({
const detailsList = [];
const treeElement = signals.createSignal(
/** @type {HTMLDivElement | null} */ (null),
/** @type {HTMLDivElement | null} */ (null)
);
/** @type {string[] | undefined} */
@@ -3030,7 +3036,7 @@ export function initOptions({
return null;
}
},
null,
null
);
partialTree.forEach((anyPartial, partialIndex) => {
@@ -3053,7 +3059,7 @@ export function initOptions({
if ("tree" in anyPartial) {
const folderId = utils.stringToId(
`${(path || []).join(" ")} ${anyPartial.name} folder`,
`${(path || []).join(" ")} ${anyPartial.name} folder`
);
/** @type {Omit<OptionsGroup, keyof PartialOptionsGroup>} */
@@ -3068,13 +3074,13 @@ export function initOptions({
const thisPath = groupAddons.id;
const passedDetails = signals.createSignal(
/** @type {HTMLDivElement | HTMLDetailsElement | null} */ (null),
/** @type {HTMLDivElement | HTMLDetailsElement | null} */ (null)
);
const childOptionsCount = recursiveProcessPartialTree(
anyPartial.tree,
passedDetails,
[...(path || []), thisPath],
[...(path || []), thisPath]
);
listForSum.push(childOptionsCount);
@@ -3206,7 +3212,7 @@ export function initOptions({
});
return signals.createMemo(() =>
listForSum.reduce((acc, s) => acc + s(), 0),
listForSum.reduce((acc, s) => acc + s(), 0)
);
}
recursiveProcessPartialTree(partialOptions, treeElement);
@@ -3233,7 +3239,7 @@ export function initOptions({
console.log(
[...m.entries()]
.filter(([_, value]) => value > 1)
.map(([key, _]) => key),
.map(([key, _]) => key)
);
throw Error("ID duplicate");