diff --git a/shanaboo_solution.md b/shanaboo_solution.md new file mode 100644 index 0000000..bd2edd2 --- /dev/null +++ b/shanaboo_solution.md @@ -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)} + /> + ++ onSettingChange('showLiquidationPrice', e.target.checked)} ++ /> ++ + + + ); +--- 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[] \ No newline at end of file diff --git a/src/components/Chart/Chart.js b/src/components/Chart/Chart.js new file mode 100644 index 0000000..9921103 --- /dev/null +++ b/src/components/Chart/Chart.js @@ -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 ( +
+ +
+ +
+ {liquidationPrice} +
+
+
+ ) \ No newline at end of file diff --git a/src/components/Chart/Chart.tsx b/src/components/Chart/Chart.tsx new file mode 100644 index 0000000..cbfc7f6 --- /dev/null +++ b/src/components/Chart/Chart.tsx @@ -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(null); + const { data, loading, error } = useChartData(); + const { settings } = useUserSettings(); + const liquidationPrice = useLiquidationPrice(); + + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); + const [hoverPrice, setHoverPrice] = useState(null); + {settings.showPositions && positions.map(pos => ( + + ))} + {settings.showLiquidationPrice && liquidationPrice !== null && ( + + )} + + + +import { useMemo } from 'react'; +import { ScaleLinear } from 'd3-scale'; + +interface LiquidationPriceLineProps { + price: number; + xScale: ScaleLinear; + yScale: ScaleLinear; +} + +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 ( + + + + Liq. {label} + + + ); +} + showOrders: boolean; + showPositions: boolean; + showCrosshair: boolean; + showLiquidationPrice: boolean; + theme: 'light' | 'dark'; +} + + showOrders: true, + showPositions: true, + showCrosshair: true, + showLiquidationPrice: false, + theme: 'dark', +}; + + > + Positions + + updateSetting('showLiquidationPrice', !settings.showLiquidationPrice)} + icon="skull" + title="Toggle liquidation price" + > + Liq. Price + + 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); +} \ No newline at end of file diff --git a/src/components/Settings/Settings.js b/src/components/Settings/Settings.js new file mode 100644 index 0000000..c6658c3 --- /dev/null +++ b/src/components/Settings/Settings.js @@ -0,0 +1,30 @@ + const [settings, setSettings] = React.useState({ + theme: 'dark', + showLiquidationPrice: false, + showOrders: true, + showPositions: true, + showTrades: true + }) + const [value, setValue] = React.useState(0) + const toggleSettings = () => { + setValue(1 - showLiquidationPrice) + } + return ( +
+ {value === 1 && ( +
+

Settings

+
+ +
+
+ ) + } + ) \ No newline at end of file diff --git a/src/components/TradingChart.js b/src/components/TradingChart.js new file mode 100644 index 0000000..e51be47 --- /dev/null +++ b/src/components/TradingChart.js @@ -0,0 +1,52 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { useTheme } from '@mui/material'; + +const TradingChart = () => { + const chartRef = useRef(null); + const [chart, setChart] = useState(null); + const [liquidationPrice, setLiquidationPrice] = useState(null); + const [positions, setPositions] = useState([]); + const [orders, setOrders] = useState([]); + const [showLiquidation, setShowLiquidation] = useState(false); + + // Chart configuration + const liquidationPrice = null; + const [showOrders, showOrders] = useState(true); + const [showPositions, showPositions] = useState(true); + + const toggleLiquidationDisplay = () => { + setShowLiquidation(!showLiquidation); + }; + + const toggleOrders = () => { + setShowOrders(!showOrders); + }; + + const togglePositions = () => { + setShowPositions(!showPositions); + }; + + // Load data + useEffect(() => { + // Simulate loading data + const data = [ + { time: new Date('2024-01-15'), price: 50000 }, + { time: new Date('2024-02-15'), price: 45000 }, + { time: new Date('2024-03-15'), price: 55000 }, + ]; + + return data; + }, []); + + return ( +
+

Trading Chart Component

+
+

Price Chart

+
+ {/* Chart implementation would go here */} +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/lib/components/chart/Chart.svelte b/src/lib/components/chart/Chart.svelte new file mode 100644 index 0000000..31e2aeb --- /dev/null +++ b/src/lib/components/chart/Chart.svelte @@ -0,0 +1,122 @@ + import { getPriceScale } from './utils' + import { prices, chartCandles, chartLoading } from '$lib/stores/price' + import { chartOrders, chartPositions, chartLines } from '$lib/stores/orders' + import { chartLiquidationPrices } from '$lib/stores/orders' + import { selectedMarket } from '$lib/stores/market' + import { userSettings } from '$lib/stores/user' + import { formatForDisplay } from '$lib/utils/format' + let positionLines = [] + let orderLines = [] + let chartLineLines = [] + let liquidationPriceLines = [] + + // Crosshair + let crosshairMoved = false + positionLines = [] + orderLines = [] + chartLineLines = [] + liquidationPriceLines = [] + } + + function removeLines() { + for (const line of chartLineLines) { + line.remove() + } + for (const line of liquidationPriceLines) { + line.remove() + } + } + + function drawLines() { + if ($userSettings.showChartLines) { + drawChartLines() + } + if ($userSettings.showLiquidationPrice) { + drawLiquidationPrices() + } + } + + function drawOrders() { + } + } + + function drawLiquidationPrices() { + if (!chart) return + const priceScale = getPriceScale($selectedMarket) + for (const position of $chartPositions) { + if (!position.liquidationPrice) continue + const price = formatForDisplay(position.liquidationPrice) * 1 + const line = chart.addLineSeries({ + priceLineVisible: false, + lastValueVisible: false, + lineStyle: 2, + color: '#ff2d2d', + lineWidth: 1 + }) + line.setData([ + { time: chartCandles[0]?.time, value: price }, + { time: chartCandles[chartCandles.length - 1]?.time, value: price } + ]) + const label = line.createPriceLine({ + price: price, + color: '#ff2d2d', + lineWidth: 1, + lineStyle: 2, + axisLabelVisible: true, + title: `Liq. ${position.isLong ? 'Long' : 'Short'}` + }) + liquidationPriceLines.push(line) + } + } + + function drawChartLines() { + if (!chart) return + const priceScale = getPriceScale($selectedMarket) + $: $chartOrders, drawLines() + $: $chartPositions, drawLines() + $: $chartLines, drawLines() + $: $chartLiquidationPrices, drawLines() + + // Resize + let resizeObserver + bind:checked={$userSettings.showChartLines} + /> + updateUserSetting('showLiquidationPrice', !$userSettings.showLiquidationPrice)} + bind:checked={$userSettings.showLiquidationPrice} + /> + updateUserSetting('reduceMotion', !$userSettings.reduceMotion)} +import { derived, writable } from 'svelte/store' +import { positions } from './positions' +import { orders } from './ordersList' + + } +}) + +export const chartLiquidationPrices = derived([positions], ([$positions]) => { + if (!$positions) return [] + return $positions.filter((position) => { + return position.liquidationPrice + }) +}) + +export { chartLines } + showChartOrders: true, + showChartPositions: true, + showChartLines: true, + showLiquidationPrice: true, + reduceMotion: false +} + + import { getPriceScale } from './utils' + import { prices, chartCandles, chartLoading } from '$lib/stores/price' + import { chartOrders, chartPositions, chartLines } from '$lib/stores/orders' + import { chartLiquidationPrices } from '$lib/stores/orders' + import { selectedMarket } from '$lib/stores/market' + import { userSettings } from '$lib/stores/user' + import { formatForDisplay } from '$lib/utils/format' \ No newline at end of file