git: reset

This commit is contained in:
k
2024-06-23 17:38:53 +02:00
commit a1a576d088
375 changed files with 40952 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
export const addressCohortsBySize = [
{
key: "plankton",
name: "Plankton",
},
{
key: "shrimp",
name: "Shrimp",
},
{ key: "crab", name: "Crab" },
{ key: "fish", name: "Fish" },
{ key: "shark", name: "Shark" },
{ key: "whale", name: "Whale" },
{ key: "humpback", name: "Humpback" },
{ key: "megalodon", name: "Megalodon" },
] as const;
export const addressCohortsByType = [
{ key: "p2pk", name: "P2PK" },
{ key: "p2pkh", name: "P2PKH" },
{ key: "p2sh", name: "P2SH" },
{ key: "p2wpkh", name: "P2WPKH" },
{ key: "p2wsh", name: "P2WSH" },
{ key: "p2tr", name: "P2TR" },
] as const;
export const addressCohorts = [
...addressCohortsBySize,
...addressCohortsByType,
] as const;

View File

@@ -0,0 +1,147 @@
export const xthCohorts = [
{
key: "sth",
name: "Short Term Holders",
legend: "STH",
},
{
key: "lth",
name: "Long Term Holders",
legend: "LTH",
},
] as const;
export const upToCohorts = [
{ key: "up_to_1d", name: "Up To 1 Day", legend: "1D" },
{ key: "up_to_1w", name: "Up To 1 Week", legend: "1W" },
{ key: "up_to_1m", name: "Up To 1 Month", legend: "1M" },
{ key: "up_to_2m", name: "Up To 2 Months", legend: "2M" },
{ key: "up_to_3m", name: "Up To 3 Months", legend: "3M" },
{ key: "up_to_4m", name: "Up To 4 Months", legend: "4M" },
{ key: "up_to_5m", name: "Up To 5 Months", legend: "5M" },
{ key: "up_to_6m", name: "Up To 6 Months", legend: "6M" },
{ key: "up_to_1y", name: "Up To 1 Year", legend: "1Y" },
{ key: "up_to_2y", name: "Up To 2 Years", legend: "2Y" },
{ key: "up_to_3y", name: "Up To 3 Years", legend: "3Y" },
{ key: "up_to_5y", name: "Up To 5 Years", legend: "5Y" },
{ key: "up_to_7y", name: "Up To 7 Years", legend: "7Y" },
{ key: "up_to_10y", name: "Up To 10 Years", legend: "10Y" },
{ key: "up_to_15y", name: "Up To 15 Years", legend: "15Y" },
] as const;
export const fromXToYCohorts = [
{
key: "from_1d_to_1w",
name: "From 1 Day To 1 Week",
legend: "1D - 1W",
},
{
key: "from_1w_to_1m",
name: "From 1 Week To 1 Month",
legend: "1W - 1M",
},
{
key: "from_1m_to_3m",
name: "From 1 Month To 3 Months",
legend: "1M - 3M",
},
{
key: "from_3m_to_6m",
name: "From 3 Months To 6 Months",
legend: "3M - 6M",
},
{
key: "from_6m_to_1y",
name: "From 6 Months To 1 Year",
legend: "6M - 1Y",
},
{
key: "from_1y_to_2y",
name: "From 1 Year To 2 Years",
legend: "1Y - 2Y",
},
{
key: "from_2y_to_3y",
name: "From 2 Years To 3 Years",
legend: "2Y - 3Y",
},
{
key: "from_3y_to_5y",
name: "From 3 Years To 5 Years",
legend: "3Y - 5Y",
},
{
key: "from_5y_to_7y",
name: "From 5 Years To 7 Years",
legend: "5Y - 7Y",
},
{
key: "from_7y_to_10y",
name: "From 7 Years To 10 Years",
legend: "7Y - 10Y",
},
{
key: "from_10y_to_15y",
name: "From 10 Years To 15 Years",
legend: "10Y - 15Y",
},
] as const;
export const fromXCohorts = [
{
key: "from_1y",
name: "From 1 Year",
legend: "1Y+",
},
{
key: "from_2y",
name: "From 2 Years",
legend: "2Y+",
},
{
key: "from_4y",
name: "From 4 Years",
legend: "4Y+",
},
{
key: "from_10y",
name: "From 10 Years",
legend: "10Y+",
},
{
key: "from_15y",
name: "From 15 Years",
legend: "15Y+",
},
] as const;
export const yearCohorts = [
{ key: "year_2009", name: "2009" },
{ key: "year_2010", name: "2010" },
{ key: "year_2011", name: "2011" },
{ key: "year_2012", name: "2012" },
{ key: "year_2013", name: "2013" },
{ key: "year_2014", name: "2014" },
{ key: "year_2015", name: "2015" },
{ key: "year_2016", name: "2016" },
{ key: "year_2017", name: "2017" },
{ key: "year_2018", name: "2018" },
{ key: "year_2019", name: "2019" },
{ key: "year_2020", name: "2020" },
{ key: "year_2021", name: "2021" },
{ key: "year_2022", name: "2022" },
{ key: "year_2023", name: "2023" },
{ key: "year_2024", name: "2024" },
] as const;
export const ageCohorts = [
{
key: "",
name: "",
},
...xthCohorts,
...upToCohorts,
...fromXToYCohorts,
...fromXCohorts,
...yearCohorts,
] as const;

View File

@@ -0,0 +1,15 @@
export const averages = [
{ 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 },
] as const;

View File

@@ -0,0 +1,11 @@
export const liquidities = [
{
key: "illiquid",
name: "Illiquid",
},
{ key: "liquid", name: "Liquid" },
{
key: "highly_liquid",
name: "Highly Liquid",
},
] as const;

View File

@@ -0,0 +1,116 @@
export const percentiles = [
{
key: "median_price_paid",
name: "Median",
title: "Median Paid",
value: 50,
},
{
key: "95p_price_paid",
name: `95%`,
title: `95th Percentile Paid`,
value: 95,
},
{
key: "90p_price_paid",
name: `90%`,
title: `90th Percentile Paid`,
value: 90,
},
{
key: "85p_price_paid",
name: `85%`,
title: `85th Percentile Paid`,
value: 85,
},
{
key: "80p_price_paid",
name: `80%`,
title: `80th Percentile Paid`,
value: 80,
},
{
key: "75p_price_paid",
name: `75%`,
title: `75th Percentile Paid`,
value: 75,
},
{
key: "70p_price_paid",
name: `70%`,
title: `70th Percentile Paid`,
value: 70,
},
{
key: "65p_price_paid",
name: `65%`,
title: `65th Percentile Paid`,
value: 65,
},
{
key: "60p_price_paid",
name: `60%`,
title: `60th Percentile Paid`,
value: 60,
},
{
key: "55p_price_paid",
name: `55%`,
title: `55th Percentile Paid`,
value: 55,
},
{
key: "45p_price_paid",
name: `45%`,
title: `45th Percentile Paid`,
value: 45,
},
{
key: "40p_price_paid",
name: `40%`,
title: `40th Percentile Paid`,
value: 40,
},
{
key: "35p_price_paid",
name: `35%`,
title: `35th Percentile Paid`,
value: 35,
},
{
key: "30p_price_paid",
name: `30%`,
title: `30th Percentile Paid`,
value: 30,
},
{
key: "25p_price_paid",
name: `25%`,
title: `25th Percentile Paid`,
value: 25,
},
{
key: "20p_price_paid",
name: `20%`,
title: `20th Percentile Paid`,
value: 20,
},
{
key: "15p_price_paid",
name: `15%`,
title: `15th Percentile Paid`,
value: 15,
},
{
key: "10p_price_paid",
name: `10%`,
title: `10th Percentile Paid`,
value: 10,
},
{
key: "05p_price_paid",
name: `5%`,
title: `5th Percentile Paid`,
value: 5,
},
] as const;

View File

