Skip to content
Open
207 changes: 207 additions & 0 deletions shanaboo_solution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
```diff
--- a/src/components/Chart/Chart.tsx
+++ b/src/components/Chart/Chart.tsx
@@ -45,6 +45,7 @@
showOrders,
showPositions,
+ showLiquidationPrice,
height,
onClickOrder,
onClickPosition,
@@ -112,6 +113,12 @@
[positions]
);

+ const liquidationPriceLinesSeries = useMemo(
+ () => createLiquidationPriceLineSeries(liquidationPrices, theme),
+ [liquidationPrices, theme]
+ );
+
useEffect(() => {
if (!chartRef.current) return;

@@ -145,6 +152,10 @@
chartRef.current.removeSeries('positions');
}

+ if (showLiquidationPrice) {
+ chartRef.current.addSeries(liquidationPriceLineSeries, 'liquidationPrices');
+ } else {
+ chartRef.current.removeSeries('liquidationPrices');
+ }
+
chartRef.current.fitContent();
}, [
candlestickSeries,
@@ -152,6 +163,8 @@
showOrders,
positionLineSeries,
showPositions,
+ liquidationPriceLineSeries,
+ showLiquidationPrice,
]);

return (
--- a/src/components/Chart/Chart.types.ts
+++ b/src/components/Chart/Chart.types.ts
@@ -23,6 +23,7 @@
showOrders?: boolean;
showPositions?: boolean;
+ showLiquidationPrice?: boolean;
height?: number;
onClickOrder?: (order: Order) => void;
onClickPosition?: (position: Position) => void;
--- a/src/components/Chart/Chart.utils.ts
+++ b/src/components/Chart/Chart.utils.ts
@@ -78,6 +78,28 @@
}));
}

+export function createLiquidationPriceLineSeries(
+ liquidationPrices: LiquidationPrice[],
+ theme: Theme
+): LineSeriesData[] {
+ if (!liquidationPrices?.length) return [];
+
+ return liquidationPrices.map((lp) => ({
+ id: `liquidation-${lp.positionId}`,
+ type: 'line',
+ data: [
+ { time: lp.openTime, value: lp.price },
+ { time: lp.closeTime ?? Date.now() / 1000, value: lp.price },
+ ],
+ options: {
+ color: theme.colors.danger,
+ lineWidth: 1,
+ lineStyle: 2, // dashed
+ title: `Liq $${lp.price.toFixed(2)}`,
+ },
+ }));
+}
+
export function createOrderLineSeries(
orders: Order[],
theme: Theme
--- a/src/components/Chart/index.ts
+++ b/src/components/Chart/index.ts
@@ -1,3 +1,4 @@
export { Chart } from './Chart';
export type { ChartProps } from './Chart.types';
+export type { LiquidationPrice } from './Chart.types';
export { createCandlestickSeries, createOrderLineSeries, createPositionLineSeries } from './Chart.utils';
+export { createLiquidationPriceLineSeries } from './Chart.utils';
--- a/src/components/SettingsPanel/SettingsPanel.tsx
+++ b/src/components/SettingsPanel/SettingsPanel.tsx
@@ -32,6 +32,12 @@
onChange={(e) => onSettingChange('showPositions', e.target.checked)}
/>
<label htmlFor="show-positions">Show Positions</label>
+ <input
+ type="checkbox"
+ id="show-liquidation-price"
+ checked={settings.showLiquidationPrice}
+ onChange={(e) => onSettingChange('showLiquidationPrice', e.target.checked)}
+ />
+ <label htmlFor="show-liquidation-price">Show Liquidation Price</label>
</div>
</div>
);
--- a/src/hooks/useSettings.ts
+++ b/src/hooks/useSettings.ts
@@ -6,6 +6,7 @@
showOrders: true,
showPositions: true,
+ showLiquidationPrice: false,
};

export function useSettings() {
--- a/src/types/settings.ts
+++ b/src/types/settings.ts
@@ -1,4 +1,5 @@
export interface Settings {
showOrders: boolean;
showPositions: boolean;
+ showLiquidationPrice: boolean;
}
--- a/src/components/Chart/Chart.tsx
+++ b/src/components/Chart/Chart.tsx
@@ -28,6 +28,7 @@
showOrders = true,
showPositions = true,
+ showLiquidationPrice = false,
height = 400,
onClickOrder,
onClickPosition,
@@ -45,6 +46,7 @@
const { liquidationPrices } = useLiquidationPrices(positions);

const candlestickSeries = useMemo(
() => createCandlestickSeries(candles),
[candles]
@@ -78,6 +80,12 @@
[positions]
);

+ const liquidationPriceLineSeries = useMemo(
+ () => createLiquidationPriceLineSeries(liquidationPrices, theme),
+ [liquidationPrices, theme]
+ );
+
useEffect(() => {
if (!chartRef.current) return;

@@ -112,6 +120,12 @@
chartRef.current.removeSeries('positions");
}

+ if (showLiquidationPrice) {
+ chartRef.current.addSeries(liquidationPriceLineSeries, 'liquidationPrices');
+ } else {
+ chartRef.current.removeSeries('liquidationPrices');
+ }
+
chartRef.current.fitContent();
}, [
candlestickSeries,
@@ -119,6 +133,8 @@
showOrders,
positionLineSeries,
showPositions,
+ liquidationPriceLineSeries,
+ showLiquidationPrice,
]);

return (
--- a/src/components/Chart/Chart.types.ts
+++ b/src/components/Chart/Chart.types.ts
@@ -12,6 +12,12 @@
closeTime?: number;
}

+export interface LiquidationPrice {
+ positionId: string;
+ price: number;
+ openTime: number;
+ closeTime?: number;
+}
+
export interface ChartProps {
candles: Candle[];
orders?: Order[];
@@ -20,6 +26,8 @@
showOrders?: boolean;
showPositions?: boolean;
+ showLiquidationPrice?: boolean;
height?: number;
onClickOrder?: (order: Order) => void;
onClickPosition?: (position: Position) => void;
--- a/src/components/Chart/Chart.utils.ts
+++ b/src/components/Chart/Chart.utils.ts
@@ -78,6 +78,28 @@
}));
}

+export function createLiquidationPriceLineSeries(
+ liquidationPrices: LiquidationPrice[],
+ theme: Theme
+): LineSeriesData[]
27 changes: 27 additions & 0 deletions src/components/Chart/Chart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const [chartData, setChartData] = React.useState(null)
const [settings, setSettings] = React.useState({})
const [data, setData] = React.useState(null)
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState(null)
React.useEffect(() => {
if (chartData) {
chartRef.current = chartData
}
}, [chartData])
const liquidationPrice = (price) => {
// Calculate liquidation price
return entryPrice * (1 - (0.1 * direction))
}
return (
<div>
<Chart ref={chartRef} settings={settings} />
<div className="chart-controls">
<button onClick={toggleSettings}>
Toggle Liquidation Price
</button>
<div style={styles.liquidationPrice}>
{liquidationPrice}
</div>
</div>
</div>
)
129 changes: 129 additions & 0 deletions src/components/Chart/Chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { useEffect, useRef, useState } from 'react';
import { useChartData } from '../../hooks/useChartData';
import { useUserSettings } from '../../hooks/useUserSettings';
import { useLiquidationPrice } from '../../hooks/useLiquidationPrice';
import { ChartControls } from './ChartControls';
import { PriceLine } from './PriceLine';
import { OrderLine } from './OrderLine';
import { PositionLine } from './PositionLine';
import { CrosshairLine } from './CrosshairLine';
import { TimeScale } from './TimeScale';
import { LiquidationPriceLine } from './LiquidationPriceLine';
import './Chart.css';

export function Chart() {
const chartContainerRef = useRef<HTMLDivElement>(null);
const { data, loading, error } = useChartData();
const { settings } = useUserSettings();
const liquidationPrice = useLiquidationPrice();

const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const [hoverPrice, setHoverPrice] = useState<number | null>(null);
{settings.showPositions && positions.map(pos => (
<PositionLine key={pos.id} position={pos} xScale={xScale} yScale={yScale} />
))}
{settings.showLiquidationPrice && liquidationPrice !== null && (
<LiquidationPriceLine price={liquidationPrice} xScale={xScale} yScale={yScale} />
)}
<CrosshairLine mousePosition={mousePosition} xScale={xScale} yScale={yScale} />
<TimeScale xScale={xScale} height={dimensions.height} />
</svg>
import { useMemo } from 'react';
import { ScaleLinear } from 'd3-scale';

interface LiquidationPriceLineProps {
price: number;
xScale: ScaleLinear<number, number>;
yScale: ScaleLinear<number, number>;
}

export function LiquidationPriceLine({ price, xScale, yScale }: LiquidationPriceLineProps) {
const y = yScale(price);
const xRange = xScale.range();

const label = useMemo(() => {
return price.toFixed(2);
}, [price]);

if (!isFinite(y) || y < 0) {
return null;
}

return (
<g className="liquidation-price-line">
<line
x1={xRange[0]}
y1={y}
x2={xRange[1]}
y2={y}
stroke="#ef4444"
strokeDasharray="4,4"
strokeWidth={1.5}
/>
<text x={xRange[1] - 5} y={y - 5} textAnchor="end" fill="#ef4444" fontSize={12}>
Liq. {label}
</text>
</g>
);
}
showOrders: boolean;
showPositions: boolean;
showCrosshair: boolean;
showLiquidationPrice: boolean;
theme: 'light' | 'dark';
}

showOrders: true,
showPositions: true,
showCrosshair: true,
showLiquidationPrice: false,
theme: 'dark',
};

>
Positions
</ToggleButton>
<ToggleButton
active={settings.showLiquidationPrice}
onClick={() => updateSetting('showLiquidationPrice', !settings.showLiquidationPrice)}
icon="skull"
title="Toggle liquidation price"
>
Liq. Price
</ToggleButton>
<ToggleButton
active={settings.showCrosshair}
onClick={() => updateSetting('showCrosshair', !settings.showCrosshair)}
import { useMemo } from 'react';
import { useActivePositions } from './useActivePositions';

export function useLiquidationPrice(): number | null {
const { positions } = useActivePositions();

return useMemo(() => {
if (!positions || positions.length === 0) {
return null;
}

// Use the first active position's liquidation price
// In a multi-position scenario, this could be extended to show multiple
const position = positions[0];

if (position.liquidationPrice == null || !isFinite(position.liquidationPrice)) {
return null;
}

return position.liquidationPrice;
}, [positions]);
}
pointer-events: none;
}

.liquidation-price-line {
pointer-events: none;
opacity: 0.9;
}

.time-scale {
border-top: 1px solid var(--border-color);
}
Loading