diff --git a/src/components/trade/chart/Chart.svelte b/src/components/trade/chart/Chart.svelte
index 5fd87af..df441b7 100644
--- a/src/components/trade/chart/Chart.svelte
+++ b/src/components/trade/chart/Chart.svelte
@@ -4,8 +4,8 @@
import ChartBar from './ChartBar.svelte'
- import { initChart, loadCandles, onNewPrice } from '@lib/chart'
- import { chartHeight, selectedMarket, hoveredOHLC, prices } from '@lib/stores'
+ import { initChart, loadCandles, onNewPrice, updateLiquidationPrice } from '@lib/chart'
+ import { chartHeight, selectedMarket, hoveredOHLC, prices, positions } from '@lib/stores'
import { setPageTitle } from '@lib/ui'
import { formatPriceForDisplay, formatMarketName, formatPnl } from '@lib/formatters'
@@ -32,6 +32,7 @@
}
$: setNewPrice($prices);
+ $: updateLiquidationPrice($positions);
diff --git a/src/components/trade/chart/Chart.svelte.bak b/src/components/trade/chart/Chart.svelte.bak
new file mode 100644
index 0000000..5fd87af
--- /dev/null
+++ b/src/components/trade/chart/Chart.svelte.bak
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+ {#if $hoveredOHLC}
+
+ O: $hoveredOHLC.close}>{$hoveredOHLC.open}
+ H: $hoveredOHLC.close}>{$hoveredOHLC.high}
+ L: $hoveredOHLC.close}>{$hoveredOHLC.low}
+ C: $hoveredOHLC.close}>{$hoveredOHLC.close}
+
+ Change: $hoveredOHLC.close}>
+ {@html formatPnl($hoveredOHLC.close * 1 - $hoveredOHLC.open * 1)} ({@html formatPnl(100 * ($hoveredOHLC.close * 1 - $hoveredOHLC.open * 1) / $hoveredOHLC.open, true)})
+
+ Amplitude: $hoveredOHLC.close}>
+ {@html formatPnl($hoveredOHLC.open <= $hoveredOHLC.close ? $hoveredOHLC.high * 1 - $hoveredOHLC.low * 1 : $hoveredOHLC.low * 1 - $hoveredOHLC.high * 1)}
+
+
+ {/if}
+
+
+
\ No newline at end of file
diff --git a/src/lib/chart.js b/src/lib/chart.js
index 29e5992..1e3a01c 100644
--- a/src/lib/chart.js
+++ b/src/lib/chart.js
@@ -434,3 +434,33 @@ function clearPositionLines() {
candlestickSeries.setMarkers([]);
positionLines = [];
}
+
+// --- Ajout du prix de liquidation (bounty) ---
+let liquidationLine = null;
+
+export function updateLiquidationPrice(positions) {
+ if (!chart || !candlestickSeries) return;
+
+ // Supprime l'ancienne ligne si elle existe
+ if (liquidationLine) {
+ candlestickSeries.removePriceLine(liquidationLine);
+ liquidationLine = null;
+ }
+
+ // Trouve la première position avec un prix de liquidation
+ const position = positions?.[0];
+ if (!position || !position.liqprice) return;
+
+ const liqPrice = parseFloat(position.liqprice);
+ if (isNaN(liqPrice) || liqPrice <= 0) return;
+
+ // Crée la ligne de prix de liquidation (rouge pointillée)
+ liquidationLine = candlestickSeries.createPriceLine({
+ price: liqPrice,
+ color: '#ff4444',
+ lineWidth: 2,
+ lineStyle: 2, // pointillés
+ axisLabelVisible: true,
+ title: 'Liq',
+ });
+}
diff --git a/src/lib/chart.js.bak b/src/lib/chart.js.bak
new file mode 100644
index 0000000..29e5992
--- /dev/null
+++ b/src/lib/chart.js.bak
@@ -0,0 +1,436 @@
+import { get } from 'svelte/store'
+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 { saveUserSetting, getPrecision } from './utils'
+
+import { getMarketCandles } from '@api/prices'
+
+let candles = []; // current candle set
+
+// In s
+let end;
+let earliestCandleDate;
+
+let chart;
+let candlestickSeries;
+
+// how much history to load for each resolution (in ms)
+const lookbacks = {
+ 60: 8 * 60 * 60,
+ 300: 24 * 60 * 60,
+ 900: 48 * 60 * 60,
+ 3600: 12 * 24 * 60 * 60,
+ 14400: 4 * 12 * 24 * 60 * 60,
+ 86400: 24 * 12 * 24 * 60 * 60,
+};
+
+function correctedTime(time) {
+ const timezoneOffsetMinutes = new Date().getTimezoneOffset();
+ return time - (timezoneOffsetMinutes * 60);
+}
+
+function applyWatermark() {
+ chart && chart.applyOptions({
+ watermark: {
+ color: 'rgb(44,44,46)',
+ visible: true,
+ text: get(selectedMarket),
+ fontSize: 72,
+ horzAlign: 'center',
+ vertAlign: 'center',
+ },
+ });
+}
+
+export function initChart(cb) {
+
+ const chartElem = document.getElementById('chart');
+
+ chart = createChart(chartElem);
+
+ new ResizeObserver((entries) => {
+ if (entries.length === 0 || entries[0].target !== chartElem) return;
+ const newRect = entries[0].contentRect;
+ chart.applyOptions({ height: newRect.height });
+ }).observe(chartElem);
+
+ window.onresize = () => {
+ if (window.innerWidth > 650) {
+ chart.applyOptions({ width: window.innerWidth - 315 });
+ } else {
+ chart.applyOptions({ width: window.innerWidth });
+ }
+ };
+
+ window.dispatchEvent(new Event('resize'));
+
+ chart.applyOptions({
+ layout: {
+ background: {
+ type: ColorType.Solid,
+ color: '#1c1d1c',
+ },
+ textColor: '#b3b3b3',
+ fontSize: 13,
+ fontFamily: 'Inter var'
+ },
+ grid: {
+ vertLines: {
+ color: '#2d2e2d',
+ style: 4,
+ visible: true,
+ },
+ horzLines: {
+ color: '#2d2e2d',
+ style: 4,
+ visible: true,
+ },
+ },
+ rightPriceScale: {
+ borderVisible: true,
+ entireTextOnly: true,
+ visible: true,
+ borderColor: '#2d2e2d',
+ autoScale: true
+ },
+ leftPriceScale: {
+ visible: false,
+ },
+ timeScale: {
+ timeVisible: true,
+ borderVisible: true,
+ borderColor: '#2d2e2d'
+ },
+ crosshair: {
+ mode: 0,
+ vertLine: {
+ color: '#888',
+ width: 1,
+ labelBackgroundColor: '#585958'
+ },
+ horzLine: {
+ color: '#888',
+ width: 1,
+ labelBackgroundColor: '#585958'
+ }
+ }
+ });
+
+ candlestickSeries = chart.addCandlestickSeries({
+ upColor: 'rgb(64,214,67)',
+ downColor: '#FF5324',
+ wickUpColor: 'rgb(64,214,67)',
+ wickDownColor: '#FF5324',
+ });
+
+ chart.timeScale().subscribeVisibleTimeRangeChange(onVisibleTimeRangeChanged);
+ chart.timeScale().subscribeVisibleLogicalRangeChange(onVisibleLogicalRangeChanged);
+
+ orders.subscribe((_orders) => {
+ loadOrderLines(_orders);
+ });
+ showOrdersOnChart.subscribe(() => {
+ loadOrderLines();
+ });
+
+ positions.subscribe((_positions) => {
+ loadPositionLines(_positions);
+ });
+ showPositionsOnChart.subscribe(() => {
+ loadPositionLines();
+ });
+
+ chart.subscribeCrosshairMove(param => {
+ if (!param?.seriesPrices || param?.seriesPrices.size == 0) {
+ hoveredOHLC.set();
+ } else {
+ param?.seriesPrices.forEach((value) => {
+ hoveredOHLC.set(value);
+ });
+ }
+ });
+
+ applyWatermark();
+
+ cb();
+
+}
+
+let isLoadingCandles = false;
+async function onVisibleLogicalRangeChanged(newVisibleLogicalRange) {
+ // const barsInfo = candlestickSeries.barsInLogicalRange(newVisibleLogicalRange);
+ // if (barsInfo !== null && barsInfo.barsBefore < 5) {
+ // const resolution = get(chartResolution);
+ // if (isLoadingCandles) return;
+ // isLoadingCandles = true;
+ // await loadCandles(end - lookbacks[resolution]);
+ // isLoadingCandles = false;
+ // }
+}
+
+async function onVisibleTimeRangeChanged(newVisibleTimeRange) {
+ const resolution = get(chartResolution);
+ // console.log('newVisibleTimeRange', newVisibleTimeRange, earliestCandleDate);
+ if (isLoadingCandles) return;
+ isLoadingCandles = true;
+ if (newVisibleTimeRange.from <= earliestCandleDate) {
+ await loadCandles(newVisibleTimeRange.from);
+ }
+ isLoadingCandles = false;
+}
+
+export async function setResolution(resolution) {
+ chartResolution.set(resolution);
+ saveUserSetting('chartResolution', resolution);
+ await loadCandles();
+}
+
+let lastMarket;
+let lastResolution;
+let candleData = {};
+
+export async function loadCandles(_end) {
+
+ applyWatermark();
+ chartLoading.set(true);
+
+ const resolution = get(chartResolution);
+ const market = get(selectedMarket);
+
+ if (lastMarket != market || resolution != lastResolution) {
+ lastMarket = market;
+ lastResolution = resolution;
+ candleData = {};
+ candles = [];
+ earliestCandleDate = null;
+ }
+
+ if (!candlestickSeries) return;
+
+ if (!_end) _end = Date.now()/1000;
+
+ end = parseInt(_end);
+
+ const apiCandles = await getMarketCandles({market, resolution, end});
+
+ // After API returns, market and/or resolution may have changed in the meantime
+ if (market != get(selectedMarket) || resolution != get(chartResolution)) {
+ return;
+ }
+
+ for (const item of apiCandles) {
+ const time = correctedTime(item.t);
+ candleData[time] = {
+ time,
+ low: item.l,
+ high: item.h,
+ open: item.o,
+ close: item.c
+ };
+ }
+
+ candles = Object.values(candleData).sort((a,b) => {
+ if (a.time > b.time) return 1;
+ if (a.time < b.time) return -1;
+ return 0;
+ });
+
+ // Smooth candles
+ candles = candles.map((candle, index) => {
+ let previousCandle = candles[index-1];
+ if (previousCandle) {
+ candle.open = previousCandle.close;
+ }
+ return candle;
+ });
+
+ // set data
+ candlestickSeries.setData(candles || []);
+
+ // Set chart precision
+ if (candles.length) {
+ const lastCandle = candles[candles.length-1];
+ const precision = getPrecision(formatPriceForDisplay(lastCandle.close * 1));
+ candlestickSeries.applyOptions({
+ priceFormat: {
+ type: 'price',
+ precision: precision,
+ minMove: 1/10**precision,
+ },
+ });
+
+ // set earliest candle
+ earliestCandleDate = candles[0].time;
+ }
+
+ // chart.timeScale().fitContent();
+ chart.priceScale('right').applyOptions({autoScale: true});
+
+ // redraw order and position lines
+ loadOrderLines();
+ loadPositionLines();
+
+
+ chartLoading.set(false);
+
+}
+
+export function onNewPrice(price) {
+
+ const market = get(selectedMarket);
+
+ if (lastMarket != market) {
+ return;
+ }
+
+ if (!candlestickSeries || !candles.length) return;
+
+ // add data point to current candle set
+ // use update with time = last time for this resolution
+ // get last data point to update ohlc values based on given data point
+
+ let lastCandle = candles[candles.length - 1];
+ if (!lastCandle) return;
+
+ const timestamp = correctedTime(Date.now() / 1000); // TODO: this should be timestamp from DB
+ const resolution = get(chartResolution);
+
+ if (timestamp >= lastCandle.time + resolution) {
+ // new candle
+ let candle = {
+ time: parseInt(resolution * parseInt(timestamp/resolution)),
+ low: price,
+ high: price,
+ open: lastCandle.close || price, // smoothing
+ close: price
+ }
+ candles.push(candle);
+ candlestickSeries.update(candle);
+ } else {
+ // update existing candle
+ if (lastCandle.low > price) lastCandle.low = price;
+ if (lastCandle.high < price) lastCandle.high = price;
+ lastCandle.close = price;
+ candles[candles.length - 1] = lastCandle;
+ candlestickSeries.update(lastCandle);
+ }
+
+}
+
+// Order and position lines
+
+let orderLines = [];
+let positionLines = [];
+
+function loadOrderLines(_orders) {
+
+ if (!_orders) _orders = get(orders);
+
+ if (!candlestickSeries) {
+ setTimeout(() => {
+ loadOrderLines(_orders);
+ }, 2000);
+ return;
+ }
+
+ clearOrderLines();
+
+ if (!get(showOrdersOnChart)) return;
+
+ for (let _order of _orders) {
+
+ _order = formatOrder(_order);
+
+ if (isNaN(_order.price*1) || _order.market != get(selectedMarket)) continue;
+
+ orderLines.push(
+ candlestickSeries.createPriceLine({
+ price: _order.price,
+ color: 'rgb(72,72,74)',
+ lineWidth: 1,
+ lineStyle: LineStyle.Solid,
+ axisLabelVisible: true,
+ title: `${_order.isLong ? '▲' : '▼'} ${formatForDisplay(_order.size)} ${_order.asset}`,
+ })
+ );
+
+ }
+
+}
+
+function loadPositionLines(_positions) {
+
+ if (!_positions) _positions = get(positions);
+
+ if (!candlestickSeries) {
+ setTimeout(() => {
+ loadPositionLines(_positions);
+ }, 2000);
+ return;
+ }
+
+ clearPositionLines();
+
+
+ if (!get(showPositionsOnChart)) return;
+
+ let markers = [];
+
+ for (let _position of _positions) {
+
+ _position = formatPosition(_position);
+
+ if (_position.market != get(selectedMarket)) continue;
+
+ positionLines.push(
+ candlestickSeries.createPriceLine({
+ price: _position.price,
+ color: _position.isLong ? '#00D604' : '#FF5000',
+ lineWidth: 1,
+ lineStyle: LineStyle.Solid,
+ axisLabelVisible: true,
+ title: `${_position.isLong ? '▲' : '▼'} ${formatForDisplay(_position.size)} ${_position.asset}`,
+ })
+ );
+
+ // markers.push({
+ // time: _position.timestamp.toNumber(),
+ // position: _position.isLong ? 'belowBar' : 'aboveBar',
+ // color: _position.isLong ? '#00D604' : '#FF5000',
+ // shape: _position.isLong ? 'arrowUp' : 'arrowDown',
+ // id: 'pos',
+ // text: `${formatForDisplay(_position.size)} ${_position.asset}`,
+ // size: 2
+ // });
+
+ }
+
+ // markers.sort((a,b) => {
+ // if (a.timestamp > b.timestamp) return 1;
+ // if (a.timestamp < b.timestamp) return -1;
+ // return 0;
+ // });
+
+ // console.log('markers', markers);
+
+ // candlestickSeries.setMarkers(markers);
+
+}
+
+function clearOrderLines() {
+ for (const priceline of orderLines) {
+ candlestickSeries.removePriceLine(priceline);
+ }
+ orderLines = [];
+}
+
+function clearPositionLines() {
+ for (const priceline of positionLines) {
+ candlestickSeries.removePriceLine(priceline);
+ }
+ candlestickSeries.setMarkers([]);
+ positionLines = [];
+}