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
19 changes: 17 additions & 2 deletions src/components/trade/chart/ChartBar.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
<script>

import { setResolution } from '@lib/chart'
import { chartResolution, chartLoading } from '@lib/stores'
import { setResolution, toggleIndicator } from '@lib/chart'
import { chartResolution, chartLoading, chartIndicators } from '@lib/stores'
import { LOADING_ICON } from '@lib/icons'

const indicatorControls = [
{ key: 'sma20', label: 'SMA20' },
{ key: 'ema20', label: 'EMA20' },
{ key: 'sma50', label: 'SMA50' },
];

</script>

<style>
Expand Down Expand Up @@ -44,6 +50,11 @@
.loading-icon :global(svg) {
width: 18px;
}
.divider {
width: 1px;
height: 18px;
background-color: var(--border);
}

</style>

Expand All @@ -54,5 +65,9 @@
<a class:active={$chartResolution == 3600} on:click={async () => {await setResolution(3600)}}>1h</a>
<a class:active={$chartResolution == 14400} on:click={async () => {await setResolution(14400)}}>4h</a>
<a class:active={$chartResolution == 86400} on:click={async () => {await setResolution(86400)}}>1D</a>
<div class='divider'></div>
{#each indicatorControls as indicator}
<a class:active={$chartIndicators.includes(indicator.key)} on:click={() => {toggleIndicator(indicator.key)}}>{indicator.label}</a>
{/each}
{#if $chartLoading}<div class='loading-icon'>{@html LOADING_ICON}</div>{/if}
</div>
85 changes: 84 additions & 1 deletion src/lib/chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createChart, ColorType, LineStyle } from 'lightweight-charts'

import { CURRENCY_DECIMALS } from './config'
import { formatUnits, formatOrder, formatPosition, formatForDisplay, formatPriceForDisplay } from './formatters'
import { selectedMarket, orders, positions, chartResolution, chartLoading, showOrdersOnChart, showPositionsOnChart, hoveredOHLC } from './stores'
import { selectedMarket, orders, positions, chartResolution, chartLoading, showOrdersOnChart, showPositionsOnChart, hoveredOHLC, chartIndicators } from './stores'
import { saveUserSetting, getPrecision } from './utils'

import { getMarketCandles } from '@api/prices'
Expand All @@ -16,6 +16,13 @@ let earliestCandleDate;

let chart;
let candlestickSeries;
let indicatorSeries = {};

const indicatorOptions = {
sma20: { title: 'SMA 20', period: 20, type: 'sma', color: '#f5c542' },
ema20: { title: 'EMA 20', period: 20, type: 'ema', color: '#45a3ff' },
sma50: { title: 'SMA 50', period: 50, type: 'sma', color: '#b36bff' },
};

// how much history to load for each resolution (in ms)
const lookbacks = {
Expand Down Expand Up @@ -142,6 +149,9 @@ export function initChart(cb) {
showPositionsOnChart.subscribe(() => {
loadPositionLines();
});
chartIndicators.subscribe(() => {
renderIndicators();
});

chart.subscribeCrosshairMove(param => {
if (!param?.seriesPrices || param?.seriesPrices.size == 0) {
Expand Down Expand Up @@ -188,6 +198,77 @@ export async function setResolution(resolution) {
await loadCandles();
}

export function toggleIndicator(indicator) {
let indicators = get(chartIndicators) || [];
if (indicators.includes(indicator)) {
indicators = indicators.filter((item) => item != indicator);
} else {
indicators = [...indicators, indicator];
}
chartIndicators.set(indicators);
saveUserSetting('chartIndicators', indicators);
}

function calculateSMA(data, period) {
let sum = 0;
return data.reduce((points, candle, index) => {
sum += candle.close * 1;
if (index >= period) sum -= data[index - period].close * 1;
if (index >= period - 1) {
points.push({ time: candle.time, value: sum / period });
}
return points;
}, []);
}

function calculateEMA(data, period) {
const multiplier = 2 / (period + 1);
let ema;
return data.reduce((points, candle, index) => {
const close = candle.close * 1;
if (index == period - 1) {
ema = data.slice(0, period).reduce((sum, item) => sum + item.close * 1, 0) / period;
} else if (index >= period) {
ema = (close - ema) * multiplier + ema;
}
if (index >= period - 1 && ema != undefined) {
points.push({ time: candle.time, value: ema });
}
return points;
}, []);
}

function clearIndicators() {
if (!chart) return;
for (const key in indicatorSeries) {
chart.removeSeries(indicatorSeries[key]);
}
indicatorSeries = {};
}

function renderIndicators() {
clearIndicators();
if (!chart || !candles.length) return;

for (const indicator of (get(chartIndicators) || [])) {
const options = indicatorOptions[indicator];
if (!options) continue;

const series = chart.addLineSeries({
color: options.color,
lineWidth: 2,
priceLineVisible: false,
lastValueVisible: false,
title: options.title,
});
const data = options.type == 'ema'
? calculateEMA(candles, options.period)
: calculateSMA(candles, options.period);
series.setData(data);
indicatorSeries[indicator] = series;
}
}

let lastMarket;
let lastResolution;
let candleData = {};
Expand Down Expand Up @@ -249,6 +330,7 @@ export async function loadCandles(_end) {

// set data
candlestickSeries.setData(candles || []);
renderIndicators();

// Set chart precision
if (candles.length) {
Expand Down Expand Up @@ -317,6 +399,7 @@ export function onNewPrice(price) {
candles[candles.length - 1] = lastCandle;
candlestickSeries.update(lastCandle);
}
renderIndicators();

}

Expand Down
1 change: 1 addition & 0 deletions src/lib/stores.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const unsupportedNetwork = writable();
export const chartHeight = writable(getUserSetting('chartHeight') || 320);
export const chartResolution = writable(getUserSetting('chartResolution') || 900)
export const chartLoading = writable(false);
export const chartIndicators = writable(getUserSetting('chartIndicators') || []);
export const hoveredOHLC = writable();
export const accountHeight = writable(getUserSetting('accountHeight') || 250);

Expand Down