Skip to content

Commit

Permalink
Merge pull request #612 from tradingstrategy-ai/597-strategy-performa…
Browse files Browse the repository at this point in the history
…nce-chart

Strategy performance chart -> ChartIQ
  • Loading branch information
kenkunz authored Oct 24, 2023
2 parents 4122474 + 64ce402 commit 983a02e
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 121 deletions.
63 changes: 8 additions & 55 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@testing-library/jest-dom": "^6.1.2",
"@testing-library/svelte": "^4.0.3",
"@testing-library/user-event": "^14.4.3",
"@types/d3-time": "^3.0.2",
"@types/plotly.js": "^2.12.21",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
Expand Down Expand Up @@ -61,8 +62,7 @@
"bignumber.js": "^9.1.2",
"cheerio": "^1.0.0-rc.12",
"cookie": "^0.5.0",
"d3-array": "^3.2.3",
"d3-scale": "^4.0.2",
"d3-time": "^3.1.0",
"date-fns": "^2.29.3",
"dd-trace": "^4.15.0",
"devalue": "^4.3.2",
Expand Down
8 changes: 7 additions & 1 deletion src/lib/chart/ChartIQ.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Dynamically ChartIQ modules (if available) and render chart element.
<script lang="ts">
import { fade } from 'svelte/transition';
import { lightFormat as formatDate } from 'date-fns';
import { floorUTCDate, addUTCDays } from '$lib/helpers/date';
import { type ChartLinker, type QuoteFeed, ChartActivityTracker } from '$lib/chart';
import { Alert } from '$lib/components';
import Spinner from 'svelte-spinner';
Expand Down Expand Up @@ -125,7 +126,12 @@ Dynamically ChartIQ modules (if available) and render chart element.
// ChartIQ doesn't preserve original UTC date; restore it from tz-adjusted DT value
if (data) {
data.originalDate = new Date(data.DT.getTime() - data.DT.getTimezoneOffset() * 60000);
const { timeUnit } = chartEngine.getPeriodicity();
if (timeUnit === 'day') {
data.adjustedDate = addUTCDays(floorUTCDate(data.DT), 1);
} else {
data.adjustedDate = new Date(data.DT);
}
}
cursor = { position, data };
Expand Down
14 changes: 4 additions & 10 deletions src/routes/strategies/ChartThumbnail.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script lang="ts">
import { addUTCDays, floorUTCDate } from '$lib/helpers/date';
import { determinePriceChangeClass } from '$lib/helpers/price';
import { formatPercent } from '$lib/helpers/formatters';
import { ChartIQ, Marker } from '$lib/chart';
Expand Down Expand Up @@ -36,14 +35,9 @@
return {
update() {
chartEngine.loadChart('strategy-thumbnail', {
masterData: data.map(([date, value]) => ({
// passing ISO date (w/out time) to prevent buggy ChartIQ tz conversions
DT: date.toISOString().slice(0, 10),
Value: value,
// add baseline data for drawing a simple 0 basline series (see above)
Baseline: 0
})),
span: { base: 'day', multiplier: 90 }
periodicity: { period: 1, timeUnit: 'day' },
span: { base: 'day', multiplier: 90 },
masterData: data.map(([DT, Value]) => ({ DT, Value, Baseline: 0 }))
});
const { yAxis } = chartEngine.chart;
Expand All @@ -62,7 +56,7 @@
<Marker x={position.DateX} y={position.CloseY} size={4} />
<div class="chart-hover-info" style:--x="{position.cx}px" style:--y="{position.CloseY}px">
<UpDownCell value={data.Close - data.iqPrevClose}>
<Timestamp date={data.originalDate} />
<Timestamp date={data.adjustedDate} />
<div class="value">{formatPercent(data.Close, 2)}</div>
</UpDownCell>
</div>
Expand Down
12 changes: 2 additions & 10 deletions src/routes/strategies/[strategy]/(nav)/performance/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<script lang="ts">
import { getPortfolioLatestStats } from 'trade-executor/state/stats';
import SummaryStatistics from './SummaryStatistics.svelte';
import WebChart from '../../WebChart.svelte';
import PortfolioPerformanceChart from './PortfolioPerformanceChart.svelte';
export let data;
$: ({ state, summary, profitabilityChart } = data);
Expand All @@ -17,15 +17,7 @@

<section class="performance">
{#if profitabilityChart}
<!-- We cannot do fill here, because protability chart goes below zero -->
<WebChart
name="Profitability"
description="Compounded profitability of realised trading positions."
webChart={profitabilityChart}
yType="percent"
yAxisTitle="Realised profit"
fillMode="none"
/>
<PortfolioPerformanceChart data={profitabilityChart.data} helpLink={profitabilityChart.help_link} />
{/if}

<SummaryStatistics {oldLatestStats} {summaryStatistics} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,58 @@ Render the portfolio performance chart using ChartIQ.
- Y-axis: portfolio value
-->
<script lang="ts">
import { lightFormat as formatDate } from 'date-fns';
import { formatDollar } from '$lib/helpers/formatters';
import { utcHour, utcDay } from 'd3-time';
import { formatPercent } from '$lib/helpers/formatters';
import { determinePriceChangeClass } from '$lib/helpers/price';
import { SegmentedControl, UpDownCell } from '$lib/components';
import { SegmentedControl, Timestamp, UpDownCell } from '$lib/components';
import { ChartIQ, Marker } from '$lib/chart';
export let name: string;
export let portfolio: any;
export let data: [number, number][];
export let helpLink: string;
let chartWrapper: HTMLElement;
type ChartTick = [Date, number];
const rawData: ChartTick[] = data.map(([ts, val]) => [new Date(ts * 1000), val]);
let timeSpan = '3M';
let chartWrapper: HTMLElement;
const timeSpanOptions = {
'1W': { hours: 1, spanDays: 7, dateFormat: 'M/d HH:mm' },
'1M': { hours: 4, spanDays: 30, dateFormat: 'M/d HH:mm' },
'3M': { hours: 24, spanDays: 90, dateFormat: 'M/d/yyyy' }
};
'1W': {
spanDays: 7,
interval: utcHour.every(1),
periodicity: { period: 1, interval: 60, timeUnit: 'minute' }
},
'1M': {
spanDays: 30,
interval: utcHour.every(4),
periodicity: { period: 1, interval: 4 * 60, timeUnit: 'minute' }
},
'3M': {
spanDays: 90,
interval: utcDay.every(1),
periodicity: { period: 1, interval: 1, timeUnit: 'day' }
}
} as const;
let timeSpan: keyof typeof timeSpanOptions = '3M';
$: ({ hours, spanDays, dateFormat } = timeSpanOptions[timeSpan]);
$: ({ spanDays, interval, periodicity } = timeSpanOptions[timeSpan]);
function getNormalizedIntervalData() {
return rawData.reduce((acc: ChartTick[], [date, value]) => {
const normalizedDate = interval!.floor(date);
const replace = Number(normalizedDate.valueOf() === acc.at(-1)?.[0].valueOf());
acc.splice(acc.length - replace, replace, [normalizedDate, value]);
return acc;
}, []);
}
const options = {
layout: { chartType: 'mountain' },
controls: { chartControls: null },
dontRoll: true,
chart: {
tension: 1,
xAxis: { displayGridLines: false },
yAxis: {
displayGridLines: false,
Expand All @@ -39,16 +66,6 @@ Render the portfolio performance chart using ChartIQ.
}
};
function getChartData(portfolio) {
if (!portfolio) return [];
return portfolio.map((tick) => {
return {
DT: tick.calculated_at * 1000,
Value: tick.total_equity
};
});
}
function init(chartEngine: any) {
// update chart colors based on change in value (+/-) for visible data set
chartEngine.append('createDataSegment', () => {
Expand All @@ -66,10 +83,10 @@ Render the portfolio performance chart using ChartIQ.
return {
update() {
chartEngine.loadChart(name, {
periodicity: { period: hours, interval: 60, timeUnit: 'minute' },
chartEngine.loadChart('strategy-profitability', {
periodicity,
span: { base: 'day', multiplier: spanDays },
masterData: getChartData(portfolio)
masterData: getNormalizedIntervalData().map(([DT, Value]) => ({ DT, Value }))
});
}
};
Expand All @@ -78,19 +95,21 @@ Render the portfolio performance chart using ChartIQ.

<div class="portfolio-performance-chart">
<header>
<h2>Total Equity</h2>
<h2>Profitability</h2>
<SegmentedControl options={Object.keys(timeSpanOptions)} bind:selected={timeSpan} />
</header>
<p>Cash and market valued tokens in the strategy (USD)</p>
<p>
Compounded <a class="body-link" href={helpLink}>profitability</a> of realised trading positions.
</p>
<div bind:this={chartWrapper}>
<ChartIQ {init} {options} invalidate={[timeSpan]} 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}>
<div class="date">{formatDate(data.displayDate, dateFormat)}</div>
<div class="value">{formatDollar(data.Close)}</div>
<Timestamp date={data.adjustedDate} withTime={periodicity.timeUnit === 'minute'} />
<div class="value">{formatPercent(data.Close, 2)}</div>
</UpDownCell>
</div>
{/if}
Expand Down Expand Up @@ -128,15 +147,10 @@ Render the portfolio performance chart using ChartIQ.
h2 {
font: var(--f-heading-md-medium);
letter-spacing: var(--f-heading-md-spacing, normal);
@media (--viewport-sm-down) {
font: var(--f-heading-sm-medium);
letter-spacing: var(--f-heading-sm-spacing, normal);
}
}
p {
color: hsla(var(--hsl-text-extra-light));
color: hsla(var(--hsl-text-light));
font: var(--f-ui-md-medium);
letter-spacing: var(--f-ui-md-spacing, normal);
Expand All @@ -151,19 +165,14 @@ Render the portfolio performance chart using ChartIQ.
position: absolute;
left: var(--x);
top: var(--y);
background: hsla(var(--hsl-body), 0.8);
border-radius: var(--radius-sm);
transform: translate(-50%, calc(-100% - var(--space-md)));
.date {
font: var(--f-ui-xs-bold);
letter-spacing: var(--f-ui-xs-spacing);
color: hsla(var(--hsl-text), 0.4);
white-space: nowrap;
:global(time) {
color: hsla(var(--hsl-text-extra-light));
}
.value {
font: var(--f-ui-md-roman);
font: var(--f-ui-md-medium);
letter-spacing: var(--f-ui-md-spacing);
}
}
Expand Down

0 comments on commit 983a02e

Please sign in to comment.