@@ -0,0 +1,14 @@
export const totalReturns = [
{ 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" },
] as const;
export const compoundReturns = [{ name: "4 Years", key: "4y" }] as const;

View File

@@ -0,0 +1,19 @@
type AgeCohortKey = (typeof import("./age").ageCohorts)[number]["key"];
type AddressCohortKey =
(typeof import("./address").addressCohorts)[number]["key"];
type LiquidityKey = (typeof import("./liquidities").liquidities)[number]["key"];
type AddressCohortKeySplitByLiquidity = `${LiquidityKey}_${AddressCohortKey}`;
type AnyCohortKey = AgeCohortKey | AddressCohortKey;
type AnyPossibleCohortKey = AnyCohortKey | AddressCohortKeySplitByLiquidity;
type AverageName = (typeof import("./averages").averages)[number]["key"];
type TotalReturnKey = (typeof import("./returns").totalReturns)[number]["key"];
type CompoundReturnKey =
(typeof import("./returns").compoundReturns)[number]["key"];

View File

@@ -0,0 +1,41 @@
import groupedKeysToPath from "/src/../../datasets/grouped_keys_to_url_path.json";
import { createResourceDataset } from "./resource";
export { averages } from "./consts/averages";
export function createDateDatasets({
setActiveResources,
}: {
setActiveResources: Setter<Set<ResourceDataset<any, any>>>;
}) {
type Key = keyof typeof groupedKeysToPath.date;
type ResourceData = ReturnType<typeof createResourceDataset<"date">>;
const resourceDatasets = {} as Record<Exclude<Key, "ohlc">, ResourceData>;
Object.entries(groupedKeysToPath.date).forEach(([_key, path]) => {
const key = _key as Key;
if (key !== "ohlc") {
resourceDatasets[key] = createResourceDataset<"date">({
scale: "date",
path,
setActiveResources,
});
}
});
const price = createResourceDataset<"date", OHLC>({
scale: "date",
path: "/date-to-ohlc",
setActiveResources,
});
const datasets = {
price,
...resourceDatasets,
};
return datasets;
}

View File

@@ -0,0 +1,36 @@
import groupedKeysToPath from "/src/../../datasets/grouped_keys_to_url_path.json";
import { createResourceDataset } from "./resource";
export function createHeightDatasets({
setActiveResources,
}: {
setActiveResources: Setter<Set<ResourceDataset<any, any>>>;
}) {
type Key = keyof typeof groupedKeysToPath.height;
type ResourceData = ReturnType<typeof createResourceDataset<"height">>;
const resourceDatasets = {} as Record<Exclude<Key, "ohlc">, ResourceData>;
Object.keys(groupedKeysToPath.height).forEach(([_key, path]) => {
const key = _key as Key;
if (key !== "ohlc") {
resourceDatasets[key] = createResourceDataset<"height">({
scale: "height",
path,
setActiveResources,
});
}
});
const price = createResourceDataset<"height", OHLC>({
scale: "height",
path: "/height-to-ohlc",
setActiveResources,
});
return {
...resourceDatasets,
price,
};
}

View File

@@ -0,0 +1,17 @@
import { createDateDatasets } from "./date";
import { createHeightDatasets } from "./height";
export const scales = ["date" as const, "height" as const];
export const HEIGHT_CHUNK_SIZE = 10_000;
export function createDatasets({
setActiveResources,
}: {
setActiveResources: Setter<Set<ResourceDataset<any, any>>>;
}) {
return {
date: createDateDatasets({ setActiveResources }),
height: createHeightDatasets({ setActiveResources }),
} satisfies Record<ResourceScale, any>;
}

View File

@@ -0,0 +1,246 @@
import { createLazyMemo } from "@solid-primitives/memo";
import {
ONE_DAY_IN_MS,
ONE_HOUR_IN_MS,
ONE_MINUTE_IN_MS,
} from "/src/scripts/utils/time";
import { createRWS } from "/src/solid/rws";
import { HEIGHT_CHUNK_SIZE } from ".";
export function createResourceDataset<
Scale extends ResourceScale,
Type extends OHLC | number = number,
>({
scale,
path,
setActiveResources,
}: {
scale: Scale;
path: string;
setActiveResources: Setter<Set<ResourceDataset<any, any>>>;
}) {
const baseURL = `${
location.hostname === "localhost"
? "http://localhost:3110"
: "https://api.satonomics.xyz"
}${path}`;
type Dataset = Scale extends "date"
? FetchedDateDataset<Type>
: FetchedHeightDataset<Type>;
type Value = DatasetValue<
Type extends number ? SingleValueData : CandlestickData
>;
const fetchedJSONs = new Array(
(new Date().getFullYear() - new Date("2009-01-01").getFullYear()) *
(scale === "date" ? 2 : 8),
)
.fill(null)
.map((): FetchedResult<Scale, Type> => {
const json = createRWS<FetchedJSON<Scale, Type, Dataset> | null>(null);
return {
at: null,
json,
loading: false,
vec: createMemo(() => {
const map = json()?.dataset.map || null;
const chunkId = json()?.chunk.id!;
if (!map) {
return null;
}
if (Array.isArray(map)) {
return map.map(
(value, index) =>
({
number: chunkId + index,
time: (chunkId + index) as Time,
...(typeof value !== "number" && value !== null
? { ...(value as OHLC), value: value.close }
: { value: value === null ? NaN : (value as number) }),
}) as any as Value,
);
} else {
return Object.entries(map).map(
([date, value]) =>
({
number: new Date(date).valueOf() / ONE_DAY_IN_MS,
time: date,
...(typeof value !== "number" && value !== null
? { ...(value as OHLC), value: value.close }
: { value: value === null ? NaN : (value as number) }),
}) as any as Value,
);
}
}),
};
}) as FetchedResult<Scale, Type>[];
const _fetch = async (id: number) => {
const index =
scale === "date" ? id - 2009 : Math.floor(id / HEIGHT_CHUNK_SIZE);
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 (!fetched || fetched.loading) {
return;
} else if (fetched.at) {
const diff = new Date().valueOf() - fetched.at.valueOf();
if (
diff < ONE_MINUTE_IN_MS ||
(index < fetchedJSONs.findLastIndex((json) => json.at) &&
diff < ONE_HOUR_IN_MS)
) {
return;
}
}
fetched.loading = true;
let cache: Cache | undefined;
const urlWithQuery = `${baseURL}?chunk=${id}`;
if (!fetched.json()) {
try {
cache = await caches.open("resources");
const cachedResponse = await cache.match(urlWithQuery);
if (cachedResponse) {
const json = await convertResponseToJSON<Scale, Type>(cachedResponse);
if (json) {
console.log(`cache: ${path}?chunk=${id}`);
fetched.json.set(() => json);
}
}
} catch {}
}
try {
const fetchedResponse = await fetch(urlWithQuery);
if (!fetchedResponse.ok) {
fetched.loading = false;
return;
}
const clonedResponse = fetchedResponse.clone();
const json = await convertResponseToJSON<Scale, Type>(fetchedResponse);
if (json) {
console.log(`fetch: ${path}?chunk=${id}`);
const previousMap = fetched.json()?.dataset.map;
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 (typeof newLastValue === "number") {
if (previousLastValue === newLastValue) {
fetched.at = new Date();
fetched.loading = false;
return;
}
} else {
const previousLastOHLC = previousLastValue as OHLC;
const newLastOHLC = newLastValue as OHLC;
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);
if (cache) {
cache.put(urlWithQuery, clonedResponse);
}
}
} catch {
fetched.loading = false;
return;
}
fetched.at = new Date();
fetched.loading = false;
};
const resource: ResourceDataset<Scale, Type> = {
scale,
url: baseURL,
fetch: _fetch,
fetchedJSONs,
values: createLazyMemo(() => {
setActiveResources((resources) => resources.add(resource));
onCleanup(() =>
setActiveResources((resources) => {
resources.delete(resource);
return resources;
}),
);
const flat = fetchedJSONs.flatMap((fetched) => fetched.vec() || []);
return flat;
}),
drop() {
fetchedJSONs.forEach((fetched) => {
fetched.at = null;
fetched.json.set(null);
});
},
};
return resource;
}
async function convertResponseToJSON<
Scale extends ResourceScale,
Type extends number | OHLC,
>(response: Response) {
try {
return (await response.json()) as FetchedJSON<Scale, Type>;
} catch (_) {
return null;
}
}

98
app/src/scripts/datasets/types.d.ts vendored Normal file
View File

@@ -0,0 +1,98 @@
type Datasets = ReturnType<typeof import("./index").createDatasets>;
type DateDatasets = Datasets["date"];
type HeightDatasets = Datasets["height"];
type AnyDatasets = DateDatasets | HeightDatasets;
type ResourceScale = (typeof import("./index").scales)[index];
type DatasetValue<T> = T & Numbered & Valued;
interface Dataset<
Scale extends ResourceScale,
Value extends SingleValueData | CandlestickData = SingleValueData,
> {
scale: Scale;
values: Accessor<DatasetValue<Value>[]>;
}
interface ResourceDataset<
Scale extends ResourceScale,
Type extends OHLC | number = number,
FetchedDataset extends
| FetchedDateDataset<Type>
| FetchedHeightDataset<Type> = Scale extends "date"
? FetchedDateDataset<Type>
: FetchedHeightDataset<Type>,
Value extends SingleValueData | CandlestickData = Type extends number
? SingleValueData
: CandlestickData,
> extends Dataset<Scale, Value> {
url: string;
fetch: (id: number) => void;
fetchedJSONs: FetchedResult<Scale, Type>[];
drop: VoidFunction;
}
interface FetchedResult<
Scale extends ResourceScale,
Type extends number | OHLC,
Dataset extends
| FetchedDateDataset<Type>
| FetchedHeightDataset<Type> = Scale extends "date"
? FetchedDateDataset<Type>
: FetchedHeightDataset<Type>,
Value extends DatasetValue<SingleValueData | CandlestickData> = DatasetValue<
Type extends number ? SingleValueData : CandlestickData
>,
> {
at: Date | null;
json: RWS<FetchedJSON<Scale, Type, Dataset> | null>;
vec: Accessor<Value[] | null>;
loading: boolean;
}
interface FetchedJSON<
Scale extends ResourceScale,
Type extends number | OHLC,
Dataset extends
| FetchedDateDataset<Type>
| FetchedHeightDataset<Type> = Scale extends "date"
? FetchedDateDataset<Type>
: FetchedHeightDataset<Type>,
> {
source: FetchedSource;
chunk: FetchedChunk;
dataset: FetchedDataset<Scale, Type, Dataset>;
}
type FetchedSource = string;
interface FetchedChunk {
id: number;
previous: string | null;
next: string | null;
}
interface FetchedDataset<
Scale extends ResourceScale,
Type extends number | OHLC,
Dataset extends
| FetchedDateDataset<Type>
| FetchedHeightDataset<Type> = Scale extends "date"
? FetchedDateDataset<Type>
: FetchedHeightDataset<Type>,
> {
version: number;
map: Dataset;
}
type FetchedDateDataset<T> = Record<string, T>;
type FetchedHeightDataset<T> = T[];
interface OHLC {
open: number;
high: number;
low: number;
close: number;
}

View File

@@ -0,0 +1,11 @@
import { chartState } from "./state";
export function cleanChart() {
console.log("chart: clean");
try {
chartState.chart?.remove();
} catch {}
chartState.chart = null;
}

View File

@@ -0,0 +1,68 @@
import {
createChart as createClassicChart,
createChartEx as createCustomChart,
CrosshairMode,
} from "lightweight-charts";
import { colors } from "../../utils/colors";
import { priceToUSLocale } from "../../utils/locale";
import { cleanChart } from "./clean";
import { HorzScaleBehaviorHeight } from "./horzScaleBehavior";
import { chartState } from "./state";
export function createChart(scale: ResourceScale) {
cleanChart();
console.log(`chart: create (scale: ${scale})`);
const { white } = colors;
const options: DeepPartialChartOptions = {
autoSize: true,
layout: {
fontFamily: "Lexend",
background: { color: "transparent" },
fontSize: 14,
textColor: white,
},
grid: {
vertLines: { visible: false },
horzLines: { visible: false },
},
leftPriceScale: {
// borderColor: white,
},
rightPriceScale: {
// borderColor: white,
},
timeScale: {
minBarSpacing: scale === "date" ? 0.05 : 0.005,
shiftVisibleRangeOnNewBar: false,
allowShiftVisibleRangeOnWhitespaceReplacement: false,
},
crosshair: {
mode: CrosshairMode.Normal,
horzLine: {
color: white,
labelBackgroundColor: white,
},
vertLine: {
color: white,
labelBackgroundColor: white,
},
},
localization: {
priceFormatter: priceToUSLocale,
locale: "en-us",
},
};
if (scale === "date") {
chartState.chart = createClassicChart("chart", options);
} else {
const horzScaleBehavior = new HorzScaleBehaviorHeight();
// @ts-ignore
chartState.chart = createCustomChart("chart", horzScaleBehavior, options);
}
}

View File

@@ -0,0 +1,89 @@
// @ts-nocheck
// https://github.com/tradingview/lightweight-charts/blob/master/tests/e2e/graphics/test-cases/horizontal-price-scale.js
import { type IHorzScaleBehavior } from "lightweight-charts";
export class HorzScaleBehaviorHeight implements IHorzScaleBehavior<number> {
options() {}
setOptions() {}
preprocessData() {}
updateFormatter() {}
createConverterToInternalObj() {
return (price) => price;
}
key(item) {
return item;
}
cacheKey(item) {
return item;
}
convertHorzItemToInternal(item) {
return item;
}
formatHorzItem(item) {
return item;
}
formatTickmark(tickMark) {
return tickMark.time.toLocaleString("en-us");
}
maxTickMarkWeight(tickMarks) {
return tickMarks.reduce(markWithGreaterWeight, tickMarks[0]).weight;
}
fillWeightsForPoints(sortedTimePoints, startIndex) {
for (let index = startIndex; index < sortedTimePoints.length; ++index) {
sortedTimePoints[index].timeWeight = computeWeight(
sortedTimePoints[index].time,
);
}
}
}
function markWithGreaterWeight(a, b) {
return a.weight > b.weight ? a : b;
}
function computeWeight(value: number) {
// 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;
}

View File

@@ -0,0 +1,123 @@
import { colors } from "/src/scripts/utils/colors";
import { priceToUSLocale } from "/src/scripts/utils/locale";
import { ONE_DAY_IN_MS } from "/src/scripts/utils/time";
import { chartState } from "./state";
import { GENESIS_DAY } from "./whitespace";
export const setMinMaxMarkers = ({
scale,
candlesticks,
range,
lowerOpacity,
}: {
scale: ResourceScale;
candlesticks: DatasetValue<CandlestickData | SingleValueData>[];
range: TimeRange;
lowerOpacity: boolean;
}) => {
const first = candlesticks.at(0);
if (!first) return;
const offset =
scale === "date"
? first.number - new Date(GENESIS_DAY).valueOf() / ONE_DAY_IN_MS
: 0;
const slicedDataList = range
? candlesticks.slice(
Math.ceil(range.from - offset < 0 ? 0 : range.from - offset),
Math.floor(range.to - offset) + 1,
)
: [];
const series = chartState.priceSeries;
if (!series) return;
if (slicedDataList.length) {
const markers: (SeriesMarker<Time> & Numbered)[] = [];
const seriesIsCandlestick = series.seriesType() === "Candlestick";
[
{
mathFunction: "min" as const,
placementAttribute: seriesIsCandlestick
? ("low" as const)
: ("close" as const),
// valueAttribute: 'low' as const,
markerOptions: {
position: "belowBar" as const,
shape: "arrowUp" as const,
},
},
{
mathFunction: "max" as const,
placementAttribute: seriesIsCandlestick
? ("high" as const)
: ("close" as const),
// valueAttribute: 'high' as const,
markerOptions: {
position: "aboveBar" as const,
shape: "arrowDown" as const,
},
},
].map(
({
mathFunction,
placementAttribute,
// valueAttribute,
markerOptions,
}) => {
const value = Math[mathFunction](
// ...slicedDataList.map((data) => data[valueAttribute] || 0),
...slicedDataList.map(
(data) =>
(placementAttribute in data
? data[placementAttribute]
: data.value) || 0,
),
);
const placement = Math[mathFunction](
...slicedDataList.map(
(data) =>
(placementAttribute in data
? data[placementAttribute]
: data.value) || 0,
),
);
const candle = slicedDataList.find(
(data) =>
(placementAttribute in data
? data[placementAttribute]
: data.value) === placement,
);
return (
candle &&
markers.push({
...markerOptions,
// date: candle.date,
number: candle.number,
time: candle.time,
color: lowerOpacity ? colors.darkWhite : colors.white,
size: 0,
text: priceToUSLocale(value),
})
);
},
);
series.setMarkers(sortWhitespaceDataArray(markers));
}
};
function sortWhitespaceDataArray<T extends WhitespaceData & Numbered>(
array: T[],
) {
return array.sort(({ number: a }, { number: b }) => a - b);
}

View File

@@ -0,0 +1,176 @@
import { createRWS } from "/src/solid/rws";
import { colors } from "../../utils/colors";
import { getNumberOfDaysBetweenTwoDates } from "../../utils/date";
import { debounce } from "../../utils/debounce";
import { webSockets } from "../../ws";
import { createCandlesticksSeries } from "../series/creators/candlesticks";
import { createSeriesLegend } from "../series/creators/legend";
import { createLineSeries } from "../series/creators/line";
import { setMinMaxMarkers } from "./markers";
import { chartState } from "./state";
import { initTimeScale } from "./time";
export const PRICE_SCALE_MOMENTUM_ID = "momentum";
export const applyPriceSeries = <
Scale extends ResourceScale,
T extends SingleValueData,
>({
chart,
datasets,
preset,
dataset: _dataset,
options,
activeResources,
}: {
chart: IChartApi;
datasets: Datasets;
preset: Preset;
activeResources: Accessor<Set<ResourceDataset<any, any>>>;
dataset?: Dataset<Scale, T>;
options?: PriceSeriesOptions;
}) => {
const id = options?.id || "price";
const title = options?.title || "Price";
const dataset = createMemo(() => _dataset || datasets[preset.scale].price);
const url = "url" in dataset() ? (dataset() as any).url : undefined;
const priceScaleOptions: DeepPartial<PriceScaleOptions> = {
...(options?.halved
? {
scaleMargins: {
top: 0.05,
bottom: 0.55,
},
}
: {}),
...(options?.id || options?.title
? {}
: {
mode: 1,
// mode: PriceScaleMode.Logarithmic,
}),
...options?.priceScaleOptions,
};
const seriesType = createRWS(
checkIfUpClose(chart, chartState.range) || "Candlestick",
);
const debouncedCallback = debounce((range: TimeRange | null) => {
try {
seriesType.set((previous) => checkIfUpClose(chart, range) || previous);
} catch {}
}, 50);
chart?.timeScale().subscribeVisibleTimeRangeChange(debouncedCallback);
onCleanup(
() =>
chart === chartState.chart &&
chartState.chart
?.timeScale()
.unsubscribeVisibleTimeRangeChange(debouncedCallback),
);
const lowerOpacity = options?.lowerOpacity || options?.halved || false;
if (options?.halved) {
options.seriesOptions = {
...options.seriesOptions,
priceScaleId: "left",
};
}
const [ohlcSeries, ohlcColors] = createCandlesticksSeries(chart, {
...options,
lowerOpacity,
});
const ohlcLegend = createSeriesLegend({
id,
presetId: preset.id,
title,
color: () => ohlcColors,
series: ohlcSeries,
disabled: () => seriesType() !== "Candlestick",
url,
});
ohlcSeries.priceScale().applyOptions(priceScaleOptions);
// ---
const lineColor = lowerOpacity ? colors.darkWhite : colors.white;
const lineSeries = createLineSeries(chart, {
color: lineColor,
...options?.seriesOptions,
});
const lineLegend = createSeriesLegend({
id,
presetId: preset.id,
title,
color: () => lineColor,
series: lineSeries,
disabled: () => seriesType() !== "Line",
visible: ohlcLegend.visible,
url,
});
lineSeries.priceScale().applyOptions(priceScaleOptions);
// ---
// setMinMaxMarkers({
// scale: preset.scale,
// candlesticks:
// dataset?.values() || datasets[preset.scale].price.values() || ([] as any),
// range: chartState.range,
// lowerOpacity,
// });
initTimeScale({
activeResources,
});
createEffect(() => {
const d = dataset();
lineSeries.setData(d.values());
ohlcSeries.setData(d.values());
});
createEffect(() => {
if (preset.scale === "date") {
const latest = webSockets.liveKrakenCandle.latest();
if (latest) {
ohlcSeries.update(latest);
lineSeries.update(latest);
}
}
});
return { ohlcLegend, lineLegend };
};
function checkIfUpClose(chart: IChartApi, range?: TimeRange | null) {
if (!range) return undefined;
const from = new Date(range.from);
const to = new Date(range.to);
const width = chart.timeScale().width();
const difference = getNumberOfDaysBetweenTwoDates(from, to);
return width / difference >= 2.05
? "Candlestick"
: width / difference <= 1.95
? "Line"
: undefined;
}

View File

@@ -0,0 +1,40 @@
import { createChart } from "./create";
import { chartState } from "./state";
import { setWhitespace } from "./whitespace";
export function renderChart({
datasets,
legendSetter,
preset,
activeResources,
}: {
datasets: Datasets;
legendSetter: Setter<PresetLegend>;
preset: Preset;
activeResources: Accessor<Set<ResourceDataset<any, any>>>;
}) {
const scale = preset.scale;
createChart(scale);
const chart = chartState.chart;
if (!chart) return;
try {
setWhitespace(chart, scale);
console.log(`preset: ${preset.id}`);
const legend = preset.applyPreset({
chart,
datasets,
preset,
activeResources,
});
legendSetter(legend);
} catch (error) {
console.error("chart: render: failed", error);
}
}

View File

@@ -0,0 +1,10 @@
import { getInitialRange } from "./time";
export const LOCAL_STORAGE_RANGE_KEY = "chart-range";
export const URL_PARAMS_RANGE_FROM_KEY = "from";
export const URL_PARAMS_RANGE_TO_KEY = "to";
export const chartState = {
chart: null as IChartApi | null,
range: getInitialRange(),
};

View File

@@ -0,0 +1,110 @@
import { HEIGHT_CHUNK_SIZE } from "../../datasets";
import { debounce } from "../../utils/debounce";
import { writeURLParam } from "../../utils/urlParams";
import { setMinMaxMarkers } from "./markers";
import {
chartState,
LOCAL_STORAGE_RANGE_KEY,
URL_PARAMS_RANGE_FROM_KEY,
URL_PARAMS_RANGE_TO_KEY,
} from "./state";
const debouncedUpdateURLParams = debounce((range: TimeRange | null) => {
if (!range) return;
writeURLParam(URL_PARAMS_RANGE_FROM_KEY, String(range.from));
writeURLParam(URL_PARAMS_RANGE_TO_KEY, String(range.to));
localStorage.setItem(LOCAL_STORAGE_RANGE_KEY, JSON.stringify(range));
}, 1000);
export function initTimeScale({
activeResources,
}: {
activeResources: Accessor<Set<ResourceDataset<any, any>>>;
}) {
setTimeScale(chartState.range);
const debouncedFetch = debounce((range: TimeRange | null) => {
if (!range) return;
let ids: number[] = [];
if (typeof range.from === "string" && typeof range.to === "string") {
const from = new Date(range.from).getUTCFullYear();
const to = new Date(range.to).getUTCFullYear();
ids = Array.from({ length: to - from + 1 }, (_, i) => i + from);
} else {
const from = Math.floor(Number(range.from) / HEIGHT_CHUNK_SIZE);
const to = Math.floor(Number(range.to) / HEIGHT_CHUNK_SIZE);
const length = to - from + 1;
ids = Array.from({ length }, (_, i) => (from + i) * HEIGHT_CHUNK_SIZE);
}
ids.forEach((id) => {
activeResources().forEach((resource) => resource.fetch(id));
});
}, 100);
debouncedFetch(chartState.range);
let timeout = setTimeout(() => {
chartState.chart?.timeScale().subscribeVisibleTimeRangeChange((range) => {
debouncedFetch(range);
debouncedUpdateURLParams(range);
range = range || chartState.range;
chartState.range = range;
});
}, 50);
onCleanup(() => clearTimeout(timeout));
}
export function getInitialRange(): TimeRange {
const urlParams = new URLSearchParams(window.location.search);
const urlFrom = urlParams.get(URL_PARAMS_RANGE_FROM_KEY);
const urlTo = urlParams.get(URL_PARAMS_RANGE_TO_KEY);
if (urlFrom && urlTo) {
return {
from: urlFrom,
to: urlTo,
} satisfies TimeRange;
}
const savedTimeRange = JSON.parse(
localStorage.getItem(LOCAL_STORAGE_RANGE_KEY) || "null",
) as TimeRange | null;
if (savedTimeRange) {
return savedTimeRange;
}
const defaultTo = new Date();
const defaultFrom = new Date();
defaultFrom.setDate(defaultFrom.getUTCDate() - 6 * 30);
const defaultTimeRange = {
from: defaultFrom.toJSON().split("T")[0],
to: defaultTo.toJSON().split("T")[0],
} satisfies TimeRange;
return defaultTimeRange;
}
export function setTimeScale(range: TimeRange | null) {
if (range) {
console.log(range);
setTimeout(() => {
chartState.chart?.timeScale().setVisibleRange(range);
}, 1);
}
}

View File

@@ -0,0 +1,9 @@
interface PriceSeriesOptions {
halved?: boolean;
title?: string;
id?: string;
lowerOpacity?: boolean;
inverseColors?: boolean;
seriesOptions?: DeepPartial<SeriesOptionsCommon>;
priceScaleOptions?: DeepPartial<PriceScaleOptions>;
}

View File

@@ -0,0 +1,50 @@
import { dateToString, getNumberOfDaysBetweenTwoDates } from "../../utils/date";
import { ONE_DAY_IN_MS } from "../../utils/time";
import { createLineSeries } from "../series/creators/line";
export const DAY_BEFORE_GENESIS_DAY = "2009-01-02";
export const GENESIS_DAY = "2009-01-03";
// export const DAY_BEFORE_WHITEPAPER_DAY = "2008-10-30";
// export const WHITEPAPER_DAY = "2008-10-31";
const whitespaceStartDate = "1970-01-01";
const whitespaceEndDate = "2100-01-01";
const whitespaceDateDataset: (SingleValueData & Numbered)[] = new Array(
getNumberOfDaysBetweenTwoDates(
new Date(whitespaceStartDate),
new Date(whitespaceEndDate),
),
)
.fill(0)
.map((_, index) => {
const date = new Date(whitespaceStartDate);
date.setUTCDate(date.getUTCDay() + index);
return {
number: date.valueOf() / ONE_DAY_IN_MS,
time: dateToString(date),
value: NaN,
};
});
const whitespaceHeightDataset: (WhitespaceData & Numbered)[] = new Array(
840_000,
)
.fill(0)
.map(
(_, index) =>
({
time: index,
number: index,
}) as any,
);
export function setWhitespace(chart: IChartApi, scale: ResourceScale) {
const whitespaceSeries = createLineSeries(chart);
if (scale === "date") {
whitespaceSeries.setData(whitespaceDateDataset);
} else {
whitespaceSeries.setData(whitespaceHeightDataset);
}
}

View File

@@ -0,0 +1,28 @@
import { defaultSeriesOptions } from "./options";
type AreaOptions = DeepPartial<AreaStyleOptions & SeriesOptionsCommon>;
export const createAreaSeries = (
chart: IChartApi,
options?: AreaOptions & {
color?: string;
},
) => {
const { color } = options || {};
// const fillColor = `${color}11`;
const fillColor = color;
const seriesOptions: AreaOptions = {
// priceScaleId: 'left',
...defaultSeriesOptions,
lineColor: color,
topColor: fillColor,
bottomColor: fillColor,
...options,
};
const series = chart.addAreaSeries(seriesOptions);
return series;
};

View File

@@ -0,0 +1,52 @@
import { colors } from "/src/scripts/utils/colors";
import { defaultSeriesOptions } from "./options";
const DEFAULT_BASELINE_TOP_COLOR = colors.profit;
const DEFAULT_BASELINE_BOTTOM_COLOR = colors.loss;
export const DEFAULT_BASELINE_COLORS = [
DEFAULT_BASELINE_TOP_COLOR,
DEFAULT_BASELINE_BOTTOM_COLOR,
];
export const createBaseLineSeries = (
chart: IChartApi,
options: BaselineSeriesOptions,
) => {
const {
title,
color,
topColor,
topLineColor,
bottomColor,
bottomLineColor,
base,
lineColor,
} = options;
const allTopColor = topColor || color || DEFAULT_BASELINE_TOP_COLOR;
const topFillColor = `${allTopColor}`;
const allBottomColor = bottomColor || color || DEFAULT_BASELINE_BOTTOM_COLOR;
const bottomFillColor = `${allBottomColor}`;
const seriesOptions: DeepPartialBaselineOptions = {
priceScaleId: "right",
...defaultSeriesOptions,
lineWidth: 1,
...options,
...options.options,
...(base ? { baseValue: { type: "price", price: base } } : {}),
topLineColor: topLineColor || lineColor || allTopColor,
topFillColor1: topFillColor,
topFillColor2: topFillColor,
bottomLineColor: bottomLineColor || lineColor || allBottomColor,
bottomFillColor1: bottomFillColor,
bottomFillColor2: bottomFillColor,
title,
};
const series = chart.addBaselineSeries(seriesOptions);
return series;
};

View File

@@ -0,0 +1,42 @@
import { colors } from "/src/scripts/utils/colors";
export const createCandlesticksSeries = (
chart: IChartApi,
options: PriceSeriesOptions,
): [ISeriesApi<"Candlestick">, string[]] => {
const { inverseColors, lowerOpacity } = options;
const upColor = lowerOpacity
? inverseColors
? colors.darkLoss
: colors.darkProfit
: inverseColors
? colors.loss
: colors.profit;
const downColor = lowerOpacity
? inverseColors
? colors.darkProfit
: colors.darkLoss
: inverseColors
? colors.profit
: colors.loss;
const candlestickSeries = chart.addCandlestickSeries({
baseLineVisible: false,
upColor,
wickUpColor: upColor,
downColor,
wickDownColor: downColor,
borderVisible: false,
priceLineVisible: false,
baseLineColor: "",
borderColor: "",
borderDownColor: "",
borderUpColor: "",
// lastValueVisible: false,
...options.seriesOptions,
});
return [candlestickSeries, [upColor, downColor]];
};

View File

@@ -0,0 +1,30 @@
import { PRICE_SCALE_MOMENTUM_ID } from "../../chart/price";
import { defaultSeriesOptions } from "./options";
type HistogramOptions = DeepPartial<
HistogramStyleOptions & SeriesOptionsCommon
>;
export const createHistogramSeries = (
chart: IChartApi,
options?: HistogramOptions,
) => {
const seriesOptions: HistogramOptions = {
priceScaleId: "left",
...defaultSeriesOptions,
...options,
};
const series = chart.addHistogramSeries(seriesOptions);
try {
chart.priceScale(PRICE_SCALE_MOMENTUM_ID).applyOptions({
scaleMargins: {
top: 0.9,
bottom: 0,
},
});
} catch {}
return series;
};

View File

@@ -0,0 +1,75 @@
import {
readBooleanFromStorage,
saveToStorage,
} from "/src/scripts/utils/storage";
import {
readBooleanURLParam,
writeURLParam,
} from "/src/scripts/utils/urlParams";
import { createRWS } from "/src/solid/rws";
import { chartState } from "../../chart/state";
import { setTimeScale } from "../../chart/time";
export function createSeriesLegend({
id,
presetId,
title,
color,
series,
defaultVisible = true,
disabled: _disabled,
visible: _visible,
url,
}: {
id: string;
presetId: string;
title: string;
color: Accessor<string | string[]>;
series: ISeriesApi<SeriesType>;
defaultVisible?: boolean;
disabled?: Accessor<boolean>;
visible?: RWS<boolean>;
url?: string;
}) {
const storageID = `${presetId}-${id}`;
const visible =
_visible ||
createRWS(
readBooleanURLParam(id) ??
readBooleanFromStorage(storageID) ??
defaultVisible,
);
const disabled = createMemo(_disabled || (() => false));
createEffect(() => {
const v = visible();
const d = disabled();
series.applyOptions({
visible: !d && v,
});
setTimeScale(chartState.range);
if (v !== defaultVisible) {
writeURLParam(id, v);
saveToStorage(storageID, v);
} else {
writeURLParam(id, undefined);
saveToStorage(storageID, undefined);
}
});
return {
id,
title,
series,
color,
visible,
disabled,
url,
};
}

View File

@@ -0,0 +1,10 @@
import { defaultSeriesOptions } from "./options";
export const createLineSeries = (
chart: IChartApi,
options?: DeepPartialLineOptions,
) =>
chart.addLineSeries({
...defaultSeriesOptions,
...options,
});

View File

@@ -0,0 +1,7 @@
export const defaultSeriesOptions: DeepPartial<SeriesOptionsCommon> = {
// @ts-ignore
lineWidth: 1.5,
priceLineVisible: false,
baseLineVisible: false,
baseLineColor: "",
};

View File

@@ -0,0 +1,13 @@
interface BaselineSeriesOptions {
color?: string;
topColor?: string;
topLineColor?: string;
bottomColor?: string;
bottomLineColor?: string;
lineColor?: string;
base?: number;
options?: DeepPartialBaselineOptions;
title?: string;
}
type SeriesLegend = ReturnType<typeof import("./legend").createSeriesLegend>;

View File

@@ -0,0 +1,45 @@
export const resetRightPriceScale = (
chart: IChartApi,
options?: FullPriceScaleOptions,
) => {
const finalOptions = {
...options,
scaleMargins: {
...(options?.halved
? {
top: 0.5,
bottom: 0.05,
}
: {
top: 0.1,
bottom: 0.1,
}),
...options?.scaleMargins,
},
};
chart.priceScale("right").applyOptions(finalOptions);
return finalOptions;
};
export const resetLeftPriceScale = (
chart: IChartApi,
options?: FullPriceScaleOptions,
) =>
chart.priceScale("left").applyOptions({
visible: false,
...options,
scaleMargins: {
...(options?.halved
? {
top: 0.475,
bottom: 0.025,
}
: {
top: 0.25,
bottom: 0.25,
}),
...options?.scaleMargins,
},
});

View File

@@ -0,0 +1,3 @@
interface FullPriceScaleOptions extends DeepPartial<PriceScaleOptions> {
halved?: boolean;
}

View File

@@ -0,0 +1,218 @@
import {
addressCohortsBySize,
addressCohortsByType,
} from "../../datasets/consts/address";
import { liquidities } from "../../datasets/consts/liquidities";
import { colors } from "../../utils/colors";
import { createCohortPresetList } from "../templates/cohort";
import { applyMultipleSeries, SeriesType } from "../templates/multiple";
export function createPresets({
scale,
datasets,
}: {
scale: ResourceScale;
datasets: Datasets;
}): PartialPresetFolder {
return {
name: "Addresses",
tree: [
{
scale,
name: `Total Non Empty Addresses`,
title: `Total Non Empty Address`,
description: "",
icon: IconTablerWallet,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `Total Non Empty Address`,
color: colors.bitcoin,
seriesType: SeriesType.Area,
dataset: params.datasets[scale].address_count,
},
],
});
},
},
{
scale,
name: `New Addresses`,
title: `New Addresses`,
description: "",
icon: IconTablerSparkles,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `New Addresses`,
color: colors.white,
dataset: params.datasets[scale].created_addresses,
},
],
});
},
},
{
scale,
name: `Total Addresses Created`,
title: `Total Addresses Created`,
description: "",
icon: IconTablerArchive,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `Total Addresses Created`,
color: colors.bitcoin,
seriesType: SeriesType.Area,
dataset: params.datasets[scale].created_addresses,
},
],
});
},
},
{
scale,
name: `Total Empty Addresses`,
title: `Total Empty Addresses`,
description: "",
icon: IconTablerTrash,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `Total Empty Addresses`,
color: colors.darkWhite,
seriesType: SeriesType.Area,
dataset: params.datasets[scale].empty_addresses,
},
],
});
},
},
{
name: "By Size",
tree: addressCohortsBySize.map(({ key, name }) =>
createAddressPresetFolder({
datasets,
scale,
color: colors[key],
name,
datasetKey: key,
}),
),
},
{
scale,
name: "By Type",
tree: addressCohortsByType.map(({ key, name }) =>
createAddressPresetFolder({
datasets,
scale,
color: colors[key],
name,
datasetKey: key,
}),
),
},
],
} satisfies PartialPresetFolder;
}
function createAddressPresetFolder<Scale extends ResourceScale>({
datasets,
scale,
color,
name,
datasetKey,
}: {
datasets: Datasets;
scale: Scale;
name: string;
datasetKey: AddressCohortKey;
color: string;
}): PartialPresetFolder {
return {
name,
tree: [
createAddressCountPreset({ scale, name, datasetKey, color }),
...createCohortPresetList({
title: name,
datasets,
scale,
name,
color,
datasetKey,
}),
{
name: `Split By Liquidity`,
tree: liquidities.map(
(liquidity): PartialPresetFolder => ({
name: liquidity.name,
tree: createCohortPresetList({
title: `${liquidity.name} ${name}`,
name: `${liquidity.name} ${name}`,
datasets,
scale,
color,
datasetKey: `${liquidity.key}_${datasetKey}`,
}),
}),
),
},
],
};
}
export function createAddressCountPreset<Scale extends ResourceScale>({
scale,
color,
name,
datasetKey,
}: {
scale: Scale;
name: string;
datasetKey: AddressCohortKey;
color: string;
}): PartialPreset {
return {
scale,
name: `Address Count`,
title: `${name} Address Count`,
icon: IconTablerAddressBook,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Address Count",
color,
dataset: params.datasets[scale][`${datasetKey}_address_count`],
},
],
});
},
description: "",
};
}

