Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions src/features/cx/components/CXVolumeShare.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<script setup lang="ts">
import { computed, ComputedRef, PropType } from "vue";

import { useI18n } from "vue-i18n";
const { t } = useI18n();

// Util
import { formatNumber } from "@/util/numbers";

// UI
import { PTooltip } from "@/ui";
import { WarningAmberOutlined } from "@vicons/material";

// Types & Interfaces
import {
ICXVolumeShare,
ICXVolumeWindow,
} from "@/features/cx/cxVolumeShare.types";

const props = defineProps({
share: {
type: Object as PropType<ICXVolumeShare | undefined>,
required: false,
default: undefined,
},
});

const localShare: ComputedRef<ICXVolumeShare | undefined> = computed(
() => props.share
);

// the 7d share carries the label whatever coloured the row: it is the
// market as it stands now
const localLabel: ComputedRef<string> = computed(() => {
const share: ICXVolumeShare | undefined = localShare.value;

if (!share) return "";

if (share.illiquid)
return t("cx_volume.illiquid", { exchange: share.exchange });

if (share.window7d.share === undefined)
return t("cx_volume.share_no_volume", { exchange: share.exchange });

return t("cx_volume.share", {
percent: formatNumber(share.window7d.share * 100, 1),
exchange: share.exchange,
});
});

const localColorClass: ComputedRef<string> = computed(() => {
switch (localShare.value?.level) {
case "red":
return "text-negative";
case "yellow":
return "text-amber-400";
default:
return "text-white/40";
}
});

function windowLine(
window: ICXVolumeWindow,
exchange: string,
label: string
): string {
if (window.share === undefined)
return t("cx_volume.tooltip_window_empty", {
window: label,
exchange,
});

return t("cx_volume.tooltip_window", {
window: label,
exchange,
traded: formatNumber(window.sumTraded, 0),
perDay: formatNumber(window.sumTraded / window.days, 1),
percent: formatNumber(window.share * 100, 1),
});
}

const localTooltipLines: ComputedRef<string[]> = computed(() => {
const share: ICXVolumeShare | undefined = localShare.value;

if (!share) return [];

const lines: string[] = [
t("cx_volume.tooltip_intro", {
units: formatNumber(share.soldPerDay),
ticker: share.ticker,
exchange: share.exchange,
}),
];

if (share.illiquid) {
lines.push(
t("cx_volume.tooltip_illiquid", {
exchange: share.exchange,
ticker: share.ticker,
traded: formatNumber(share.window7d.sumTraded, 0),
})
);
} else {
lines.push(
windowLine(share.window7d, share.exchange, t("terms.7d")),
windowLine(share.window30d, share.exchange, t("terms.30d"))
);
}

return lines;
});
</script>

<template>
<PTooltip v-if="localShare">
<template #trigger>
<div
class="text-xs hover:cursor-help flex flex-row gap-x-1 items-center"
:class="localColorClass">
<WarningAmberOutlined
v-if="localShare.level !== 'none'"
class="w-3.5 h-3.5 shrink-0" />
<span>{{ localLabel }}</span>
</div>
</template>
<div class="flex flex-col gap-y-1 max-w-100">
<div v-for="(line, index) in localTooltipLines" :key="index">
{{ line }}
</div>
</div>
</PTooltip>
</template>
165 changes: 165 additions & 0 deletions src/features/cx/cxVolumeShare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Types & Interfaces
import { EXCHANGES_TYPE } from "@/database/services/useExchangeData.types";
import {
CX_VOLUME_LEVEL,
ICXVolumeShare,
ICXVolumeThresholds,
ICXVolumeWindow,
} from "@/features/cx/cxVolumeShare.types";

/** Share of daily traded volume, in percent, that turns a row yellow */
export const CX_VOLUME_YELLOW_PERCENT: number = 5;
/** Share of daily traded volume, in percent, that turns a row red */
export const CX_VOLUME_RED_PERCENT: number = 15;

/**
* Units traded over 7 days at or below which the exchange counts as
* illiquid: any sale worth mentioning moves a market this thin, so it is
* warned about directly instead of through a share whose denominator is
* meaningless — or zero — down here
*/
export const CX_VOLUME_ILLIQUID_7D: number = 10;

/** Units per week below which a sale never warns at all */
export const CX_VOLUME_MIN_WEEKLY_SOLD: number = 1;

/** Every exchange the game data carries a traded volume for */
export const CX_VOLUME_EXCHANGES: EXCHANGES_TYPE[] = [
"AI1",
"CI1",
"IC1",
"NC1",
"UNIVERSE",
];

