// @ts-check /** * @import { PriceSeriesType } from '../packages/lightweight-charts/types'; */ /** * @param {Object} args * @param {Colors} args.colors * @param {LightweightCharts} args.lightweightCharts * @param {Accessor} args.selected * @param {Signals} args.signals * @param {Utilities} args.utils * @param {Datasets} args.datasets * @param {WebSockets} args.webSockets * @param {Elements} args.elements * @param {Accessor} args.dark */ export function init({ colors, dark, datasets, elements, lightweightCharts, selected, signals, utils, webSockets, }) { console.log("init chart state"); const scale = signals.createMemo(() => selected().scale); elements.charts.append(utils.dom.createShadow("left")); elements.charts.append(utils.dom.createShadow("right")); const { headerElement, titleElement, descriptionElement } = utils.dom.createHeader({}); elements.charts.append(headerElement); signals.createEffect(selected, (option) => { titleElement.innerHTML = option.title; descriptionElement.innerHTML = option.serializedPath; }); const chart = lightweightCharts.createChart({ parent: elements.charts, signals, colors, id: "chart", scale: scale(), kind: "moveable", utils, }); const activeDatasets = signals.createSignal( /** @type {Set>} */ (new Set()), { equals: false, }, ); const priceSeriesType = signals.createSignal( /** @type {PriceSeriesType} */ ("Candlestick"), ); function createFetchChunksOfVisibleDatasetsEffect() { signals.createEffect( () => ({ ids: chart.visibleDatasetIds(), activeDatasets: activeDatasets(), }), ({ ids, activeDatasets }) => { const datasets = Array.from(activeDatasets); if (ids.length === 0 || datasets.length === 0) return; for (let i = 0; i < ids.length; i++) { const id = ids[i]; for (let j = 0; j < datasets.length; j++) { datasets[j].fetch(id); } } }, ); } createFetchChunksOfVisibleDatasetsEffect(); /** * @param {Parameters[0]} args */ function updateVisiblePriceSeriesType(args) { const ratio = chart.getTicksToWidthRatio(args); if (ratio) { if (ratio <= 0.5) { priceSeriesType.set("Candlestick"); } else { priceSeriesType.set("Line"); } } } const debouncedUpdateVisiblePriceSeriesType = utils.debounce( updateVisiblePriceSeriesType, 50, ); /** * @param {Object} args * @param {PriceSeriesType} args.type * @param {VoidFunction} args.setMinMaxMarkersWhenIdle * @param {Option} args.option * @param {ChartPane} args.chartPane */ function createPriceSeries({ type, setMinMaxMarkersWhenIdle, option, chartPane, }) { const s = scale(); /** @type {AnyDatasetPath} */ const datasetPath = `${s}-to-price`; const dataset = datasets.getOrCreate(s, datasetPath); // Don't trigger reactivity by design activeDatasets().add(dataset); const title = "BTC Price"; /** @type {SplitSeriesBlueprint} */ let blueprint; if (type === "Candlestick") { blueprint = { datasetPath, title, type: "Candlestick", }; } else { blueprint = { datasetPath, title, color: colors.default, }; } const disabled = signals.createMemo(() => priceSeriesType() !== type); const priceSeries = chartPane.createSplitSeries({ blueprint, dataset, id: option.id, index: -1, disabled, setMinMaxMarkersWhenIdle, }); function createLiveCandleUpdateEffect() { signals.createEffect(webSockets.kraken1dCandle.latest, (latest) => { if (!latest) return; const index = utils.chunkIdToIndex(s, latest.year); const series = priceSeries.chunks.at(index); if (series) { signals.createEffect(series, (series) => { series?.update(latest); }); } }); } createLiveCandleUpdateEffect(); return priceSeries; } /** * @param {ChartOption} option */ function applyChartOption(option) { const scale = option.scale; chart.visibleTimeRange.set(chart.getInitialVisibleTimeRange()); activeDatasets.set((s) => { s.clear(); return s; }); const chartCount = 1 + (option.bottom?.length ? 1 : 0); const blueprintCount = 1 + (option.top?.length || 0) + (option.bottom?.length || 0); const chartsBlueprints = [option.top || [], option.bottom].flatMap( (list) => (list ? [list] : []), ); chartsBlueprints.map((seriesBlueprints, paneIndex) => { const chartPane = chart.createPane({ paneIndex, unit: paneIndex ? option.unit : "US Dollars", whitespace: true, }); function setMinMaxMarkers() { try { const { from, to } = chart.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 = chart.visibleDatasetIds(); for (let i = 0; i < chartPane.splitSeries.length; i++) { const { chunks, dataset } = chartPane.splitSeries[i]; for (let j = 0; j < ids.length; j++) { const id = ids[j]; const chunkIndex = utils.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 = utils.date.fromTime(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