View File

@@ -0,0 +1,221 @@
import { colors } from "../../utils/colors";
import { applyMultipleSeries, SeriesType } from "../templates/multiple";
export function createPresets() {
const scale: ResourceScale = "date";
return {
name: "Blocks",
tree: [
{
scale,
icon: IconTablerWall,
name: "Height",
title: "Block Height",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Height",
color: colors.bitcoin,
dataset: params.datasets.date.last_height,
},
],
});
},
},
{
scale,
name: "Mined",
tree: [
{
scale,
icon: IconTablerCube,
name: "Daily Sum",
title: "Daily Sum Of Blocks Mined",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Target",
color: colors.white,
dataset: params.datasets.date.blocks_mined_1d_target,
options: {
lineStyle: 3,
// lineStyle: LineStyle.LargeDashed,
},
},
{
title: "1W Avg.",
color: colors.momentumYellow,
dataset: params.datasets.date.blocks_mined_1w_sma,
defaultVisible: false,
},
{
title: "1M Avg.",
color: colors.bitcoin,
dataset: params.datasets.date.blocks_mined_1m_sma,
},
{
title: "Mined",
color: colors.darkBitcoin,
dataset: params.datasets.date.blocks_mined,
},
],
});
},
},
{
scale,
icon: IconTablerLetterW,
name: "Weekly Sum",
title: "Weekly Sum Of Blocks Mined",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Target",
color: colors.white,
dataset: params.datasets.date.blocks_mined_1w_target,
options: {
lineStyle: 3,
// lineStyle: LineStyle.LargeDashed,
},
},
{
title: "Sum Mined",
color: colors.bitcoin,
dataset: params.datasets.date.blocks_mined_1w_sum,
},
],
});
},
},
{
scale,
icon: IconTablerLetterM,
name: "Monthly Sum",
title: "Monthly Sum Of Blocks Mined",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Target",
color: colors.white,
dataset: params.datasets.date.blocks_mined_1m_target,
options: {
// lineStyle: LineStyle.LargeDashed,
lineStyle: 3,
},
},
{
title: "Sum Mined",
color: colors.bitcoin,
dataset: params.datasets.date.blocks_mined_1m_sum,
},
],
});
},
},
{
scale,
icon: IconTablerLetterY,
name: "Yearly Sum",
title: "Yearly Sum Of Blocks Mined",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Target",
color: colors.white,
dataset: params.datasets.date.blocks_mined_1y_target,
options: {
lineStyle: 3,
// lineStyle: LineStyle.LargeDashed,
},
},
{
title: "Sum Mined",
color: colors.bitcoin,
dataset: params.datasets.date.blocks_mined_1y_sum,
},
],
});
},
},
{
scale,
icon: IconTablerWall,
name: "Total",
title: "Total Blocks Mined",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Mined",
color: colors.bitcoin,
seriesType: SeriesType.Area,
dataset: params.datasets.date.total_blocks_mined,
},
],
});
},
},
],
},
{
scale,
icon: IconTablerStack3,
name: "Cumulative Size",
title: "Cumulative Block Size",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Size (MB)",
color: colors.darkWhite,
seriesType: SeriesType.Area,
dataset: params.datasets.date.cumulative_block_size,
},
],
});
},
},
],
} satisfies PartialPresetFolder;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,127 @@
import {
fromXCohorts,
fromXToYCohorts,
upToCohorts,
xthCohorts,
yearCohorts,
} from "../../datasets/consts/age";
import { colors } from "../../utils/colors";
import { createCohortPresetFolder } from "../templates/cohort";
import { applyMultipleSeries } from "../templates/multiple";
export function createPresets({
scale,
datasets,
}: {
scale: ResourceScale;
datasets: Datasets;
}) {
return {
name: "Hodlers",
tree: [
{
scale,
name: `Hodl Supply`,
title: `Hodl Supply`,
description: "",
icon: IconTablerRipple,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `24h`,
color: colors.up_to_1d,
dataset:
params.datasets.date
.up_to_1d_supply_to_circulating_supply_ratio,
},
...fromXToYCohorts.map(({ key, name, legend }) => ({
title: legend,
color: colors[key],
dataset:
params.datasets.date[
`${key}_supply_to_circulating_supply_ratio`
],
})),
{
title: `15y+`,
color: colors.from_15y,
dataset:
params.datasets.date
.from_15y_supply_to_circulating_supply_ratio,
},
],
});
},
},
...xthCohorts.map(({ key, name, legend }) =>
createCohortPresetFolder({
datasets,
scale,
color: colors[key],
name: legend,
datasetKey: key,
title: name,
}),
),
{
name: "Up To X",
tree: upToCohorts.map(({ key, name }) =>
createCohortPresetFolder({
datasets,
scale,
color: colors[key],
name,
datasetKey: key,
title: name,
}),
),
},
{
name: "From X To Y",
tree: fromXToYCohorts.map(({ key, name }) =>
createCohortPresetFolder({
datasets,
scale,
color: colors[key],
name,
datasetKey: key,
title: name,
}),
),
},
{
name: "From X",
tree: fromXCohorts.map(({ key, name }) =>
createCohortPresetFolder({
datasets,
scale,
color: colors[key],
name,
datasetKey: key,
title: name,
}),
),
},
{
name: "Years",
tree: yearCohorts.map(({ key, name }) =>
createCohortPresetFolder({
datasets,
scale,
color: colors[key],
name,
datasetKey: key,
title: name,
}),
),
},
],
} satisfies PartialPresetFolder;
}

