Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix performance chart color edge case bug #859

Merged
merged 5 commits into from
Dec 5, 2024
Merged
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
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ TS_PUBLIC_TYPESENSE_API_URL=https://typesense2.tradingstrategy.ai
TS_PUBLIC_TYPESENSE_API_KEY=npdPPJNELDhdr7v6IS9rQUpFG2VvdyAL
TS_PUBLIC_DISCORD_URL=https://discord.gg/5M88m9nM8H
TS_PUBLIC_WALLET_CONNECT_PROJECT_ID=9ee7efad98897eb60ba023db6aa72355
TS_PUBLIC_STRATEGIES='[{"id":"enzyme-polygon-matic-eth-usdc","name":"ETH-MATIC-USDC momentum","url":"https://enzyme-polygon-matic-eth-usdc.tradingstrategy.ai","frontpage":true},{"id":"enzyme-arbitrum-eth-btc-rsi","name":"ETH-BTC price surge (Arbitrum)","url":"https://enzyme-arbitrum-eth-btc-rsi.tradingstrategy.ai/","frontpage":true},{"id":"enzyme-polygon-eth-rolling-ratio","name":"ETH/BTC rolling ratio","url":"https://enzyme-polygon-eth-rolling-ratio.tradingstrategy.ai/","frontpage":true},{"id":"enzyme-polygon-eth-btc-rsi","name":"ETH-BTC price surge","url":"https://enzyme-polygon-eth-btc-rsi.tradingstrategy.ai/","frontpage":true,"hiddenPositions":[4]},{"id":"enzyme-ethereum-btc-eth-stoch-rsi","name":"Stochastic ETH/BTC long","url":"https://enzyme-ethereum-btc-eth-stoch-rsi.tradingstrategy.ai","frontpage":true},{"id":"enzyme-polygon-eth-btc-usdc","name":"ETH-BTC-USDC momentum","url":"https://enzyme-polygon-eth-btc-usdc.tradingstrategy.ai","new_version_id":"enzyme-polygon-eth-btc-rsi","frontpage":true},{"id":"enzyme-polygon-matic-usdc","name":"MATIC breakout","url":"https://enzyme-polygon-matic-usdc.tradingstrategy.ai"},{"id":"enzyme-polygon-eth-breakout","name":"ETH breakout","url":"https://enzyme-polygon-eth-breakout.tradingstrategy.ai"},{"id":"enzyme-polygon-eth-usdc","name":"ETH Breakout bounce","url":"https://enzyme-polygon-eth-usdc.tradingstrategy.ai"},{"id":"enzyme-polygon-eth-usdc-sls","name":"ETH Balance snap","url":"https://enzyme-polygon-eth-usdc-sls.tradingstrategy.ai"},{"id":"polygon-eth-spot-short","name":"ETH mean reversion bounce","url":"https://polygon-eth-spot-short.tradingstrategy.ai"},{"id":"arbitrum-btc-breakout","name":"BTC Barrier Breach","url":"https://arbitrum-btc-breakout.tradingstrategy.ai"}]'
TS_PUBLIC_STRATEGIES='[{"id":"enzyme-polygon-matic-eth-usdc","name":"ETH-MATIC-USDC momentum","url":"https://enzyme-polygon-matic-eth-usdc.tradingstrategy.ai","frontpage":true},{"id":"enzyme-arbitrum-eth-btc-rsi","name":"ETH-BTC long swing","url":"https://enzyme-arbitrum-eth-btc-rsi.tradingstrategy.ai/","frontpage":true},{"id":"enzyme-polygon-eth-rolling-ratio","name":"ETH/BTC rolling ratio","url":"https://enzyme-polygon-eth-rolling-ratio.tradingstrategy.ai/","frontpage":true},{"id":"enzyme-ethereum-btc-eth-stoch-rsi","name":"Stochastic ETH-BTC","url":"https://enzyme-ethereum-btc-eth-stoch-rsi.tradingstrategy.ai","frontpage":true},{"id":"enzyme-polygon-matic-usdc","name":"MATIC Level crush","url":"https://enzyme-polygon-matic-usdc.tradingstrategy.ai"},{"id":"enzyme-polygon-eth-breakout","name":"ETH breakout","url":"https://enzyme-polygon-eth-breakout.tradingstrategy.ai"},{"id":"enzyme-polygon-eth-usdc","name":"ETH Breakout bounce","url":"https://enzyme-polygon-eth-usdc.tradingstrategy.ai"},{"id":"enzyme-polygon-eth-usdc-sls","name":"ETH Balance snap","url":"https://enzyme-polygon-eth-usdc-sls.tradingstrategy.ai"},{"id":"polygon-eth-spot-short","name":"ETH Mean Flip","url":"https://polygon-eth-spot-short.tradingstrategy.ai"},{"id":"arbitrum-btc-breakout","name":"BTC Barrier Breach","url":"https://arbitrum-btc-breakout.tradingstrategy.ai"},{"id":"ethereum-memecoin-swing","name":"ETH Memecoin Social Trend Spotter","url":"https://ethereum-memecoin-swing.tradingstrategy.ai"},{"id":"ethereum-memecoin-vol-basket","name":"Memecoin volatility index","url":"https://ethereum-memecoin-vol-basket.tradingstrategy.ai"},{"id":"enzyme-polygon-eth-btc-rsi","name":"ETH-BTC price surge","url":"https://enzyme-polygon-eth-btc-rsi.tradingstrategy.ai/","newVersionId":"enzyme-arbitrum-eth-btc-rsi","hiddenPositions":[4],"frontpage":true},{"id":"enzyme-polygon-eth-btc-usdc","name":"ETH-BTC-USDC momentum","url":"https://enzyme-polygon-eth-btc-usdc.tradingstrategy.ai","newVersionId":"enzyme-polygon-eth-btc-rsi","frontpage":true}]'
TS_PUBLIC_GEO_BLOCK='{"strategies:view":["CU","IR","KP","RU","SY"],"strategies:deposit":["CU","IR","KP","RU","SY","US","UK"]}'
# Uncomment to test chain maintenance error
# TS_PUBLIC_CHAINS_UNDER_MAINTENANCE='{ "binance": "BNB Chain" }'
Expand Down
93 changes: 55 additions & 38 deletions src/lib/chart/PerformanceChart.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,51 +15,57 @@ Display a peformance line chart for a given (static) dataset.
```
-->
<script lang="ts">
import type { Quote, Periodicity } from '$lib/chart';
import { createEventDispatcher } from 'svelte';
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 { relativeProfitability } from 'trade-executor/helpers/profit';
import { merge } from '$lib/helpers/object';

export let loading = false;
export let data: Quote[] = [];
export let options: any = undefined;
export let formatValue: Formatter<MaybeNumber>;
export let spanDays: MaybeNumber;
export let studies: any[] = [];
let initCallback: Function | undefined = undefined;
export { initCallback as init };
export let invalidate: any[] = [];

const dispatch = createEventDispatcher<{
change: {
first: Maybe<Quote>;
last: Maybe<Quote>;
firstTickPosition: number;
};
}>();
type Props = {
loading?: boolean;
data?: Quote[];
options?: object;
formatValue: Formatter<MaybeNumber>;
spanDays: MaybeNumber;
studies?: any[];
invalidate?: any[];
init?: (arg: any) => () => void;
onPeriodPerformanceChange?: (value: MaybeNumber) => void;
};

let {
loading = false,
data = [],
options,
formatValue,
spanDays,
studies = [],
invalidate = [],
init: initCallback,
onPeriodPerformanceChange
}: Props = $props();

let chartWrapper: HTMLElement;

let viewportWidth: number;
$: hideYAxis = viewportWidth <= 576;
let viewportWidth = $state() as number;
let hideYAxis = $derived(viewportWidth <= 576);

// if spanDays is not set, assume "max" (full data range)
$: if (spanDays === undefined) {
// set displayDays to spanDays if set; otherwise max data range
let displayDays = $derived.by(() => {
if (spanDays) return spanDays;
const start = data[0]?.DT;
spanDays = start ? differenceInCalendarDays(new Date(), start) : 0;
}

$: periodicity = getPeriodicity(spanDays!);

function getPeriodicity(days: number): Periodicity {
if (days <= 7) return { period: 1, interval: 1, timeUnit: 'hour' };
if (days <= 30) return { period: 4, interval: 1, timeUnit: 'hour' };
if (days <= 365) return { period: 1, interval: 1, timeUnit: 'day' };
return start ? differenceInCalendarDays(new Date(), start) : 0;
});

// dynamically set periodicity based on number of days displayed
let periodicity = $derived.by(() => {
if (displayDays <= 7) return { period: 1, interval: 1, timeUnit: 'hour' };
if (displayDays <= 30) return { period: 4, interval: 1, timeUnit: 'hour' };
if (displayDays <= 365) return { period: 1, interval: 1, timeUnit: 'day' };
return { period: 7, interval: 1, timeUnit: 'day' };
}
});

const defaultOptions = {
layout: { chartType: 'mountain' },
Expand All @@ -79,19 +85,30 @@ Display a peformance line chart for a given (static) dataset.
// update chart colors based on change in value (+/-) for visible data set
chartEngine.append('createDataSegment', () => {
const dataSegment = chartEngine.getDataSegment();
const first = chartEngine.getFirstLastDataRecord(dataSegment, 'Close');
const last = chartEngine.getFirstLastDataRecord(dataSegment, 'Close', true);
const direction = determinePriceChangeClass(last?.Close - first?.Close);
const first = chartEngine.getFirstLastDataRecord(dataSegment, 'Value');
const last = chartEngine.getFirstLastDataRecord(dataSegment, 'Value', true);
const firstTickPosition = chartEngine.pixelFromTick(0);

let initialValue = first?.Value;

// if max timeframe OR first tick is after start of displayed chart window
// use initial value of 0 instead of first quote value (since chart data does
// not always start at 0)
if (!spanDays || firstTickPosition > 0) {
initialValue = 0;
}

const direction = determinePriceChangeClass(last?.Value - initialValue);
const periodPerformance = 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;
chartEngine.clearStyles();
}

dispatch('change', { first, last, firstTickPosition });
onPeriodPerformanceChange?.(periodPerformance);
});

const updateCallback = initCallback?.(chartEngine);
Expand All @@ -105,7 +122,7 @@ Display a peformance line chart for a given (static) dataset.

chartEngine.setSpan({
base: 'day',
multiplier: spanDays,
multiplier: displayDays,
goIntoPast: true
});

Expand Down
10 changes: 9 additions & 1 deletion src/lib/chart/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function isCandleTimeBucket(value: MaybeString): value is CandleTimeBucke
* @param interval A d3 time interval
*/
export function normalizeDataForInterval(data: RawTick[], interval: TimeInterval) {
return data.reduce((acc, [ts, Value]) => {
const normalized = data.reduce((acc, [ts, Value]) => {
const date = parseDate(ts);
if (!date) return acc;
const normalizedDate = interval.floor(date);
Expand All @@ -61,6 +61,14 @@ export function normalizeDataForInterval(data: RawTick[], interval: TimeInterval
acc.push({ DT: normalizedDate, Value });
return acc;
}, [] as Quote[]);

// prepend entry for prior interval (if needed) so starting value doesn't get swallowed
if (data[0] && normalized[0] && data[0][1] !== normalized[0].Value) {
const priorInterval = interval.offset(normalized[0].DT as Date, -1);
normalized.unshift({ DT: priorInterval, Value: data[0][1] });
}

return normalized;
}

export function rawTicksToQuotes(data: RawTick[]): Quote[] {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/trade-executor/components/StrategyIcon.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
const localIconUrl = `/avatars/${strategy.id}.webp`;
const strategyIconUrl = strategy.icon_url?.replace(/^http:/, 'https:');
const dataUrl = strategy.connected ? localIconUrl : strategyIconUrl;
const outdated = Boolean(strategy.new_version_id);
const outdated = Boolean(strategy.newVersionId);
</script>

<div class="strategy-icon" class:outdated>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/trade-executor/schemas/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const strategyConfigurationSchema = z.object({
id: z.string(),
name: z.string(),
url: z.string().url(),
new_version_id: z.string().optional(),
newVersionId: z.string().optional(),
hiddenPositions: primaryKey.array().default([]),
frontpage: z.boolean().default(false),
microsite: z.boolean().default(false),
Expand Down
2 changes: 1 addition & 1 deletion src/lib/wallet/MyDeposits.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
contracts.fund_value_calculator
].every(Boolean);
const isOutdated = Boolean(strategy.new_version_id);
const isOutdated = Boolean(strategy.newVersionId);
$: connected = $wallet.isConnected;
$: wrongNetwork = connected && $wallet.chain?.id !== chain.id;
Expand Down
6 changes: 3 additions & 3 deletions src/routes/strategies/StrategyTile.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
<DataBadge status="success">live</DataBadge>
{/if}

{#if isNew && !strategy.new_version_id}
{#if isNew && !strategy.newVersionId}
<DataBadge status="success">new</DataBadge>
{/if}

Expand All @@ -88,12 +88,12 @@
{/each}
{/if}

{#if !simplified && strategy.new_version_id}
{#if !simplified && strategy.newVersionId}
<Tooltip>
<DataBadge slot="trigger" status="error">Outdated</DataBadge>
<svelte:fragment slot="popup">
This is an outdated strategy. An updated version is available
<a href="/strategies/{strategy.new_version_id}">here</a>.
<a href="/strategies/{strategy.newVersionId}">here</a>.
</svelte:fragment>
</Tooltip>
{/if}
Expand Down
8 changes: 4 additions & 4 deletions src/routes/strategies/[strategy]/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
$: isPrivate = !strategy.tags.includes('live');
$: isOverviewPage = $page.url.pathname.endsWith(strategy.id);
$: hasError = shouldDisplayError(strategy, admin);
$: isOutdated = Boolean(strategy.new_version_id);
$: isOutdated = Boolean(strategy.newVersionId);
$: displayWarning = isOverviewPage && (hasError || isOutdated);

$: breadcrumbs = {
Expand Down Expand Up @@ -73,9 +73,9 @@
<AlertList status="warning" size="md" let:AlertItem>
<AlertItem title="Outdated strategy" displayWhen={isOutdated}>
You are viewing an outdated version of this strategy. An updated version is available
<a href="/strategies/{strategy.new_version_id}" data-sveltekit-reload>here</a>. To maximize future
returns, participants should consider tranfering deposits to the latest version (though there is no
guarantee of better performance).
<a href="/strategies/{strategy.newVersionId}" data-sveltekit-reload>here</a>. To maximize future returns,
participants should consider tranfering deposits to the latest version (though there is no guarantee of
better performance).
</AlertItem>
<AlertItem title="Ongoing execution issues" displayWhen={hasError}>
<StrategyError {strategy} />
Expand Down
Loading
Loading