Skip to content

Commit

Permalink
Merge pull request #859 from tradingstrategy-ai/858-chart-color
Browse files Browse the repository at this point in the history
Fix performance chart color edge case bug
  • Loading branch information
kenkunz authored Dec 5, 2024
2 parents 3c4a347 + bbedd0c commit 1272486
Show file tree
Hide file tree
Showing 10 changed files with 319 additions and 302 deletions.
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

0 comments on commit 1272486

Please sign in to comment.