mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-25 23:29:58 -07:00
git: reset
This commit is contained in:
30
app/src/scripts/datasets/consts/address.ts
Normal file
30
app/src/scripts/datasets/consts/address.ts
Normal 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;
|
||||
147
app/src/scripts/datasets/consts/age.ts
Normal file
147
app/src/scripts/datasets/consts/age.ts
Normal 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;
|
||||
15
app/src/scripts/datasets/consts/averages.ts
Normal file
15
app/src/scripts/datasets/consts/averages.ts
Normal 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;
|
||||
11
app/src/scripts/datasets/consts/liquidities.ts
Normal file
11
app/src/scripts/datasets/consts/liquidities.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const liquidities = [
|
||||
{
|
||||
key: "illiquid",
|
||||
name: "Illiquid",
|
||||
},
|
||||
{ key: "liquid", name: "Liquid" },
|
||||
{
|
||||
key: "highly_liquid",
|
||||
name: "Highly Liquid",
|
||||
},
|
||||
] as const;
|
||||
116
app/src/scripts/datasets/consts/percentiles.ts
Normal file
116
app/src/scripts/datasets/consts/percentiles.ts
Normal 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;
|
||||
14
app/src/scripts/datasets/consts/returns.ts
Normal file
14
app/src/scripts/datasets/consts/returns.ts
Normal 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;
|
||||
19
app/src/scripts/datasets/consts/types.d.ts
vendored
Normal file
19
app/src/scripts/datasets/consts/types.d.ts
vendored
Normal 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"];
|
||||
41
app/src/scripts/datasets/date.ts
Normal file
41
app/src/scripts/datasets/date.ts
Normal 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;
|
||||
}
|
||||
36
app/src/scripts/datasets/height.ts
Normal file
36
app/src/scripts/datasets/height.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
17
app/src/scripts/datasets/index.ts
Normal file
17
app/src/scripts/datasets/index.ts
Normal 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>;
|
||||
}
|
||||
246
app/src/scripts/datasets/resource.ts
Normal file
246
app/src/scripts/datasets/resource.ts
Normal 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
98
app/src/scripts/datasets/types.d.ts
vendored
Normal 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;
|
||||
}
|
||||
11
app/src/scripts/lightweightCharts/chart/clean.ts
Normal file
11
app/src/scripts/lightweightCharts/chart/clean.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { chartState } from "./state";
|
||||
|
||||
export function cleanChart() {
|
||||
console.log("chart: clean");
|
||||
|
||||
try {
|
||||
chartState.chart?.remove();
|
||||
} catch {}
|
||||
|
||||
chartState.chart = null;
|
||||
}
|
||||
68
app/src/scripts/lightweightCharts/chart/create.ts
Normal file
68
app/src/scripts/lightweightCharts/chart/create.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
89
app/src/scripts/lightweightCharts/chart/horzScaleBehavior.ts
Normal file
89
app/src/scripts/lightweightCharts/chart/horzScaleBehavior.ts
Normal 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;
|
||||
}
|
||||
123
app/src/scripts/lightweightCharts/chart/markers.ts
Normal file
123
app/src/scripts/lightweightCharts/chart/markers.ts
Normal 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);
|
||||
}
|
||||
176
app/src/scripts/lightweightCharts/chart/price.ts
Normal file
176
app/src/scripts/lightweightCharts/chart/price.ts
Normal 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;
|
||||
}
|
||||
40
app/src/scripts/lightweightCharts/chart/render.ts
Normal file
40
app/src/scripts/lightweightCharts/chart/render.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
10
app/src/scripts/lightweightCharts/chart/state.ts
Normal file
10
app/src/scripts/lightweightCharts/chart/state.ts
Normal 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(),
|
||||
};
|
||||
110
app/src/scripts/lightweightCharts/chart/time.ts
Normal file
110
app/src/scripts/lightweightCharts/chart/time.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
9
app/src/scripts/lightweightCharts/chart/types.d.ts
vendored
Normal file
9
app/src/scripts/lightweightCharts/chart/types.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
interface PriceSeriesOptions {
|
||||
halved?: boolean;
|
||||
title?: string;
|
||||
id?: string;
|
||||
lowerOpacity?: boolean;
|
||||
inverseColors?: boolean;
|
||||
seriesOptions?: DeepPartial<SeriesOptionsCommon>;
|
||||
priceScaleOptions?: DeepPartial<PriceScaleOptions>;
|
||||
}
|
||||
50
app/src/scripts/lightweightCharts/chart/whitespace.ts
Normal file
50
app/src/scripts/lightweightCharts/chart/whitespace.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
28
app/src/scripts/lightweightCharts/series/creators/area.ts
Normal file
28
app/src/scripts/lightweightCharts/series/creators/area.ts
Normal 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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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]];
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
75
app/src/scripts/lightweightCharts/series/creators/legend.ts
Normal file
75
app/src/scripts/lightweightCharts/series/creators/legend.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
10
app/src/scripts/lightweightCharts/series/creators/line.ts
Normal file
10
app/src/scripts/lightweightCharts/series/creators/line.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defaultSeriesOptions } from "./options";
|
||||
|
||||
export const createLineSeries = (
|
||||
chart: IChartApi,
|
||||
options?: DeepPartialLineOptions,
|
||||
) =>
|
||||
chart.addLineSeries({
|
||||
...defaultSeriesOptions,
|
||||
...options,
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
export const defaultSeriesOptions: DeepPartial<SeriesOptionsCommon> = {
|
||||
// @ts-ignore
|
||||
lineWidth: 1.5,
|
||||
priceLineVisible: false,
|
||||
baseLineVisible: false,
|
||||
baseLineColor: "",
|
||||
};
|
||||
13
app/src/scripts/lightweightCharts/series/creators/types.d.ts
vendored
Normal file
13
app/src/scripts/lightweightCharts/series/creators/types.d.ts
vendored
Normal 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>;
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
3
app/src/scripts/lightweightCharts/series/options/types.d.ts
vendored
Normal file
3
app/src/scripts/lightweightCharts/series/options/types.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
interface FullPriceScaleOptions extends DeepPartial<PriceScaleOptions> {
|
||||
halved?: boolean;
|
||||
}
|
||||
218
app/src/scripts/presets/addresses/index.ts
Normal file
218
app/src/scripts/presets/addresses/index.ts
Normal 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: "",
|
||||
};
|
||||
}
|
||||
221
app/src/scripts/presets/blocks/index.ts
Normal file
221
app/src/scripts/presets/blocks/index.ts
Normal 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;
|
||||
}
|
||||
1032
app/src/scripts/presets/coinblocks/index.ts
Normal file
1032
app/src/scripts/presets/coinblocks/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
127
app/src/scripts/presets/hodlers/index.ts
Normal file
127
app/src/scripts/presets/hodlers/index.ts
Normal 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;
|
||||
}
|
||||
296
app/src/scripts/presets/index.ts
Normal file
296
app/src/scripts/presets/index.ts
Normal 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) }] : [];
|
||||
});
|
||||
}
|
||||
78
app/src/scripts/presets/market/averages/index.ts
Normal file
78
app/src/scripts/presets/market/averages/index.ts
Normal 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;
|
||||
}
|
||||
77
app/src/scripts/presets/market/index.ts
Normal file
77
app/src/scripts/presets/market/index.ts
Normal 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;
|
||||
}
|
||||
6
app/src/scripts/presets/market/indicators/index.ts
Normal file
6
app/src/scripts/presets/market/indicators/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export function createPresets(datasets: Datasets) {
|
||||
return {
|
||||
name: "Indicators",
|
||||
tree: [],
|
||||
} satisfies PartialPresetFolder;
|
||||
}
|
||||
79
app/src/scripts/presets/market/returns/index.ts
Normal file
79
app/src/scripts/presets/market/returns/index.ts
Normal 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`],
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
902
app/src/scripts/presets/miners/index.ts
Normal file
902
app/src/scripts/presets/miners/index.ts
Normal 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;
|
||||
}
|
||||
985
app/src/scripts/presets/templates/cohort.ts
Normal file
985
app/src/scripts/presets/templates/cohort.ts
Normal 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;
|
||||
}
|
||||
121
app/src/scripts/presets/templates/momentum.ts
Normal file
121
app/src/scripts/presets/templates/momentum.ts
Normal 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: "",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// };
|
||||
// }
|
||||
183
app/src/scripts/presets/templates/multiple.ts
Normal file
183
app/src/scripts/presets/templates/multiple.ts
Normal 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;
|
||||
}
|
||||
289
app/src/scripts/presets/templates/ratio.ts
Normal file
289
app/src/scripts/presets/templates/ratio.ts
Normal 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`],
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// };
|
||||
// }
|
||||
225
app/src/scripts/presets/transactions/index.ts
Normal file
225
app/src/scripts/presets/transactions/index.ts
Normal 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
62
app/src/scripts/presets/types.d.ts
vendored
Normal 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[];
|
||||
16
app/src/scripts/utils/array.ts
Normal file
16
app/src/scripts/utils/array.ts
Normal 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);
|
||||
}
|
||||
246
app/src/scripts/utils/colors.ts
Normal file
246
app/src/scripts/utils/colors.ts
Normal 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,
|
||||
};
|
||||
10
app/src/scripts/utils/date.ts
Normal file
10
app/src/scripts/utils/date.ts
Normal 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));
|
||||
19
app/src/scripts/utils/debounce.ts
Normal file
19
app/src/scripts/utils/debounce.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
};
|
||||
12
app/src/scripts/utils/history.ts
Normal file
12
app/src/scripts/utils/history.ts
Normal 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()}`);
|
||||
}
|
||||
3
app/src/scripts/utils/id.ts
Normal file
3
app/src/scripts/utils/id.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function stringToId(s: string) {
|
||||
return s.replace(/\W/g, " ").trim().replace(/ +/g, "-").toLowerCase();
|
||||
}
|
||||
31
app/src/scripts/utils/locale.ts
Normal file
31
app/src/scripts/utils/locale.ts
Normal 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,
|
||||
});
|
||||
22
app/src/scripts/utils/math/averages.ts
Normal file
22
app/src/scripts/utils/math/averages.ts
Normal 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),
|
||||
),
|
||||
}));
|
||||
};
|
||||
5
app/src/scripts/utils/math/random.ts
Normal file
5
app/src/scripts/utils/math/random.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function random<T>(array: T[]) {
|
||||
if (array && array.length) {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
}
|
||||
4
app/src/scripts/utils/math/round.ts
Normal file
4
app/src/scripts/utils/math/round.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const roundValue = (value: number, decimals = 5) => {
|
||||
const tenPowerX = 10 ** decimals;
|
||||
return Math.round(value * tenPowerX) / tenPowerX;
|
||||
};
|
||||
2
app/src/scripts/utils/math/sum.ts
Normal file
2
app/src/scripts/utils/math/sum.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const computeSum = (values: number[]) =>
|
||||
values.reduce((total, currentValue) => total + currentValue, 0);
|
||||
1
app/src/scripts/utils/run.ts
Normal file
1
app/src/scripts/utils/run.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const run = <T>(f: () => T) => f();
|
||||
9
app/src/scripts/utils/scroll.ts
Normal file
9
app/src/scripts/utils/scroll.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const scrollIntoView = (
|
||||
element?: HTMLElement | Element | null,
|
||||
block: ScrollLogicalPosition = "nearest",
|
||||
behavior: ScrollBehavior = "instant",
|
||||
) =>
|
||||
element?.scrollIntoView({
|
||||
block,
|
||||
behavior,
|
||||
});
|
||||
119
app/src/scripts/utils/selectableList/index.ts
Normal file
119
app/src/scripts/utils/selectableList/index.ts
Normal 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);
|
||||
33
app/src/scripts/utils/selectableList/types.d.ts
vendored
Normal file
33
app/src/scripts/utils/selectableList/types.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
9
app/src/scripts/utils/sleep.ts
Normal file
9
app/src/scripts/utils/sleep.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
export function tick() {
|
||||
return sleep(1);
|
||||
}
|
||||
19
app/src/scripts/utils/storage.ts
Normal file
19
app/src/scripts/utils/storage.ts
Normal 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";
|
||||
}
|
||||
8
app/src/scripts/utils/time.ts
Normal file
8
app/src/scripts/utils/time.ts
Normal 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;
|
||||
40
app/src/scripts/utils/urlParams.ts
Normal file
40
app/src/scripts/utils/urlParams.ts
Normal 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;
|
||||
}
|
||||
62
app/src/scripts/ws/base.ts
Normal file
62
app/src/scripts/ws/base.ts
Normal 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;
|
||||
};
|
||||
10
app/src/scripts/ws/index.ts
Normal file
10
app/src/scripts/ws/index.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
49
app/src/scripts/ws/kraken.ts
Normal file
49
app/src/scripts/ws/kraken.ts
Normal 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
6
app/src/scripts/ws/types.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
interface WebsocketResource<T> {
|
||||
live: Accessor<boolean>;
|
||||
latest: Accessor<T | null>;
|
||||
open: VoidFunction;
|
||||
close: VoidFunction;
|
||||
}
|
||||
Reference in New Issue
Block a user