View File

@@ -0,0 +1,296 @@
import { createRWS } from "/src/solid/rws";
import { colors } from "../utils/colors";
import { replaceHistory } from "../utils/history";
import { stringToId } from "../utils/id";
import { resetURLParams } from "../utils/urlParams";
import { createPresets as createAddressesPresets } from "./addresses";
import { createPresets as createBlocksPresets } from "./blocks";
import { createPresets as createCoinblocksPresets } from "./coinblocks";
import { createPresets as createHodlersPresets } from "./hodlers";
import { createPresets as createMarketPresets } from "./market";
import { createPresets as createMinersPresets } from "./miners";
import { createCohortPresetList } from "./templates/cohort";
import { createPresets as createTransactionsPresets } from "./transactions";
export const LOCAL_STORAGE_FAVORITES_KEY = "favorites";
export const LOCAL_STORAGE_FOLDERS_KEY = "folders";
export const LOCAL_STORAGE_HISTORY_KEY = "history";
export const LOCAL_STORAGE_SELECTED_KEY = "preset";
export const LOCAL_STORAGE_VISITED_KEY = "visited";
export function createPresets(datasets: Datasets): Presets {
const partialTree = [
{
name: "Dashboards (Coming soon)",
tree: [],
},
{
name: "Charts",
tree: [
{
name: "By Date",
tree: [
createMarketPresets({ scale: "date", datasets }),
createBlocksPresets(),
createMinersPresets("date"),
createTransactionsPresets("date"),
...createCohortPresetList({
datasets,
scale: "date",
color: colors.bitcoin,
datasetKey: "",
name: "",
title: "",
}),
createHodlersPresets({ scale: "date", datasets }),
createAddressesPresets({ scale: "date", datasets }),
createCoinblocksPresets({ scale: "date", datasets }),
],
} satisfies PartialPresetFolder,
{
name: "By Height (Coming soon)",
tree: [
// createMarketPresets({ scale: "height", datasets }),
// createMinersPresets("height"),
// createTransactionsPresets("height"),
// ...createCohortPresetList({
// datasets,
// scale: "height",
// color: colors.bitcoin,
// name: "",
// datasetKey: "",
// title: "",
// }),
// createHodlersPresets({ scale: "height", datasets }),
// createAddressesPresets({ scale: "height", datasets }),
// createCoinblocksPresets({ scale: "height", datasets }),
],
} satisfies PartialPresetFolder,
],
},
];
const { list, ids, tree } = flatten(partialTree);
checkIfDuplicateIds(ids);
setIsFavorites(list);
setVisited(list);
const favorites = createMemo(() =>
list.filter((preset) => preset.isFavorite()),
);
createEffect(() => {
localStorage.setItem(
LOCAL_STORAGE_FAVORITES_KEY,
JSON.stringify(favorites().map((p) => p.id)),
);
});
const visited = createMemo(() => list.filter((preset) => preset.visited()));
createEffect(() => {
localStorage.setItem(
LOCAL_STORAGE_VISITED_KEY,
JSON.stringify(visited().map((p) => p.id)),
);
});
createEffect(() => {
const serializedHistory: SerializedPresetsHistory = history().map(
({ preset, date }) => ({
p: preset.id,
d: date.valueOf(),
}),
);
localStorage.setItem(
LOCAL_STORAGE_HISTORY_KEY,
JSON.stringify(serializedHistory),
);
});
const history: PresetsHistorySignal = createRWS(getHistory(list), {
equals: false,
});
const selected = createRWS(findInitialPreset(list), {
equals: false,
});
createEffect((previousPreset: Preset) => {
if (previousPreset && previousPreset !== selected()) {
resetURLParams();
}
return selected();
}, selected());
createEffect(() => selected().visited.set(true));
const select = (preset: Preset) => {
if (selected().id === preset.id) {
return;
}
history.set((l) => {
l.unshift({
date: new Date(),
preset,
});
return l;
});
_select(preset, selected.set);
};
const openedFolders = createRWS(
new Set(
JSON.parse(
localStorage.getItem(LOCAL_STORAGE_FOLDERS_KEY) || "[]",
) as string[],
),
{
equals: false,
},
);
createEffect(() => {
localStorage.setItem(
LOCAL_STORAGE_FOLDERS_KEY,
JSON.stringify(Array.from(openedFolders())),
);
});
return {
tree,
list,
selected,
favorites,
history,
select,
openedFolders,
};
}
function _select(preset: Preset, set: Setter<Preset>) {
const key = LOCAL_STORAGE_SELECTED_KEY;
const value = preset.id;
localStorage.setItem(key, value);
replaceHistory({ pathname: `/${value}` });
set(preset);
}
function flatten(partialTree: PartialPresetTree) {
const result: { list: Preset[]; ids: string[] } = { list: [], ids: [] };
const _flatten = (partialTree: PartialPresetTree, path?: FilePath) => {
partialTree.forEach((anyPreset) => {
if ("tree" in anyPreset) {
const id = stringToId(
`${(path || [])?.map(({ name }) => name).join(" ")} ${anyPreset.name} folder`,
);
const presetFolder: PresetFolder = {
...anyPreset,
tree: anyPreset.tree as PresetTree,
id,
};
Object.assign(anyPreset, presetFolder);
result.ids.push(presetFolder.id);
return _flatten(presetFolder.tree, [
...(path || []),
{
name: presetFolder.name,
id: presetFolder.id,
},
]);
} else {
const preset = {
...anyPreset,
path: path || [],
isFavorite: createRWS(false),
visited: createRWS(false),
id: `${anyPreset.scale}-to-${stringToId(anyPreset.title)}`,
} satisfies Preset;
result.list.push(Object.assign(anyPreset, preset));
result.ids.push(preset.id);
}
});
};
_flatten(partialTree);
return { ...result, tree: partialTree as PresetTree };
}
function checkIfDuplicateIds(ids: string[]) {
if (ids.length !== new Set(ids).size) {
const m = new Map<string, number>();
ids.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");
}
}
function findInitialPreset(presets: Preset[]): Preset {
const urlPreset = document.location.pathname.substring(1);
return (
(urlPreset &&
(presets.find((preset) => preset.id === urlPreset) ||
presets.find(
(preset) =>
preset.id === localStorage.getItem(LOCAL_STORAGE_SELECTED_KEY),
))) ||
presets[0]
);
}
function setIsFavorites(list: Preset[]) {
(
JSON.parse(
localStorage.getItem(LOCAL_STORAGE_FAVORITES_KEY) || "[]",
) as string[]
).forEach((id) => {
list.find((preset) => preset.id === id)?.isFavorite.set(true);
});
}
function setVisited(list: Preset[]) {
(
JSON.parse(
localStorage.getItem(LOCAL_STORAGE_VISITED_KEY) || "[]",
) as string[]
).forEach((id) => {
list.find((preset) => preset.id === id)?.visited.set(true);
});
}
function getHistory(list: Preset[]): PresetsHistory {
return (
JSON.parse(
localStorage.getItem(LOCAL_STORAGE_HISTORY_KEY) || "[]",
) as SerializedPresetsHistory
).flatMap(({ p, d }) => {
const preset = list.find((preset) => preset.id === p);
return preset ? [{ preset, date: new Date(d) }] : [];
});
}

View File

@@ -0,0 +1,78 @@
import { averages } from "/src/scripts/datasets/date";
import { colors } from "/src/scripts/utils/colors";
import { applyMultipleSeries } from "../../templates/multiple";
export function createPresets(datasets: Datasets): PartialPresetFolder {
const scale: ResourceScale = "date";
return {
name: "Averages",
tree: [
{
scale,
icon: IconTablerMathAvg,
name: "All",
title: "All Averages",
applyPreset(params) {
return applyMultipleSeries({
...params,
list: averages.map((average) => ({
title: average.key.toUpperCase(),
color: colors[`_${average.key}`],
dataset: params.datasets.date[`price_${average.key}_sma`],
})),
});
},
description: "",
},
...averages.map(({ name, key }) =>
createPresetFolder({
datasets,
scale,
color: colors[`_${key}`],
name,
key,
}),
),
],
};
}
function createPresetFolder({
scale,
datasets,
color,
name,
key,
}: {
datasets: Datasets;
scale: ResourceScale;
color: string;
name: string;
key: AverageName;
}) {
return {
// id,
// name,
// tree: [
// {
scale,
name,
description: "",
icon: IconTablerMathAvg,
title: `${name} Moving Average`,
applyPreset(params) {
return applyMultipleSeries({
...params,
list: [
{
title: `SMA`,
color,
dataset: datasets.date[`price_${key}_sma`],
},
],
});
},
} satisfies PartialPreset;
}

View File

@@ -0,0 +1,77 @@
import { colors } from "../../utils/colors";
import { applyMultipleSeries } from "../templates/multiple";
import { createPresets as createAveragesPresets } from "./averages";
import { createPresets as createIndicatorsPresets } from "./indicators";
import { createPresets as createReturnsPresets } from "./returns";
export function createPresets({
scale,
datasets,
}: {
scale: ResourceScale;
datasets: Datasets;
}) {
return {
name: "Market",
tree: [
{
scale,
icon: IconTablerCurrencyDollar,
name: "Price",
title: "Market Price",
applyPreset(params) {
return applyMultipleSeries({ ...params });
},
description: "",
},
{
scale,
icon: IconTablerPercentage,
name: "Performance",
title: "Market Performance",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceOptions: {
id: "performance",
title: "Performance",
priceScaleOptions: {
mode: 2,
},
},
});
},
description: "",
},
{
scale,
icon: IconTablerInfinity,
name: "Capitalization",
title: "Market Capitalization",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Market Cap.",
dataset: params.datasets[scale].market_cap,
color: colors.bitcoin,
},
],
});
},
description: "",
},
...(scale === "date"
? ([
createAveragesPresets(datasets),
createReturnsPresets(datasets),
createIndicatorsPresets(datasets),
] satisfies PartialPresetTree)
: []),
],
} satisfies PartialPresetFolder;
}