/**
* Calculates one traded volume window measured against a daily sale
* @author raukk
*
* @export
* @param {number} soldPerDay Units per day sold to the exchange
* @param {number} sumTraded Units traded over the whole window
* @param {number} days Length of the window in days
* @returns {ICXVolumeWindow} Window with its share
*/
export function volumeWindow(
soldPerDay: number,
sumTraded: number,
days: number
): ICXVolumeWindow {
const perDay: number = sumTraded / days;

return {
sumTraded,
days,
share: perDay > 0 ? soldPerDay / perDay : undefined,
};
}

/**
* Severity a single share carries on its own
* @author raukk
*
* @export
* @param {number | undefined} share Share of daily traded volume
* @param {ICXVolumeThresholds} thresholds Colour thresholds in percent
* @returns {CX_VOLUME_LEVEL} Severity
*/
export function levelOfShare(
share: number | undefined,
thresholds: ICXVolumeThresholds
): CX_VOLUME_LEVEL {
if (share === undefined) return "none";

const percent: number = share * 100;

if (percent >= thresholds.redPercent) return "red";
if (percent >= thresholds.yellowPercent) return "yellow";

return "none";
}

/**
* The worse of two severities
* @author raukk
*
* @export
* @param {CX_VOLUME_LEVEL} a First severity
* @param {CX_VOLUME_LEVEL} b Second severity
* @returns {CX_VOLUME_LEVEL} Worse of the two
*/
export function worstLevel(
a: CX_VOLUME_LEVEL,
b: CX_VOLUME_LEVEL
): CX_VOLUME_LEVEL {
if (a === "red" || b === "red") return "red";
if (a === "yellow" || b === "yellow") return "yellow";

return "none";
}

/** Traded sums of one ticker on the exchange the sale lands on */
export interface ICXVolumeSums {
sumTraded7d: number;
sumTraded30d: number;
}

/**
* Calculates a material row's pressure on the exchange it is sold at.
* Both windows are computed and the worse of the two colours the row:
* 7d catches the market as it stands, 30d catches a ticker that has
* just gone quiet and whose 7d window is no longer representative
* @author raukk
*
* @export
* @param {string} ticker Material ticker
* @param {EXCHANGES_TYPE} exchange Exchange the row is sold at
* @param {number} soldPerDay Units per day reaching that exchange
* @param {ICXVolumeSums} sums Traded sums of the ticker
* @param {ICXVolumeThresholds} thresholds Colour thresholds in percent
* @returns {ICXVolumeShare} Shares and severity of the row
*/
export function calculateCXVolumeShare(
ticker: string,
exchange: EXCHANGES_TYPE,
soldPerDay: number,
sums: ICXVolumeSums,
thresholds: ICXVolumeThresholds
): ICXVolumeShare {
const window7d: ICXVolumeWindow = volumeWindow(
soldPerDay,
sums.sumTraded7d,
7
);
const window30d: ICXVolumeWindow = volumeWindow(
soldPerDay,
sums.sumTraded30d,
30
);

// too small a sale to warn about, whatever the exchange looks like
const worthWarning: boolean =
soldPerDay > 0 && soldPerDay * 7 >= CX_VOLUME_MIN_WEEKLY_SOLD;

const illiquid: boolean =
worthWarning && sums.sumTraded7d < CX_VOLUME_ILLIQUID_7D;

const level: CX_VOLUME_LEVEL = !worthWarning
? "none"
: illiquid
? "red"
: worstLevel(
levelOfShare(window7d.share, thresholds),
levelOfShare(window30d.share, thresholds)
);

return {
ticker,
exchange,
soldPerDay,
window7d,
window30d,
illiquid,
level,
};
}
36 changes: 36 additions & 0 deletions src/features/cx/cxVolumeShare.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { EXCHANGES_TYPE } from "@/database/services/useExchangeData.types";

/** Severity of a row's share of the exchange's traded volume */
export type CX_VOLUME_LEVEL = "none" | "yellow" | "red";

/** Shares of daily traded volume, in percent, that colour a row */
export interface ICXVolumeThresholds {
yellowPercent: number;
redPercent: number;
}

/** One traded volume window of an exchange, measured against a sale */
export interface ICXVolumeWindow {
sumTraded: number;
days: number;
/** soldPerDay / (sumTraded / days), undefined while nothing trades */
share: number | undefined;
}

/** A single material row's pressure on the exchange it is sold at */
export interface ICXVolumeShare {
ticker: string;
exchange: EXCHANGES_TYPE;
soldPerDay: number;
window7d: ICXVolumeWindow;
window30d: ICXVolumeWindow;
/** Exchange trades too little for any share to be meaningful */
illiquid: boolean;
level: CX_VOLUME_LEVEL;
}

/** Input of a volume share calculation, one per material row */
export interface ICXVolumeRow {
ticker: string;
soldPerDay: number;
}
Loading