Skip to content

Commit

Permalink
Merge pull request #863 from tradingstrategy-ai/862-fix-profit-format…
Browse files Browse the repository at this point in the history
…ting

Fix profit / price change formatting issues
  • Loading branch information
kenkunz authored Dec 16, 2024
2 parents f54d1dc + 2ed1a5e commit b6f4d95
Show file tree
Hide file tree
Showing 39 changed files with 561 additions and 520 deletions.
20 changes: 10 additions & 10 deletions src/lib/chart/ChartIQ.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ Dynamically ChartIQ modules (if available) and render chart element.
import * as studyModules from './studies';
import './chart.css';
export type ChartCursor = {
position: {
cx?: number;
cy?: number;
DateX?: number;
CloseY?: number;
};
data?: any;
};
/**
* NOTE: normal dynamic import doesn't work for optional dependency due to Vite's
* pre-bundling import analysis; using Vite's custom import.meta.glob instead.
Expand Down Expand Up @@ -73,16 +83,6 @@ Dynamically ChartIQ modules (if available) and render chart element.
let updating = false;
interface ChartCursor {
position: {
cx?: number;
cy?: number;
DateX?: number;
CloseY?: number;
};
data?: any;
}
let cursor: ChartCursor = { position: {} };
// Svelte 5's behavior for action updates differs from Svelte 4. Action updates are triggered
Expand Down
75 changes: 75 additions & 0 deletions src/lib/chart/ChartTooltip.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<script lang="ts">
import type { ChartCursor } from './ChartIQ.svelte';
import Marker from './Marker.svelte';
import Timestamp from '$lib/components/Timestamp.svelte';
type Props = {
cursor: ChartCursor;
withTime?: boolean;
formatValue: Formatter<MaybeNumber>;
};
let { cursor, withTime, formatValue }: Props = $props();
let { position, data } = $derived(cursor);
let direction = $derived(Math.sign(data?.Close - data?.iqPrevClose || 0));
let directionClass = $derived(direction === 0 ? 'neutral' : direction > 0 ? 'bullish' : 'bearish');
</script>

{#if data}
<Marker x={position.DateX} y={position.CloseY} size={4.5} />

<div class="chart-tooltip" style:--x="{position.cx}px" style:--y="{position.CloseY}px">
<div class="content {directionClass}">
<div class="timestamp">
<Timestamp date={data.adjustedDate} {withTime} />
</div>
<div class="value">
{formatValue(data.Close, 2)}
</div>
</div>
</div>
{/if}

<style>
.chart-tooltip {
position: absolute;
left: var(--x);
top: var(--y);
transform: translate(-50%, calc(-100% - var(--space-md)));
/* opaque (no transparency) variant of chart container background color */
--background-opaque: color-mix(in srgb, var(--c-body), hsl(var(--hsl-box)) var(--box-1-alpha));
/* semi-opaque base background color (allows chart lines to partially bleed through) */
--background-base: color-mix(in srgb, transparent, var(--background-opaque) 75%);
.content {
display: grid;
gap: 0.25rem;
border-radius: var(--radius-sm);
padding: 0.5rem 0.75rem;
text-align: right;
&.neutral {
color: var(--c-text);
background: color-mix(in srgb, var(--background-base), var(--c-box-4));
}
&:is(.bullish, .bearish) {
background: color-mix(in srgb, var(--background-base), currentColor 15%);
}
}
.timestamp {
font: var(--f-ui-sm-medium);
letter-spacing: var(--f-ui-sm-spacing);
color: var(--c-text-extra-light);
}
.value {
font: var(--f-ui-md-medium);
letter-spacing: var(--f-ui-md-spacing);
}
}
</style>
12 changes: 6 additions & 6 deletions src/lib/chart/PairCandleChart.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ Display trading pair candles (ohlc+v) charts, with attached quoteFeed for chart
import type { ChartLinker, QuoteFeed, TimeBucket } from '$lib/chart';
import { timeBucketToPeriodicity, ChartIQ, HudRow, HudMetric } from '$lib/chart';
import { Alert } from '$lib/components';
import { formatDollar, formatPriceChange, formatTokenAmount } from '$lib/helpers/formatters';
import { formatTokenAmount } from '$lib/helpers/formatters';
import { getProfitInfo } from '$lib/components/Profitability.svelte';
export let feed: QuoteFeed;
export let pairId: number | string;
Expand Down Expand Up @@ -102,18 +103,17 @@ Display trading pair candles (ohlc+v) charts, with attached quoteFeed for chart
let:cursor
>
{#if cursor.data}
{@const priceChangeAmt = cursor.data.Close - cursor.data.Open}
{@const priceChangePct = priceChangeAmt / cursor.data.Open}
{@const direction = Math.sign(priceChangeAmt)}
{@const priceDiff = cursor.data.Close - cursor.data.Open}
{@const { direction, ...priceChange } = getProfitInfo(priceDiff / cursor.data.Open)}

<div class="pair-candle-chart-hud">
<HudRow>
<HudMetric label="O" value={formatForHud(cursor.data.Open)} {direction} />
<HudMetric label="H" value={formatForHud(cursor.data.High)} {direction} />
<HudMetric label="L" value={formatForHud(cursor.data.Low)} {direction} />
<HudMetric label="C" value={formatForHud(cursor.data.Close)} {direction} />
<HudMetric value={formatForHud(priceChangeAmt)} {direction} />
<HudMetric value={formatPriceChange(priceChangePct)} {direction} />
<HudMetric value={formatForHud(priceDiff)} {direction} />
<HudMetric value={priceChange.toString()} {direction} />
</HudRow>

<slot name="hud-row-volume" {cursor} formatter={formatForHud} />
Expand Down
42 changes: 8 additions & 34 deletions src/lib/chart/PerformanceChart.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ Display a peformance line chart for a given (static) dataset.
<script lang="ts">
import type { Quote } from '$lib/chart';
import { differenceInCalendarDays } from 'date-fns';
import { ChartIQ, Marker } from '$lib/chart';
import { Timestamp, UpDownCell } from '$lib/components';
import { determinePriceChangeClass } from '$lib/helpers/price';
import ChartIQ from '$lib/chart/ChartIQ.svelte';
import ChartTooltip from './ChartTooltip.svelte';
import { relativeProfitability } from 'trade-executor/helpers/profit';
import { type ProfitInfo, getProfitInfo } from '$lib/components/Profitability.svelte';
import { merge } from '$lib/helpers/object';
type Props = {
Expand All @@ -32,7 +32,7 @@ Display a peformance line chart for a given (static) dataset.
studies?: any[];
invalidate?: any[];
init?: (arg: any) => () => void;
onPeriodPerformanceChange?: (value: MaybeNumber) => void;
onPeriodPerformanceChange?: (value: ProfitInfo) => void;
};
let {
Expand Down Expand Up @@ -98,13 +98,12 @@ Display a peformance line chart for a given (static) dataset.
initialValue = 0;
}
const direction = determinePriceChangeClass(last?.Value - initialValue);
const periodPerformance = relativeProfitability(initialValue, last?.Value);
const periodPerformance = getProfitInfo(relativeProfitability(initialValue, last?.Value));
// NOTE: setting attribute selector on HTML element rather than declaratively via
// Svelte template; needed to prevent race condition / ensure colors update correctly.
if (chartWrapper.dataset.direction !== direction) {
chartWrapper.dataset.direction = direction;
if (chartWrapper.dataset.direction !== periodPerformance.directionClass) {
chartWrapper.dataset.direction = periodPerformance.directionClass;
chartEngine.clearStyles();
}
Expand Down Expand Up @@ -147,16 +146,7 @@ Display a peformance line chart for a given (static) dataset.
invalidate={[data, periodicity, hideYAxis, ...invalidate]}
let:cursor
>
{@const { position, data } = cursor}
{#if data}
<Marker x={position.DateX} y={position.CloseY} size={4.5} />
<div class="chart-hover-info" style:--x="{position.cx}px" style:--y="{position.CloseY}px">
<UpDownCell value={data.Close - data.iqPrevClose}>
<Timestamp date={data.adjustedDate} withTime={periodicity.timeUnit === 'hour'} />
<div class="value">{formatValue(data.Close, 2)}</div>
</UpDownCell>
</div>
{/if}
<ChartTooltip {cursor} withTime={periodicity.timeUnit === 'hour'} {formatValue} />
</ChartIQ>
</div>

Expand All @@ -173,21 +163,5 @@ Display a peformance line chart for a given (static) dataset.
--chart-aspect-ratio: 1.25;
}
}
.chart-hover-info {
position: absolute;
left: var(--x);
top: var(--y);
transform: translate(-50%, calc(-100% - var(--space-md)));
:global(time) {
color: var(--c-text-extra-light);
}
.value {
font: var(--f-ui-md-medium);
letter-spacing: var(--f-ui-md-spacing);
}
}
}
</style>
124 changes: 124 additions & 0 deletions src/lib/components/Profitability.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<!--
@component
Display profit/loss or price change value, formatted as percent, with color class
and ▼ ◼︎ ▲ direction markers.
The `boxed` prop displays the value with padding and a background color.
The module also exports a `getProfitInfo` utility function for scenarios where
using the component isn't practical.
@example
```svelte
<Profitability of={profitValue} />
<Profitability of={profitValue} boxed>
{someOtherValue} with profit/loss color coding
</Profitability>
<Profitability of={profitValue}>
{#snippet children(profitInfo, getLabel)}
{profitInfo}
<span class="custom-label">
{getLabel('down', 'neutral', 'up')}
</span>
{/snippet}
</Profitability>
```
-->
<script module lang="ts">
import { toFloatingPoint, isNumber, notFilledMarker } from '$lib/helpers/formatters';
export type ProfitInfo = ReturnType<typeof getProfitInfo>;
/**
* Get information used to display profit/loss or price change values.
*
* This encapsulates the core display logic of the Profitability component as
* a utility function for scenarios where using the component isn't practical.
*
* The returned object provides a `getLabel` method, which accepts a tuple of
* of ('down', 'neutral', 'up') labels and returns the appropriate option.
*
* The object also includes a `toString` method, enabling it to be used
* directly in template or string interpolation contexts.
*
* @param n decimal representation profit or price change value
*/
export function getProfitInfo(n: MaybeNumberlike) {
const value = toFloatingPoint(n);
const formatted = formatProfitability(value);
const direction = getDirection(value, formatted);
const getLabel = (...labels: string[]) => labels[direction + 1];
const marker = getLabel('', '◼︎', '');
const directionClass = getLabel('bearish', 'neutral', 'bullish');
const toString = () => (value === undefined ? notFilledMarker : `${marker} ${formatted}`);
return { value, formatted, direction, marker, directionClass, getLabel, toString };
}
function formatProfitability(value: number | undefined) {
if (!isNumber(value)) return;
return value.toLocaleString('en-us', {
minimumFractionDigits: 1,
maximumFractionDigits: Math.abs(value) < 0.001 ? 2 : 1,
style: 'percent',
signDisplay: 'never'
});
}
function getDirection(value: number | undefined, formatted: string | undefined) {
if (!value || formatted === '0.0%') return 0;
return Math.sign(value);
}
</script>

<script lang="ts">
import type { Snippet } from 'svelte';
type Props = {
of: MaybeNumberlike;
boxed?: boolean;
class?: string;
children?: Snippet<[ProfitInfo, ProfitInfo['getLabel']]>;
};
let { of, boxed = false, class: classes, children }: Props = $props();
let profitInfo = $derived(getProfitInfo(of));
</script>

<span class="profitability {profitInfo.directionClass} {classes}" class:boxed class:default={!children}>
{#if children}
{@render children?.(profitInfo, profitInfo.getLabel)}
{:else}
{profitInfo}
{/if}
</span>

<style>
.profitability {
&.default {
white-space: nowrap;
}
&.boxed {
border-radius: var(--radius-sm);
padding: 0.5em 0.75em;
&.neutral {
background: var(--c-box-2);
--background-hover: var(--c-box-4);
}
&:is(.bullish, .bearish) {
background: color-mix(in srgb, transparent, currentColor 12%);
--background-hover: color-mix(in srgb, transparent, currentColor 24%);
}
}
}
</style>
Loading

0 comments on commit b6f4d95

Please sign in to comment.