View File

@@ -0,0 +1,6 @@
export function createPresets(datasets: Datasets) {
return {
name: "Indicators",
tree: [],
} satisfies PartialPresetFolder;
}

View File

@@ -0,0 +1,79 @@
import {
compoundReturns,
totalReturns,
} from "/src/scripts/datasets/consts/returns";
import { applyMultipleSeries, SeriesType } from "../../templates/multiple";
export function createPresets(datasets: Datasets) {
return {
name: "Returns",
tree: [
{
name: "Total",
tree: [
...totalReturns.map(({ name, key }) =>
createPreset({
scale: "date",
datasets,
name,
title: `${name} Total`,
key: `${key}_total`,
}),
),
],
},
{
name: "Compound",
tree: [
...compoundReturns.map(({ name, key }) =>
createPreset({
scale: "date",
datasets,
name,
title: `${name} Compound`,
key: `${key}_compound`,
}),
),
],
},
],
} satisfies PartialPresetFolder;
}
function createPreset({
scale,
datasets,
name,
title,
key,
}: {
scale: ResourceScale;
datasets: Datasets;
name: string;
title: string;
key: `${TotalReturnKey}_total` | `${CompoundReturnKey}_compound`;
}): PartialPreset {
return {
scale,
name,
description: "",
icon: IconTablerReceiptTax,
title: `${title} Return`,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `Return (%)`,
seriesType: SeriesType.Based,
dataset: datasets.date[`price_${key}_return`],
},
],
});
},
};
}

View File

@@ -0,0 +1,902 @@
import { colors } from "../../utils/colors";
import { applyMultipleSeries, SeriesType } from "../templates/multiple";
export function createPresets(scale: ResourceScale) {
return {
name: "Miners",
tree: [
{
name: "Coinbases",
tree: [
...(scale === "date"
? ([
{
name: "Last",
tree: [
{
scale,
icon: IconTablerCoinBitcoin,
name: "In Bitcoin",
title: "Last Coinbase (In Bitcoin)",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Last",
color: colors.bitcoin,
dataset: params.datasets[scale].last_coinbase,
},
],
});
},
},
{
scale,
icon: IconTablerCoin,
name: "In Dollars",
title: "Last Coinbase (In Dollars)",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Last",
color: colors.dollars,
dataset:
params.datasets[scale].last_coinbase_in_dollars,
},
],
});
},
},
],
},
{
scale,
name: "Daily Sum",
tree: [
{
scale,
icon: IconTablerMoneybag,
name: "In Bitcoin",
title: "Daily Sum Of Bitcoin Coinbases",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Coinbases (Bitcoin)",
color: colors.bitcoin,
dataset: params.datasets[scale].coinbase,
},
],
});
},
},
{
scale,
icon: IconTablerCash,
name: "In Dollars",
title: "Daily Sum Of Dollar Coinbases",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Coinbases (Dollars)",
color: colors.dollars,
dataset:
params.datasets[scale].coinbase_in_dollars,
},
],
});
},
},
],
},
{
scale,
name: "Yearly Sum",
tree: [
{
scale,
icon: IconTablerMoneybag,
name: "In Bitcoin",
title: "Yearly Sum Of Bitcoin Coinbases",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Coinbases (Bitcoin)",
color: colors.bitcoin,
dataset: params.datasets[scale].coinbase_1y_sum,
},
],
});
},
},
{
scale,
icon: IconTablerCash,
name: "In Dollars",
title: "Yearly Sum Of Dollar Coinbases",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Coinbases (Dollars)",
color: colors.dollars,
dataset:
params.datasets[scale]
.coinbase_in_dollars_1y_sum,
},
],
});
},
},
],
},
{
scale,
name: "Cumulative",
tree: [
{
scale,
icon: IconTablerMoneybag,
name: "In Bitcoin",
title: "Cumulative Bitcoin Coinbases",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Coinbases (Bitcoin)",
color: colors.bitcoin,
dataset:
params.datasets[scale].cumulative_coinbase,
},
],
});
},
},
{
scale,
icon: IconTablerCash,
name: "In Dollars",
title: "Cumulative Dollar Coinbases",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Coinbases (Dollars)",
color: colors.dollars,
dataset:
params.datasets[scale]
.cumulative_coinbase_in_dollars,
},
],
});
},
},
],
},
] satisfies PartialPresetTree)
: []),
],
},
{
name: "Subsidies",
tree: [
...(scale === "date"
? ([
{
name: "Last",
tree: [
{
scale,
icon: IconTablerCoinBitcoin,
name: "In Bitcoin",
title: "Last Subsidy (In Bitcoin)",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Last",
color: colors.bitcoin,
dataset: params.datasets[scale].last_subsidy,
},
],
});
},
},
{
scale,
icon: IconTablerCoin,
name: "In Dollars",
title: "Last Subsidy (In Dollars)",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Last",
color: colors.dollars,
dataset:
params.datasets[scale].last_subsidy_in_dollars,
},
],
});
},
},
],
},
{
scale,
name: "Daily Sum",
tree: [
{
scale,
icon: IconTablerMoneybag,
name: "In Bitcoin",
title: "Daily Sum Of Bitcoin Subsidies",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Subsidies (Bitcoin)",
color: colors.bitcoin,
dataset: params.datasets[scale].subsidy,
},
],
});
},
},
{
scale,
icon: IconTablerCash,
name: "In Dollars",
title: "Daily Sum Of Dollar Subsidies",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Subsidies (Dollars)",
color: colors.dollars,
dataset:
params.datasets[scale].subsidy_in_dollars,
},
],
});
},
},
],
},
{
scale,
name: "Yearly Sum",
tree: [
{
scale,
icon: IconTablerMoneybag,
name: "In Bitcoin",
title: "Yearly Sum Of Bitcoin Subsidies",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Subsidies (Bitcoin)",
color: colors.bitcoin,
dataset: params.datasets[scale].subsidy_1y_sum,
},
],
});
},
},
{
scale,
icon: IconTablerCash,
name: "In Dollars",
title: "Yearly Sum Of Dollar Subsidies",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Subsidies (Dollars)",
color: colors.dollars,
dataset:
params.datasets[scale]
.subsidy_in_dollars_1y_sum,
},
],
});
},
},
],
},
{
scale,
name: "Cumulative",
tree: [
{
scale,
icon: IconTablerMoneybag,
name: "In Bitcoin",
title: "Cumulative Bitcoin Subsidies",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Subsidies (Bitcoin)",
color: colors.bitcoin,
dataset:
params.datasets[scale].cumulative_subsidy,
},
],
});
},
},
{
scale,
icon: IconTablerCash,
name: "In Dollars",
title: "Cumulative Dollar Subsidies",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Subsidies (Dollars)",
color: colors.dollars,
dataset:
params.datasets[scale]
.cumulative_subsidy_in_dollars,
},
],
});
},
},
],
},
] satisfies PartialPresetTree)
: []),
],
},
{
name: "Fees",
tree: [
...(scale === "date"
? ([
{
name: "Last",
tree: [
{
scale,
icon: IconTablerCoinBitcoin,
name: "In Bitcoin",
title: "Last Fees (In Bitcoin)",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Last",
color: colors.bitcoin,
dataset: params.datasets[scale].last_fees,
},
],
});
},
},
{
scale,
icon: IconTablerCoin,
name: "In Dollars",
title: "Last Fees (In Dollars)",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Last",
color: colors.dollars,
dataset:
params.datasets[scale].last_fees_in_dollars,
},
],
});
},
},
],
},
{
scale,
name: "Daily Sum",
tree: [
{
scale,
icon: IconTablerMoneybag,
name: "In Bitcoin",
title: "Daily Sum Of Bitcoin Fees",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Fees (Bitcoin)",
color: colors.bitcoin,
dataset: params.datasets[scale].fees,
},
],
});
},
},
{
scale,
icon: IconTablerCash,
name: "In Dollars",
title: "Daily Sum Of Dollar Fees",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Fees (Dollars)",
color: colors.dollars,
dataset: params.datasets[scale].fees_in_dollars,
},
],
});
},
},
],
},
{
scale,
name: "Yearly Sum",
tree: [
{
scale,
icon: IconTablerMoneybag,
name: "In Bitcoin",
title: "Yearly Sum Of Bitcoin Fees",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Fees (Bitcoin)",
color: colors.bitcoin,
dataset: params.datasets[scale].fees_1y_sum,
},
],
});
},
},
{
scale,
icon: IconTablerCash,
name: "In Dollars",
title: "Yearly Sum Of Dollar Fees",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Fees (Dollars)",
color: colors.dollars,
dataset:
params.datasets[scale].fees_in_dollars_1y_sum,
},
],
});
},
},
],
},
{
scale,
name: "Cumulative",
tree: [
{
scale,
icon: IconTablerMoneybag,
name: "In Bitcoin",
title: "Cumulative Bitcoin Fees",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Fees (Bitcoin)",
color: colors.bitcoin,
dataset: params.datasets[scale].cumulative_fees,
},
],
});
},
},
{
scale,
icon: IconTablerCash,
name: "In Dollars",
title: "Cumulative Dollar Fees",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Fees (Dollars)",
color: colors.dollars,
dataset:
params.datasets[scale]
.cumulative_fees_in_dollars,
},
],
});
},
},
],
},
] satisfies PartialPresetTree)
: []),
],
},
{
scale,
icon: IconTablerSwords,
name: "Subsidy V. Fees",
title: "Subsidy V. Fees",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Subsidy (%)",
color: colors.bitcoin,
dataset: params.datasets[scale].subsidy_to_coinbase_ratio,
},
{
title: "Fees (%)",
color: colors.darkBitcoin,
dataset: params.datasets[scale].fees_to_coinbase_ratio,
},
],
});
},
},
...(scale === "date"
? ([
{
scale,
icon: IconTablerCalculator,
name: "Puell Multiple",
title: "Puell Multiple",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
mode: 1,
},
list: [
{
title: "Multiple",
color: colors.bitcoin,
dataset: params.datasets.date.puell_multiple,
},
],
});
},
},
{
scale,
icon: IconTablerPick,
name: "Hash Rate",
title: "Hash Rate (EH/s)",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
mode: 1,
},
list: [
{
title: "1M SMA",
color: colors.momentumYellow,
dataset: params.datasets.date.hash_rate_1m_sma,
},
{
title: "1W SMA",
color: colors.bitcoin,
dataset: params.datasets.date.hash_rate_1w_sma,
},
{
title: "Rate",
color: colors.darkBitcoin,
dataset: params.datasets.date.hash_rate,
},
],
});
},
},
{
scale,
icon: IconTablerRibbonHealth,
name: "Hash Ribbon",
title: "Hash Ribbon (EH/s)",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
mode: 1,
},
list: [
{
title: "1M SMA",
color: colors.profit,
dataset: params.datasets.date.hash_rate_1m_sma,
},
{
title: "2M SMA",
color: colors.loss,
dataset: params.datasets.date.hash_rate_2m_sma,
},
],
});
},
},
{
scale,
icon: IconTablerTag,
name: "Hash Price",
title: "Hash Price",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Price ($/PH/s)",
color: colors.dollars,
dataset: params.datasets.date.hash_price,
},
],
});
},
},
] satisfies PartialPreset[])
: []),
{
scale,
icon: IconTablerWeight,
name: "Difficulty",
title: "Difficulty",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
mode: 1,
},
list: [
{
title: "Difficulty",
color: colors.bitcoin,
dataset: params.datasets[scale].difficulty,
},
],
});
},
},
...(scale === "date"
? ([
{
scale,
icon: IconTablerAdjustments,
name: "Difficulty Adjustment",
title: "Difficulty Adjustment",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Adjustment (%)",
// color: colors.bitcoin,
seriesType: SeriesType.Based,
dataset: params.datasets[scale].difficulty_adjustment,
},
],
});
},
},
] satisfies PartialPreset[])
: []),
{
scale,
icon: IconTablerBuildingFactory,
name: "Annualized Issuance",
title: "Annualized Issuance",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
mode: 1,
},
list: [
{
title: "Issuance",
color: colors.bitcoin,
dataset: params.datasets[scale].annualized_issuance,
},
],
});
},
},
{
scale,
icon: IconTablerBuildingFactory2,
name: "Yearly Inflation Rate",
title: "Yearly Inflation Rate",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
mode: 1,
},
list: [
{
title: "Rate (%)",
color: colors.bitcoin,
dataset: params.datasets[scale].yearly_inflation_rate,
},
],
});
},
},
// For scale === "height"
// block_size,
// block_weight,
// block_vbytes,
// block_interval,
],
} satisfies PartialPresetFolder;
}

View File

@@ -0,0 +1,985 @@
import { percentiles } from "../../datasets/consts/percentiles";
import { colors } from "../../utils/colors";
import { applyMultipleSeries, SeriesType } from "./multiple";
export function createCohortPresetFolder<Scale extends ResourceScale>({
datasets,
scale,
color,
name,
datasetKey,
title,
}: {
datasets: Datasets;
scale: Scale;
name: string;
datasetKey: AnyPossibleCohortKey;
color: string;
title: string;
}) {
return {
name,
tree: createCohortPresetList({
title,
datasets,
name,
scale,
color,
datasetKey,
}),
} satisfies PartialPresetFolder;
}
export function createCohortPresetList<Scale extends ResourceScale>({
name,
datasets,
scale,
color,
datasetKey,
title,
}: {
name: string;
datasets: Datasets;
scale: Scale;
datasetKey: AnyPossibleCohortKey;
title: string;
color: string;
}) {
const datasetPrefix = datasetKey
? (`${datasetKey}_` as const)
: ("" as const);
return [
{
name: "UTXOs",
tree: [
{
scale,
name: `Count`,
title: `${title} Unspent Transaction Outputs Count`,
icon: () => IconTablerTicket,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Count",
color,
seriesType: SeriesType.Area,
dataset: params.datasets[scale][`${datasetPrefix}utxo_count`],
},
],
});
},
description: "",
},
],
},
{
name: "Realized",
tree: [
{
scale,
name: `Price`,
title: `${title} Realized Price`,
description: "",
icon: () => IconTablerTag,
applyPreset(params) {
return applyMultipleSeries({
...params,
list: [
{
title: "Realized Price",
color,
dataset:
params.datasets[scale][`${datasetPrefix}realized_price`],
},
],
});
},
},
{
scale,
name: `Capitalization`,
title: `${title} Realized Capitalization`,
icon: () => IconTablerPigMoney,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `${name} Realized Cap.`,
color,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][`${datasetPrefix}realized_cap`],
},
...(datasetKey
? [
{
title: "Realized Cap.",
color: colors.bitcoin,
dataset: params.datasets[scale].realized_cap,
defaultVisible: false,
},
]
: []),
],
});
},
description: "",
},
{
scale,
name: `Capitalization 1M Net Change`,
title: `${title} Realized Capitalization 1 Month Net Change`,
icon: () => IconTablerStatusChange,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `Net Change`,
seriesType: SeriesType.Based,
dataset:
params.datasets[scale][
`${datasetPrefix}realized_cap_1m_net_change`
],
},
],
});
},
description: "",
},
{
scale,
name: `Profit`,
title: `${title} Realized Profit`,
icon: () => IconTablerCash,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Realized Profit",
dataset:
params.datasets[scale][`${datasetPrefix}realized_profit`],
color: colors.profit,
seriesType: SeriesType.Area,
},
],
});
},
description: "",
},
{
scale,
name: "Loss",
title: `${title} Realized Loss`,
icon: () => IconTablerCoffin,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Realized Loss",
dataset:
params.datasets[scale][`${datasetPrefix}realized_loss`],
color: colors.loss,
seriesType: SeriesType.Area,
},
],
});
},
description: "",
},
{
scale,
name: `PNL`,
title: `${title} Realized Profit And Loss`,
icon: () => IconTablerArrowsVertical,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Profit",
color: colors.profit,
dataset:
params.datasets[scale][`${datasetPrefix}realized_profit`],
seriesType: SeriesType.Based,
},
{
title: "Loss",
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}negative_realized_loss`
],
seriesType: SeriesType.Based,
},
],
});
},
description: "",
},
{
scale,
name: `Net PNL`,
title: `${title} Net Realized Profit And Loss`,
icon: () => IconTablerScale,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Net PNL",
seriesType: SeriesType.Based,
dataset:
params.datasets[scale][
`${datasetPrefix}net_realized_profit_and_loss`
],
},
],
});
},
description: "",
},
{
scale,
name: `Net PNL Relative To Market Cap`,
title: `${title} Net Realized Profit And Loss Relative To Market Capitalization`,
icon: () => IconTablerDivide,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Net",
seriesType: SeriesType.Based,
dataset:
params.datasets[scale][
`${datasetPrefix}net_realized_profit_and_loss_to_market_cap_ratio`
],
},
],
});
},
description: "",
},
{
scale,
name: `Cumulative Profit`,
title: `${title} Cumulative Realized Profit`,
icon: () => IconTablerSum,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Cumulative Realized Profit",
color: colors.profit,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}cumulative_realized_profit`
],
},
],
});
},
description: "",
},
{
scale,
name: "Cumulative Loss",
title: `${title} Cumulative Realized Loss`,
icon: () => IconTablerSum,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Cumulative Realized Loss",
color: colors.loss,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}cumulative_realized_loss`
],
},
],
});
},
description: "",
},
{
scale,
name: `Cumulative Net PNL`,
title: `${title} Cumulative Net Realized Profit And Loss`,
icon: () => IconTablerSum,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Cumulative Net Realized PNL",
seriesType: SeriesType.Based,
dataset:
params.datasets[scale][
`${datasetPrefix}cumulative_net_realized_profit_and_loss`
],
},
],
});
},
description: "",
},
{
scale,
name: `Cumulative Net PNL 30 Day Change`,
title: `${title} Cumulative Net Realized Profit And Loss 30 Day Change`,
icon: () => IconTablerTimeDuration30,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Cumulative Net Realized PNL 30d Change",
dataset:
params.datasets[scale][
`${datasetPrefix}cumulative_net_realized_profit_and_loss_1m_net_change`
],
seriesType: SeriesType.Based,
},
],
});
},
description: "",
},
],
},
{
name: "Unrealized",
tree: [
{
scale,
name: `Profit`,
title: `${title} Unrealized Profit`,
icon: () => IconTablerMoodDollar,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Profit",
dataset:
params.datasets[scale][`${datasetPrefix}unrealized_profit`],
color: colors.profit,
seriesType: SeriesType.Area,
},
],
});
},
description: "",
},
{
scale,
name: "Loss",
title: `${title} Unrealized Loss`,
icon: () => IconTablerMoodSadDizzy,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Loss",
dataset:
params.datasets[scale][`${datasetPrefix}unrealized_loss`],
color: colors.loss,
seriesType: SeriesType.Area,
},
],
});
},
description: "",
},
{
scale,
name: `PNL`,
title: `${title} Unrealized Profit And Loss`,
icon: () => IconTablerArrowsVertical,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Profit",
color: colors.profit,
dataset:
params.datasets[scale][`${datasetPrefix}unrealized_profit`],
seriesType: SeriesType.Based,
},
{
title: "Loss",
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}negative_unrealized_loss`
],
seriesType: SeriesType.Based,
},
],
});
},
description: "",
},
{
scale,
name: `Net PNL`,
title: `${title} Net Unrealized Profit And Loss`,
icon: () => IconTablerScale,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Net Unrealized PNL",
dataset:
params.datasets[scale][
`${datasetPrefix}net_unrealized_profit_and_loss`
],
seriesType: SeriesType.Based,
},
],
});
},
description: "",
},
{
scale,
name: `Net PNL Relative To Market Cap`,
title: `${title} Net Unrealized Profit And Loss Relative To Total Market Capitalization`,
icon: () => IconTablerDivide,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Relative Net Unrealized PNL",
dataset:
params.datasets[scale][
`${datasetPrefix}net_unrealized_profit_and_loss_to_market_cap_ratio`
],
seriesType: SeriesType.Based,
},
],
});
},
description: "",
},
],
},
{
name: "Supply",
tree: [
{
name: "Absolute",
tree: [
{
scale,
name: "All",
title: `${title} Profit And Loss`,
icon: () => IconTablerArrowsCross,
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "In Profit",
color: colors.profit,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_profit`
],
},
{
title: "In Loss",
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_loss`
],
},
{
title: "Total",
color: colors.white,
dataset: params.datasets[scale][`${datasetPrefix}supply`],
},
{
title: "Halved Total",
color: colors.gray,
dataset:
params.datasets[scale][`${datasetPrefix}halved_supply`],
options: {
lineStyle: 4,
},
},
],
});
},
},
{
scale,
name: `Total`,
title: `${title} Total supply`,
icon: () => IconTablerSum,
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
color,
seriesType: SeriesType.Area,
dataset: params.datasets[scale][`${datasetPrefix}supply`],
},
],
});
},
},
{
scale,
name: "In Profit",
title: `${title} Supply In Profit`,
icon: () => IconTablerTrendingUp,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
color: colors.profit,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_profit`
],
},
],
});
},
description: "",
},
{
scale,
name: "In Loss",
title: `${title} Supply In Loss`,
icon: () => IconTablerTrendingDown,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
color: colors.loss,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_loss`
],
},
],
});
},
description: "",
},
],
},
{
name: "Relative To Circulating",
tree: [
{
scale,
name: "All",
title: `${title} Profit And Loss Relative To Circulating Supply`,
icon: () => IconTablerArrowsCross,
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "In Profit",
color: colors.profit,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_profit_to_circulating_supply_ratio`
],
},
{
title: "In Loss",
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_loss_to_circulating_supply_ratio`
],
},
{
title: "100%",
color: colors.white,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_to_circulating_supply_ratio`
],
},
{
title: "50%",
color: colors.gray,
dataset:
params.datasets[scale][
`${datasetPrefix}halved_supply_to_circulating_supply_ratio`
],
options: {
lineStyle: 4,
},
},
],
});
},
},
{
scale,
name: `Total`,
title: `${title} Total supply Relative To Circulating Supply`,
icon: () => IconTablerSum,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
color,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_to_circulating_supply_ratio`
],
},
],
});
},
description: "",
},
{
scale,
name: "In Profit",
title: `${title} Supply In Profit Relative To Circulating Supply`,
icon: () => IconTablerTrendingUp,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
color: colors.profit,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_profit_to_circulating_supply_ratio`
],
},
],
});
},
description: "",
},
{
scale,
name: "In Loss",
title: `${title} Supply In Loss Relative To Circulating Supply`,
icon: () => IconTablerTrendingDown,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
seriesType: SeriesType.Area,
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_loss_to_circulating_supply_ratio`
],
},
],
});
},
description: "",
},
],
},
{
name: "Relative To Own",
tree: [
{
scale,
name: "All",
title: `${title} Supply In Profit And Loss Relative To Own Supply`,
icon: () => IconTablerArrowsCross,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "In profit",
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_profit_to_own_supply_ratio`
],
color: colors.profit,
},
{
title: "In loss",
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_loss_to_own_supply_ratio`
],
},
{
title: "100%",
color: colors.white,
dataset: params.datasets[scale][100],
options: {
lastValueVisible: false,
},
},
{
title: "50%",
color: colors.gray,
dataset: params.datasets[scale][50],
options: {
lineStyle: 4,
lastValueVisible: false,
},
},
],
});
},
description: "",
},
{
scale,
name: "In Profit",
title: `${title} Supply In Profit Relative To Own Supply`,
icon: () => IconTablerTrendingUp,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
color: colors.profit,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_profit_to_own_supply_ratio`
],
},
],
});
},
description: "",
},
{
scale,
name: "In Loss",
title: `${title} Supply In Loss Relative To Own Supply`,
icon: () => IconTablerTrendingDown,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
seriesType: SeriesType.Area,
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_loss_to_own_supply_ratio`
],
},
],
});
},
description: "",
},
],
},
// createMomentumPresetFolder({
// datasets: datasets[scale],
// scale,
// id: `${scale}-${id}-supply-in-profit-and-loss-percentage-self`,
// title: `${title} Supply In Profit And Loss (% Self)`,
// datasetKey: `${datasetKey}SupplyPNL%Self`,
// }),
],
},
{
name: "Prices Paid",
tree: [
{
scale,
name: `Average`,
title: `${title} Average Price Paid - Realized Price`,
icon: () => IconTablerMathAvg,
applyPreset(params) {
return applyMultipleSeries({
...params,
list: [
{
title: "Average",
color,
dataset:
params.datasets[scale][`${datasetPrefix}realized_price`],
},
],
});
},
description: "",
},
{
scale,
name: `Deciles`,
title: `${title} deciles`,
icon: () => IconTablerSquareHalf,
applyPreset(params) {
return applyMultipleSeries({
...params,
list: percentiles
.filter(({ value }) => Number(value) % 10 === 0)
.map(({ name, key }) => ({
dataset: params.datasets[scale][`${datasetPrefix}${key}`],
color,
title: name,
})),
});
},
description: "",
},
...percentiles.map(
(percentile): PartialPreset => ({
scale,
name: percentile.name,
title: `${title} ${percentile.title}`,
icon: () => IconTablerSquareHalf,
applyPreset(params) {
return applyMultipleSeries({
...params,
list: [
{
title: percentile.name,
color,
dataset:
params.datasets[scale][
`${datasetPrefix}${percentile.key}`
],
},
],
});
},
description: "",
}),
),
],
},
] satisfies PartialPresetTree;
}

View File

@@ -0,0 +1,121 @@
// import { PriceScaleMode } from "lightweight-charts";
// import {
// applyMultipleSeries,
// colors,
// PRICE_SCALE_MOMENTUM_ID,
// SeriesType,
// } from "/src/scripts";
// // type HeightMomentumKey =
// // | `${AnyPossibleCohortKey}SupplyPNL%Self`
// // | `${AnyPossibleCohortKey}RealizedPriceRatio`
// // | "activePriceRatio"
// // | "vaultedPriceRatio"
// // | "trueMarketMeanRatio";
// // type DateMomentumKey = HeightMomentumKey | `price${AverageName}MARatio`;
// export function createMomentumPresetFolder<
// Scale extends ResourceScale,
// Key extends string,
// >({
// datasets,
// scale,
// id,
// title,
// datasetKey,
// }: {
// datasets: Record<`${Key}${MomentumKey}`, Dataset<ResourceScale>>;
// scale: Scale;
// id: string;
// title: string;
// datasetKey: Key;
// }): PartialPresetFolder {
// return {
// id: `${scale}-${id}-momentum`,
// name: "Momentum",
// tree: [
// {
// id: `${scale}-${id}-momentum-value`,
// name: "Value",
// title: `${title} Momentum`,
// icon: () => IconTablerRollercoaster,
// applyPreset(params) {
// return applyMultipleSeries({
// scale,
// ...params,
// list: [
// {
// title: "Momentum",
// colors: colors.momentum,
// seriesType: SeriesType.Histogram,
// dataset: datasets[`${datasetKey}Momentum`],
// options: {
// priceScaleId: PRICE_SCALE_MOMENTUM_ID,
// lastValueVisible: false,
// },
// },
// ],
// });
// },
// description: "",
// },
// {
// id: `${scale}-${id}-momentum-buy-low-sell-high`,
// name: "BLSH - Buy Low Sell High",
// tree: [
// {
// id: `${scale}-${id}-buy-low-sell-high-bitcoin-returns`,
// name: "Bitcoin Returns",
// title: `${title} Momentum Based Buy Low Sell High Bitcoin Returns`,
// icon: () => IconTablerReceiptBitcoin,
// applyPreset(params) {
// return applyMultipleSeries({
// scale,
// ...params,
// priceScaleOptions: {
// halved: true,
// mode: PriceScaleMode.Percentage,
// },
// list: [
// {
// title: "Bitcoin Returns",
// dataset:
// datasets[`${datasetKey}MomentumBLSHBitcoinReturns`],
// color: colors.bitcoin,
// },
// ],
// });
// },
// description: "",
// },
// {
// id: `${scale}-${id}-momentum-buy-low-sell-high-dollar-returns`,
// name: "Dollar Returns",
// title: `${title} Momentum Based Buy Low Sell High Dollar Returns`,
// icon: () => IconTablerReceiptDollar,
// applyPreset(params) {
// return applyMultipleSeries({
// scale,
// ...params,
// priceScaleOptions: {
// halved: true,
// mode: PriceScaleMode.Percentage,
// },
// list: [
// {
// title: "Dollar Returns",
// dataset: datasets[`${datasetKey}MomentumBLSHDollarReturns`],
// color: colors.dollars,
// },
// ],
// });
// },
// description: "",
// },
// ],
// },
// ],
// };
// }

View File

@@ -0,0 +1,183 @@
import { applyPriceSeries } from "../../lightweightCharts/chart/price";
import { chartState } from "../../lightweightCharts/chart/state";
import { setTimeScale } from "../../lightweightCharts/chart/time";
import { createAreaSeries } from "../../lightweightCharts/series/creators/area";
import {
createBaseLineSeries,
DEFAULT_BASELINE_COLORS,
} from "../../lightweightCharts/series/creators/baseLine";
import { createHistogramSeries } from "../../lightweightCharts/series/creators/histogram";
import { createSeriesLegend } from "../../lightweightCharts/series/creators/legend";
import { createLineSeries } from "../../lightweightCharts/series/creators/line";
import { resetRightPriceScale } from "../../lightweightCharts/series/options/priceScale";
import { stringToId } from "../../utils/id";
export enum SeriesType {
Normal,
Based,
Area,
Histogram,
}
export function applyMultipleSeries<
Scale extends ResourceScale,
DS extends Dataset<Scale> & Partial<ResourceDataset<Scale>>,
>({
chart,
list = [],
preset,
priceScaleOptions,
datasets,
priceDataset,
priceOptions,
activeResources,
}: {
chart: IChartApi;
preset: Preset;
priceDataset?: DS;
priceOptions?: PriceSeriesOptions;
priceScaleOptions?: FullPriceScaleOptions;
list?: (
| {
dataset: DS;
color?: string;
colors?: undefined;
seriesType: SeriesType.Based;
title: string;
options?: BaselineSeriesOptions;
defaultVisible?: boolean;
}
| {
dataset: DS;
color?: string;
colors?: string[];
seriesType: SeriesType.Histogram;
title: string;
options?: DeepPartialHistogramOptions;
defaultVisible?: boolean;
}
| {
dataset: DS;
color: string;
colors?: undefined;
seriesType?: SeriesType.Normal | SeriesType.Area;
title: string;
options?: DeepPartialLineOptions;
defaultVisible?: boolean;
}
)[];
datasets: Datasets;
activeResources: Accessor<Set<ResourceDataset<any, any>>>;
}): PresetLegend {
const { halved } = priceScaleOptions || {};
const price = applyPriceSeries({
chart,
datasets,
preset,
dataset: priceDataset,
activeResources,
options: {
...priceOptions,
halved,
},
});
const legendList: PresetLegend = [price.lineLegend, price.ohlcLegend];
const isAnyArea = list.find(
(config) => config.seriesType === SeriesType.Area,
);
const rightPriceScaleOptions = resetRightPriceScale(chart, {
...priceScaleOptions,
...(isAnyArea
? {
scaleMargins: {
bottom: 0,
},
}
: {}),
});
[...list]
.reverse()
.forEach(
({
dataset,
color,
colors,
seriesType: type,
title,
options,
defaultVisible,
}) => {
let series: ISeriesApi<"Baseline" | "Line" | "Area" | "Histogram">;
if (type === SeriesType.Based) {
series = createBaseLineSeries(chart, {
color,
...options,
});
} else if (type === SeriesType.Area) {
series = createAreaSeries(chart, {
color,
autoscaleInfoProvider: (getInfo: () => AutoscaleInfo | null) => {
const info = getInfo();
if (info) {
info.priceRange.minValue = 0;
}
return info;
},
...options,
});
} else if (type === SeriesType.Histogram) {
series = createHistogramSeries(chart, {
color,
...options,
});
} else {
series = createLineSeries(chart, {
color,
...options,
});
}
legendList.splice(
0,
0,
createSeriesLegend({
id: stringToId(title),
presetId: preset.id,
title,
series,
color: () => colors || color || DEFAULT_BASELINE_COLORS,
defaultVisible,
url: dataset.url,
}),
);
createEffect(() => {
series.setData(dataset?.values() || []);
setTimeScale(chartState.range);
});
},
);
createEffect(() => {
const options = {
scaleMargins: {
top:
price.lineLegend.visible() || price.ohlcLegend.visible()
? rightPriceScaleOptions.scaleMargins.top
: rightPriceScaleOptions.scaleMargins.bottom,
bottom: rightPriceScaleOptions.scaleMargins.bottom,
},
};
chart.priceScale("right").applyOptions(options);
});
return legendList;
}

View File

@@ -0,0 +1,289 @@
// import {
// applyMultipleSeries,
// colors,
// createMomentumPresetFolder,
// SeriesType,
// } from "/src/scripts";
// // type HeightRatioKey =
// // | `${AnyPossibleCohortKey}RealizedPrice`
// // | "activePrice"
// // | "vaultedPrice"
// // | "trueMarketMean";
// // // type DateRatioKey = HeightRatioKey;
// // type DateRatioKey = HeightRatioKey | `price${AverageName}MA`;
// export function createRatioPresetFolder<
// Scale extends ResourceScale,
// Key extends string,
// >({
// datasets,
// scale,
// id,
// title,
// datasetKey,
// color,
// }: {
// datasets: Record<`${Key}${RatioKey}`, Dataset<ResourceScale>>;
// scale: Scale;
// id: string;
// title: string;
// color: string;
// datasetKey: Key;
// }): PartialPresetFolder {
// return {
// id: `${scale}-${id}-ratio`,
// name: "Ratio",
// tree: [
// {
// id: `${scale}-${id}-ratio-value`,
// name: `Value`,
// title: `Bitcoin Price to ${title} Ratio`,
// icon: () => IconTablerDivide,
// applyPreset(params) {
// return applyMultipleSeries({
// scale,
// ...params,
// priceScaleOptions: {
// halved: true,
// },
// list: [
// {
// title: "Ratio",
// seriesType: SeriesType.Based,
// dataset: datasets[`${datasetKey}Ratio`],
// options: {
// base: 1,
// },
// },
// ],
// });
// },
// description: "",
// },
// {
// id: `${scale}-${id}-ratio-1y-average`,
// name: "Averages",
// tree: [
// {
// id: `${scale}-${id}-ratio-averages`,
// name: `7 Day VS. 1 Year`,
// title: `Bitcoin Price to ${title} Ratio Moving Averages`,
// icon: () => IconTablerSwords,
// applyPreset(params) {
// return applyMultipleSeries({
// scale,
// ...params,
// priceScaleOptions: {
// halved: true,
// },
// list: [
// {
// title: "Ratio",
// seriesType: SeriesType.Based,
// color: colors.gray,
// dataset: datasets[`${datasetKey}Ratio`],
// options: {
// base: 1,
// },
// },
// {
// title: "7 Day Moving Average",
// color: colors.closes7DMA,
// dataset: datasets[`${datasetKey}Ratio7DayMovingAverage`],
// },
// {
// title: "1 Year Moving Average",
// color: colors.closes1YMA,
// dataset: datasets[`${datasetKey}Ratio1YearMovingAverage`],
// },
// ],
// });
// },
// description: "",
// },
// createMomentumPresetFolder({
// datasets,
// scale,
// id: `${scale}-${id}-ratio-averages`,
// title: `${title} Ratio Moving Averages`,
// datasetKey: `${datasetKey}Ratio`,
// }),
// ],
// },
// {
// id: `${scale}-${id}-ratio-extremes`,
// name: "Extremes",
// tree: [
// {
// id: `${scale}-${id}-extreme-top-ratios`,
// name: "Top Ratios",
// description: "",
// icon: () => IconTablerJetpack,
// title: `${title} Extreme Top Ratios`,
// applyPreset(params) {
// return applyMultipleSeries({
// scale,
// ...params,
// priceScaleOptions: {
// halved: true,
// },
// list: [
// {
// id: "ratio",
// title: "Ratio",
// color: colors.white,
// seriesType: SeriesType.Based,
// dataset: datasets[`${datasetKey}Ratio`],
// options: {
// base: 1,
// options: {
// baseLineColor: color,
// baseLineVisible: true,
// },
// },
// },
// {
// id: "99.9-percentile",
// title: "99.9th Percentile",
// dataset: datasets[`${datasetKey}Ratio99.9Percentile`],
// color: colors.extremeMax,
// },
// {
// id: "99.5-percentile",
// title: "99.5th Percentile",
// color: colors.extremeMiddle,
// dataset: datasets[`${datasetKey}Ratio99.5Percentile`],
// },
// {
// id: "99-percentile",
// title: "99th Percentile",
// color: colors.extremeMin,
// dataset: datasets[`${datasetKey}Ratio99Percentile`],
// },
// ],
// });
// },
// },
// {
// id: `${scale}-${id}-extreme-bottom-ratios`,
// name: "Bottom Ratios",
// description: "",
// icon: () => IconTablerScubaMask,
// title: `${title} Extreme Bottom Ratios`,
// applyPreset(params) {
// return applyMultipleSeries({
// scale,
// ...params,
// priceScaleOptions: {
// halved: true,
// },
// list: [
// {
// id: "ratio",
// title: "Ratio",
// color: colors.white,
// seriesType: SeriesType.Based,
// dataset: datasets[`${datasetKey}Ratio`],
// options: {
// base: 1,
// options: {
// baseLineColor: color,
// baseLineVisible: true,
// },
// },
// },
// {
// id: "1-percentile",
// title: "1st Percentile",
// color: colors.extremeMin,
// dataset: datasets[`${datasetKey}Ratio1Percentile`],
// },
// {
// id: "0.5-percentile",
// title: "0.5th Percentile",
// color: colors.extremeMiddle,
// dataset: datasets[`${datasetKey}Ratio0.5Percentile`],
// },
// {
// id: "0.1-percentile",
// title: "0.1th Percentile",
// color: colors.extremeMax,
// dataset: datasets[`${datasetKey}Ratio0.1Percentile`],
// },
// ],
// });
// },
// },
// {
// id: `${scale}-${id}-extreme-top-prices`,
// name: "Top Prices",
// description: "",
// icon: () => IconTablerRocket,
// title: `${title} Extreme Top Prices`,
// applyPreset(params) {
// return applyMultipleSeries({
// scale,
// ...params,
// list: [
// {
// id: "99.9-percentile",
// title: "99.9th Percentile",
// color: colors.extremeMax,
// dataset: datasets[`${datasetKey}Ratio99.9Price`],
// },
// {
// id: "99.5-percentile",
// title: "99.5th Percentile",
// color: colors.extremeMiddle,
// dataset: datasets[`${datasetKey}Ratio99.5Price`],
// },
// {
// id: "99-percentile",
// title: "99th Percentile",
// color: colors.extremeMin,
// dataset: datasets[`${datasetKey}Ratio99Price`],
// },
// ],
// });
// },
// },
// {
// id: `${scale}-${id}-extreme-bottom-prices`,
// name: "Bottom Prices",
// description: "",
// icon: () => IconTablerSubmarine,
// title: `${title} Extreme Bottom Prices`,
// applyPreset(params) {
// return applyMultipleSeries({
// scale,
// ...params,
// list: [
// {
// id: "1-percentile",
// title: "1st Percentile",
// color: colors.extremeMin,
// dataset: datasets[`${datasetKey}Ratio1Price`],
// },
// {
// id: "0.5-percentile",
// title: "0.5th Percentile",
// color: colors.extremeMiddle,
// dataset: datasets[`${datasetKey}Ratio0.5Price`],
// },
// {
// id: "0.1-percentile",
// title: "0.1th Percentile",
// color: colors.extremeMax,
// dataset: datasets[`${datasetKey}Ratio0.1Price`],
// },
// ],
// });
// },
// },
// ],
// },
// ],
// };
// }

View File

@@ -0,0 +1,225 @@
import { colors } from "../../utils/colors";
import { applyMultipleSeries } from "../templates/multiple";
export function createPresets(scale: ResourceScale) {
return {
name: "Transactions",
tree: [
{
scale,
icon: IconTablerHandThreeFingers,
name: "Count",
title: "Transaction Count",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "1M SMA",
color: colors.momentumYellow,
dataset: params.datasets[scale].transaction_count_1m_sma,
},
{
title: "1W SMA",
color: colors.bitcoin,
dataset: params.datasets[scale].transaction_count_1w_sma,
},
{
title: "Raw",
color: colors.darkBitcoin,
dataset: params.datasets[scale].transaction_count,
},
],
});
},
},
{
name: "Volume",
tree: [
{
scale,
icon: IconTablerCoinBitcoin,
name: "In Bitcoin",
title: "Transaction Volume",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "1M SMA",
color: colors.momentumYellow,
dataset: params.datasets[scale].transaction_volume_1m_sma,
},
{
title: "1W SMA",
color: colors.bitcoin,
dataset: params.datasets[scale].transaction_volume_1w_sma,
},
{
title: "Raw",
color: colors.darkBitcoin,
dataset: params.datasets[scale].transaction_volume,
},
],
});
},
},
{
scale,
icon: IconTablerCoin,
name: "In Dollars",
title: "Transaction Volume In Dollars",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
mode: 1,
},
list: [
{
title: "1M SMA",
color: colors.lightDollars,
dataset:
params.datasets[scale]
.transaction_volume_in_dollars_1m_sma,
},
{
title: "1W SMA",
color: colors.dollars,
dataset:
params.datasets[scale]
.transaction_volume_in_dollars_1w_sma,
},
{
title: "Raw",
color: colors.darkDollars,
dataset:
params.datasets[scale].transaction_volume_in_dollars,
},
],
});
},
},
],
},
{
name: "Annualized Volume",
tree: [
{
scale,
icon: IconTablerCoinBitcoin,
name: "In Bitcoin",
title: "Annualized Transaction Volume",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Volume",
color: colors.bitcoin,
dataset:
params.datasets[scale].annualized_transaction_volume,
},
],
});
},
},
{
scale,
icon: IconTablerCoin,
name: "In Dollars",
title: "Annualized Transaction Volume In Dollars",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Volume",
color: colors.dollars,
dataset:
params.datasets[scale]
.annualized_transaction_volume_in_dollars,
},
],
});
},
},
],
},
{
scale,
icon: IconTablerWind,
name: "Velocity",
title: "Transactions Velocity",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Transactions Velocity",
color: colors.bitcoin,
dataset: params.datasets[scale].transaction_velocity,
},
],
});
},
},
{
scale,
icon: IconTablerAlarm,
name: "Per Second",
title: "Transactions Per Second",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "1M SMA",
color: colors.lightBitcoin,
dataset: params.datasets[scale].transactions_per_second_1m_sma,
},
{
title: "1W SMA",
color: colors.bitcoin,
dataset: params.datasets[scale].transactions_per_second_1w_sma,
},
{
title: "Raw",
color: colors.darkBitcoin,
dataset: params.datasets[scale].transactions_per_second,
},
],
});
},
},
],
} satisfies PartialPresetFolder;
}

62
app/src/scripts/presets/types.d.ts vendored Normal file
View File

@@ -0,0 +1,62 @@
interface PartialPreset {
scale: ResourceScale;
icon?: () => JSXElement;
name: string;
title: string;
applyPreset: ApplyPreset;
description: string;
}
interface Preset extends PartialPreset {
id: string;
path: FilePath;
isFavorite: RWS<boolean>;
visited: RWS<boolean>;
}
type FilePath = {
id: string;
name: string;
}[];
type ApplyPreset = (params: {
chart: IChartApi;
datasets: Datasets;
preset: Preset;
activeResources: Accessor<Set<ResourceDataset<any, any>>>;
}) => ApplyPresetReturn;
type ApplyPresetReturn = PresetLegend;
interface PartialPresetFolder {
name: string;
tree: PartialPresetTree;
}
interface PresetFolder extends PartialPresetFolder {
id: string;
tree: PresetTree;
}
type PartialPresetTree = (PartialPreset | PartialPresetFolder)[];
type PresetTree = (Preset | PresetFolder)[];
// type PresetList = Preset[];
// type FavoritePresets = Accessor<Preset[]>;
type PresetsHistory = { date: Date; preset: Preset }[];
type PresetsHistorySignal = RWS<PresetsHistory>;
type SerializedPresetsHistory = { p: string; d: number }[];
interface Presets {
tree: (Preset | PresetFolder)[];
list: Preset[];
favorites: Accessor<Preset[]>;
history: PresetsHistorySignal;
selected: RWS<Preset>;
openedFolders: RWS<Set<string>>;
select(preset: Preset): void;
}
type PresetLegend = SeriesLegend[];

View File

@@ -0,0 +1,16 @@
export function sortedInsert(array: number[], value: number) {
let low = 0;
let high = array.length;
while (low < high) {
const mid = (low + high) >>> 1;
if (array[mid] < value) {
low = mid + 1;
} else {
high = mid;
}
}
array.splice(low, 0, value);
}

View File

@@ -0,0 +1,246 @@
import {
amber as amberTailwind,
blue as blueTailwind,
cyan as cyanTailwind,
emerald as emeraldTailwind,
fuchsia as fuchsiaTailwind,
neutral as grayTailwind,
green as greenTailwind,
indigo as indigoTailwind,
lime as limeTailwind,
orange as orangeTailwind,
pink as pinkTailwind,
purple as purpleTailwind,
red as redTailwind,
rose as roseTailwind,
sky as skyTailwind,
teal as tealTailwind,
violet as violetTailwind,
yellow as yellowTailwind,
} from "tailwindcss/colors";
// ---
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// ---
const lightRed = redTailwind[300];
const red = redTailwind[500];
const darkRed = redTailwind[900];
const orange = orangeTailwind[500];
const darkOrange = orangeTailwind[900];
const amber = amberTailwind[500];
const darkAmber = amberTailwind[900];
const yellow = yellowTailwind[500];
const darkYellow = yellowTailwind[500];
const lime = limeTailwind[500];
const darkLime = limeTailwind[900];
const green = greenTailwind[500];
const darkGreen = greenTailwind[900];
const lightEmerald = emeraldTailwind[300];
const emerald = emeraldTailwind[500];
const darkEmerald = emeraldTailwind[900];
const teal = tealTailwind[500];
const darkTeal = tealTailwind[900];
const cyan = cyanTailwind[500];
const darkCyan = cyanTailwind[900];
const sky = skyTailwind[500];
const darkSky = skyTailwind[900];
const blue = blueTailwind[500];
const darkBlue = blueTailwind[900];
const indigo = indigoTailwind[500];
const darkIndigo = indigoTailwind[900];
const violet = violetTailwind[500];
const darkViolet = violetTailwind[900];
const purple = purpleTailwind[500];
const darkPurple = purpleTailwind[900];
const fuchsia = fuchsiaTailwind[500];
const darkFuchsia = fuchsiaTailwind[900];
const pink = pinkTailwind[500];
const darkPink = pinkTailwind[900];
const rose = roseTailwind[500];
const darkRose = roseTailwind[900];
const darkWhite = grayTailwind[400];
const gray = grayTailwind[600];
const black = "#000000";
const white = "#ffffff";
export const convertCandleToCandleColor = (
candle: { close: number; open: number },
inverse?: boolean,
) =>
(candle.close || 1) > (candle.open || 0)
? !inverse
? green
: red
: !inverse
? red
: green;
export const convertCandleToVolumeColor = (
candle: { close: number; open: number },
inverse?: boolean,
) =>
(candle.close || 1) > (candle.open || 0)
? !inverse
? darkGreen
: darkRed
: !inverse
? darkRed
: darkGreen;
export const colors = {
white,
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,
extremeMax: red,
extremeMiddle: orange,
extremeMin: 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,
};

View File

@@ -0,0 +1,10 @@
// import { ONE_DAY_IN_MS } from "./time";
import { ONE_DAY_IN_MS } from "./time";
export const dateToString = (date: Date) => date.toJSON().split("T")[0];
// export const FIVE_MONTHS_IN_DAYS = 30 * 5;
export const getNumberOfDaysBetweenTwoDates = (oldest: Date, youngest: Date) =>
Math.round(Math.abs((youngest.valueOf() - oldest.valueOf()) / ONE_DAY_IN_MS));

View File

@@ -0,0 +1,19 @@
export const debounce = <F extends (...args: any[]) => any>(
callback: F,
wait = 250,
) => {
let timeoutId: number | undefined;
let latestArgs: Parameters<F>;
return (...args: Parameters<F>) => {
latestArgs = args;
if (!timeoutId) {
timeoutId = window.setTimeout(async () => {
await callback(...latestArgs);
timeoutId = undefined;
}, wait);
}
};
};

View File

@@ -0,0 +1,12 @@
export function replaceHistory({
urlParams,
pathname,
}: {
urlParams?: URLSearchParams;
pathname?: string;
}) {
urlParams ||= new URLSearchParams(window.location.search);
pathname ||= window.location.pathname;
window.history.replaceState(null, "", `${pathname}?${urlParams.toString()}`);
}

View File

@@ -0,0 +1,3 @@
export function stringToId(s: string) {
return s.replace(/\W/g, " ").trim().replace(/ +/g, "-").toLowerCase();
}

View File

@@ -0,0 +1,31 @@
export const priceToUSLocale = (price: number, compact = true) => {
const absolutePrice = Math.abs(price);
const lessThan100 = absolutePrice < 100;
const lessThan1000 = absolutePrice < 1_000;
const biggerThanMillion = absolutePrice >= 1_000_000;
return numberToUSLocale(
price,
lessThan1000 ? (lessThan100 ? 2 : 1) : biggerThanMillion ? 3 : 0,
biggerThanMillion && compact
? {
notation: "compact",
compactDisplay: "short",
}
: undefined,
);
};
export const percentageToUSLocale = (percentage: number) =>
numberToUSLocale(percentage, 1);
const numberToUSLocale = (
value: number,
digits: number,
options?: Intl.NumberFormatOptions | undefined,
) =>
value.toLocaleString("en-us", {
...options,
minimumFractionDigits: digits,
maximumFractionDigits: digits,
});

View File

@@ -0,0 +1,22 @@
import { computeSum } from "./sum";
export const computeAverage = (values: number[]) =>
computeSum(values) / values.length;
export const computeMovingAverage = <
T extends SingleValueData = SingleValueData,
>(
dataset: T[],
interval: number,
) => {
if (!dataset.length) return [];
return dataset.map((data, index) => ({
...data,
value: computeAverage(
dataset
.slice(Math.max(index - interval + 1, 0), index + 1)
.map((data) => data.value || 1),
),
}));
};

View File

@@ -0,0 +1,5 @@
export function random<T>(array: T[]) {
if (array && array.length) {
return array[Math.floor(Math.random() * array.length)];
}
}

View File

@@ -0,0 +1,4 @@
export const roundValue = (value: number, decimals = 5) => {
const tenPowerX = 10 ** decimals;
return Math.round(value * tenPowerX) / tenPowerX;
};

View File

@@ -0,0 +1,2 @@
export const computeSum = (values: number[]) =>
values.reduce((total, currentValue) => total + currentValue, 0);

View File

@@ -0,0 +1 @@
export const run = <T>(f: () => T) => f();

View File

@@ -0,0 +1,9 @@
export const scrollIntoView = (
element?: HTMLElement | Element | null,
block: ScrollLogicalPosition = "nearest",
behavior: ScrollBehavior = "instant",
) =>
element?.scrollIntoView({
block,
behavior,
});

View File

@@ -0,0 +1,119 @@
import { createRWS } from "/src/solid/rws";
export const createSelectableList = <T, L extends T[] = T[]>(
list: L,
parameters?: {
selected?: L[number];
selectedIndex?: number | null;
},
) => {
const selected = createRWS<L[number] | null>(null);
const selectedIndex = createRWS<number | null>(null);
const selectableList: SelectableList<L[number], L> = {
selected,
selectedIndex,
list: createRWS(list, {
equals: false,
}),
select(s) {
if (this.selected() !== s) {
batch(() => {
selected.set(() => s);
this.selectIndex(this.list().indexOf(s) ?? null);
});
}
},
resetSelected() {
selected.set(null);
selectedIndex.set(null);
},
selectFind(search, callback) {
const element = this.list().find(
(_element) => callback(_element) === search,
);
if (element) {
this.select(element);
}
return element;
},
selectIndex(i) {
i = i === -1 ? null : i;
if (i && (i < 0 || i >= this.list().length)) {
throw new Error(
`SelectableList: selectIndex: ${i} is incorrect ! (has ${
this.list().length
} elements)`,
);
}
if (i !== this.selectedIndex()) {
selectedIndex.set(i);
const value = getValueFromIndexInList<L[number]>(i, this.list());
if (value !== null) {
this.select(value);
}
}
},
push(value) {
this.list.set((l) => {
l.push(value);
return l;
});
},
pushAndSelect(value) {
batch(() => {
this.push(value);
this.select(value);
});
},
removeIndex(index) {
let value = null;
this.list.set((l) => {
value = l.splice(index, 1)?.[0];
return l;
});
return value;
},
toJSON<TJSON, LJSON extends TJSON[] = TJSON[]>(
transform: (value: T) => TJSON,
filter?: (value: T) => boolean,
): JSONSelectableList<TJSON, LJSON> {
return {
version: 1,
selectedIndex: getIndexOfSelectedInSelectableList(this),
list: (filter ? this.list().filter(filter) : this.list()).map((value) =>
transform(value),
) as LJSON,
};
},
};
if (parameters?.selected !== undefined) {
selectableList.select(parameters.selected);
} else if (parameters?.selectedIndex !== undefined) {
selectableList.selectIndex(parameters.selectedIndex);
}
return selectableList;
};
export const createSL = createSelectableList;
export const getIndexOfSelectedInSelectableList = <T, L extends T[] = T[]>(
sl: SelectableList<L[number], L>,
) => {
const selected = sl.selected();
return selected ? sl.list().indexOf(selected) : null;
};
const getValueFromIndexInList = <T, L extends T[] = T[]>(
index: number | null,
list: L,
) => (index !== null && list.length > 0 ? list.at(index) || list[0] : null);

View File

@@ -0,0 +1,33 @@
// ---
// JSON
// ---
interface JSONSelectableList<T, L extends T[] = T[]> {
readonly version: 1;
selectedIndex: number | null;
readonly list: L;
}
// ---
// Object
// ---
interface SelectableList<T, L extends T[] = T[]> {
readonly selected: Accessor<T | null>;
readonly selectedIndex: Accessor<number | null>;
readonly list: RWS<L>;
readonly select: <S extends L[number] = L[number]>(s: S) => void;
readonly selectFind: <K>(
search: K,
callback: (element: T) => K,
) => T | undefined;
readonly selectIndex: (i: number | null) => void;
readonly push: <S extends L[number] = L[number]>(s: S) => void;
readonly pushAndSelect: <S extends L[number] = L[number]>(s: S) => void;
readonly removeIndex: (i: number) => L[number] | null;
readonly resetSelected: VoidFunction;
readonly toJSON: <TJSON, LJSON extends TJSON[] = TJSON[]>(
transform: (value: T) => LJSON[number],
filter?: (value: T) => boolean,
) => JSONSelectableList<TJSON, LJSON>;
}

View File

@@ -0,0 +1,9 @@
export function sleep(ms: number) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
export function tick() {
return sleep(1);
}

View File

@@ -0,0 +1,19 @@
export function saveToStorage(key?: string, value?: string | boolean) {
if (key) {
value !== undefined && value !== null
? localStorage.setItem(key, String(value))
: localStorage.removeItem(key);
}
}
export function readBooleanFromStorage(key: string) {
const saved = localStorage.getItem(key);
if (saved) {
return isSerializedBooleanTrue(saved);
}
return null;
}
export function isSerializedBooleanTrue(serialized: string) {
return serialized === "true" || serialized === "1";
}

View File

@@ -0,0 +1,8 @@
export const ONE_SECOND_IN_MS = 1_000;
export const FIVE_SECOND_IN_MS = 5 * ONE_SECOND_IN_MS;
export const TEN_SECOND_IN_MS = 2 * FIVE_SECOND_IN_MS;
export const ONE_MINUTE_IN_MS = 6 * TEN_SECOND_IN_MS;
export const FIVE_MINUTES_IN_MS = 5 * ONE_MINUTE_IN_MS;
export const TEN_MINUTES_IN_MS = 2 * FIVE_MINUTES_IN_MS;
export const ONE_HOUR_IN_MS = 6 * TEN_MINUTES_IN_MS;
export const ONE_DAY_IN_MS = 24 * ONE_HOUR_IN_MS;

View File

@@ -0,0 +1,40 @@
import { replaceHistory } from "./history";
import { isSerializedBooleanTrue } from "./storage";
const whitelist = ["from", "to"];
export function resetURLParams() {
const urlParams = new URLSearchParams();
[...new URLSearchParams(window.location.search).entries()]
.filter(([key, _]) => whitelist.includes(key))
.forEach(([key, value]) => {
urlParams.set(key, value);
});
replaceHistory({ urlParams });
}
export function writeURLParam(key: string, value?: string | boolean) {
const urlParams = new URLSearchParams(window.location.search);
if (value !== undefined) {
urlParams.set(key, String(value));
} else {
urlParams.delete(key);
}
replaceHistory({ urlParams });
}
export function readBooleanURLParam(key: string) {
const urlParams = new URLSearchParams(window.location.search);
const parameter = urlParams.get(key);
if (parameter) {
return isSerializedBooleanTrue(parameter);
}
return null;
}

View File

@@ -0,0 +1,62 @@
import { makeEventListener } from "@solid-primitives/event-listener";
import { createRWS } from "/src/solid/rws";
export const createResourceWS = <T>(
creator: (callback: (value: T) => void) => WebSocket,
) => {
let ws: WebSocket | null = null;
const live = createRWS(false);
const latest = createRWS<T | null>(null);
let clearFocusListener: VoidFunction | undefined;
let clearOnlineListener: VoidFunction | undefined;
const resource: WebsocketResource<T> = {
live,
latest,
open() {
ws = creator((value) => latest.set(() => value));
ws.addEventListener("open", () => {
console.log("ws: open");
live.set(true);
});
ws.addEventListener("close", () => {
console.log("ws: close");
live.set(false);
});
const reinitWebSocket = () => {
if (!ws || ws.readyState === ws.CLOSED) {
console.log("ws: reinit");
resource.open();
}
};
clearFocusListener = makeEventListener(
document,
"visibilitychange",
() => !document.hidden && reinitWebSocket(),
);
clearOnlineListener = makeEventListener(
window,
"online",
reinitWebSocket,
);
},
close() {
ws?.close();
clearFocusListener = clearFocusListener?.() || undefined;
clearOnlineListener = clearOnlineListener?.() || undefined;
live.set(false);
ws = null;
},
};
return resource;
};

View File

@@ -0,0 +1,10 @@
import { createResourceWS } from "./base";
import { krakenAPI } from "./kraken";
export const webSockets = {
liveKrakenCandle: createResourceWS(krakenAPI.createLiveCandleWebsocket),
openAll() {
this.liveKrakenCandle.open();
onCleanup(this.liveKrakenCandle.close);
},
};

View File

@@ -0,0 +1,49 @@
import { dateToString } from "../utils/date";
import { ONE_DAY_IN_MS } from "../utils/time";
export const krakenAPI = {
createLiveCandleWebsocket(
callback: (candle: DatasetCandlestickData) => void,
) {
const ws = new WebSocket("wss://ws.kraken.com");
ws.addEventListener("open", () => {
ws.send(
JSON.stringify({
event: "subscribe",
pair: ["XBT/USD"],
subscription: {
name: "ohlc",
interval: 1440,
},
}),
);
});
ws.addEventListener("message", (message) => {
const result = JSON.parse(message.data);
if (!Array.isArray(result)) return;
const [timestamp, _, open, high, low, close, __, volume] = result[1];
const dateStr = dateToString(new Date(Number(timestamp) * 1000));
const candle: DatasetCandlestickData = {
// date: dateStr,
number: new Date(dateStr).valueOf() / ONE_DAY_IN_MS,
time: dateStr,
open: Number(open),
high: Number(high),
low: Number(low),
close: Number(close),
value: Number(close),
// volume: Number(volume),
};
candle && callback({ ...candle });
});
return ws;
},
};

6
app/src/scripts/ws/types.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
interface WebsocketResource<T> {
live: Accessor<boolean>;
latest: Accessor<T | null>;
open: VoidFunction;
close: VoidFunction;
}