diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx index 4240b5999..24452d05e 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx @@ -406,7 +406,6 @@ export const DealTicket = ({ riskFactors, scalingFactors, side, - sizeStep, type, generalAccountBalance, openVolume, @@ -523,7 +522,7 @@ export const DealTicket = ({ field.onChange(value)} diff --git a/libs/deal-ticket/src/hooks/use-max-size.spec.ts b/libs/deal-ticket/src/hooks/use-max-size.spec.ts new file mode 100644 index 000000000..8f228f2d3 --- /dev/null +++ b/libs/deal-ticket/src/hooks/use-max-size.spec.ts @@ -0,0 +1,161 @@ +import { MarginMode, OrderType, Side } from '@vegaprotocol/types'; +import { type UseMaxSizeProps, useMaxSize } from './use-max-size'; +import { renderHook } from '@testing-library/react'; +import { removeDecimal } from '@vegaprotocol/utils'; + +describe('useMaxSize', () => { + const positionDecimalPlaces = 1; + const decimalPlaces = 2; + const accountDecimals = 3; + const initialProps: UseMaxSizeProps = { + openVolume: '0', + positionDecimalPlaces, + generalAccountBalance: removeDecimal('100', accountDecimals), + side: Side.SIDE_BUY, + marginMode: MarginMode.MARGIN_MODE_ISOLATED_MARGIN, + marginFactor: '0.1', + type: OrderType.TYPE_MARKET, + marginAccountBalance: '0', + accountDecimals, + price: removeDecimal('8', decimalPlaces), // 8.0 + marketPrice: removeDecimal('10', decimalPlaces), // 10.0 + decimalPlaces, + activeOrders: [], + riskFactors: { + long: '0.9', + short: '0.8', + market: '', + }, + scalingFactors: { + initialMargin: 1.5, + }, + }; + + const renderUseMaxSizeHook = (initialProps: UseMaxSizeProps) => + renderHook((props: UseMaxSizeProps) => useMaxSize(props), { + initialProps, + }); + describe('in MARGIN_MODE_ISOLATED_MARGIN', () => { + it('calculates maxSize = collateral / marginFactor / price', () => { + const { result } = renderUseMaxSizeHook({ ...initialProps }); + // available collateral / marginFactor / price = 100 / 0.1 / 8 = 125 + expect(result.current).toEqual(125); + }); + + it('use only general account balance', () => { + const { result } = renderUseMaxSizeHook({ + ...initialProps, + openVolume: removeDecimal('25', positionDecimalPlaces), + marginAccountBalance: removeDecimal('25', accountDecimals), + generalAccountBalance: removeDecimal('75', accountDecimals), + }); + + // 75 / 0.1 / 8 = 125 + expect(result.current).toEqual(93.7); + }); + + it('if reduce market order use general and margin account balance', () => { + const { result } = renderUseMaxSizeHook({ + ...initialProps, + openVolume: `-${removeDecimal('25', positionDecimalPlaces)}`, + marginAccountBalance: removeDecimal('25', accountDecimals), + generalAccountBalance: removeDecimal('75', accountDecimals), + }); + // ((75 + 25) / 0.1 / 8) + 25 (reduced volume) = 125 + expect(result.current).toEqual(150); + }); + + it('if reduce limit order use only general account balance', () => { + const { result } = renderUseMaxSizeHook({ + ...initialProps, + type: OrderType.TYPE_LIMIT, + openVolume: `-${removeDecimal('25', positionDecimalPlaces)}`, + marginAccountBalance: removeDecimal('25', accountDecimals), + generalAccountBalance: removeDecimal('75', accountDecimals), + }); + // 75 / 0.1 / 8 = 125 + expect(result.current).toEqual(93.7); + }); + }); + + describe('in MARGIN_MODE_CROSS_MARGIN', () => { + it('calculates maxSize = availableMargin / riskFactor / initialMargin / marketPrice', () => { + const { result, rerender } = renderUseMaxSizeHook({ + ...initialProps, + marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN, + }); + // available collateral / marginFactor / price = 100 / 0.9 / 1.5 / 10 = 7.4 + expect(result.current).toEqual(7.4); + rerender({ + ...initialProps, + marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN, + side: Side.SIDE_SELL, + }); + // available collateral / marginFactor / price = 100 / 0.8 / 1.5 / 10 = 8.3 + expect(result.current).toEqual(8.3); + }); + + it('if increasing position subtract open volume', () => { + const { result } = renderUseMaxSizeHook({ + ...initialProps, + marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN, + openVolume: removeDecimal('1.8', positionDecimalPlaces), + marginAccountBalance: removeDecimal('25', accountDecimals), + generalAccountBalance: removeDecimal('75', accountDecimals), + }); + + // 75 / 0.9 / 1.5 / 10 = 5.6 + expect(result.current).toEqual(5.6); + }); + + it('if reduce market order use add reduced volume', () => { + const { result } = renderUseMaxSizeHook({ + ...initialProps, + marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN, + openVolume: `-${removeDecimal('1.8', positionDecimalPlaces)}`, + marginAccountBalance: removeDecimal('25', accountDecimals), + generalAccountBalance: removeDecimal('75', accountDecimals), + }); + // ((75 + 25) * 0.9 / 1.5 / 10) + 1.8 (reduced volume) = 9.2 + expect(result.current).toEqual(9.2); + }); + + it("if reduce limit order subtract don't include existing volume", () => { + const { result } = renderUseMaxSizeHook({ + ...initialProps, + marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN, + type: OrderType.TYPE_LIMIT, + openVolume: `-${removeDecimal('1.8', positionDecimalPlaces)}`, + marginAccountBalance: removeDecimal('25', accountDecimals), + generalAccountBalance: removeDecimal('75', accountDecimals), + }); + // (75 + 25) / 0.9 / 1.5 / 10 = 5.6 + expect(result.current).toEqual(7.4); + }); + + it('subtracts remaining orders', () => { + const { result } = renderUseMaxSizeHook({ + ...initialProps, + marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN, + activeOrders: [ + { + remaining: removeDecimal('0.5', positionDecimalPlaces), + side: Side.SIDE_SELL, + }, + { + remaining: removeDecimal('0.9', positionDecimalPlaces), + side: Side.SIDE_BUY, + }, + { + remaining: removeDecimal('0.9', positionDecimalPlaces), + side: Side.SIDE_BUY, + }, + ], + marginAccountBalance: removeDecimal('25', accountDecimals), + generalAccountBalance: removeDecimal('75', accountDecimals), + }); + // ((50 + 50) / 0.9 / 1.5/ 10) - 1.8 = 5.6 + expect(result.current).toEqual(5.6); + }); + }); +}); diff --git a/libs/deal-ticket/src/hooks/use-max-size.ts b/libs/deal-ticket/src/hooks/use-max-size.ts index b9f14c2bf..44efcf0b3 100644 --- a/libs/deal-ticket/src/hooks/use-max-size.ts +++ b/libs/deal-ticket/src/hooks/use-max-size.ts @@ -1,13 +1,13 @@ import type { MarketInfo } from '@vegaprotocol/markets'; import type { OrderFieldsFragment } from '@vegaprotocol/orders'; import { MarginMode, OrderType, Side } from '@vegaprotocol/types'; -import { toBigNum } from '@vegaprotocol/utils'; +import { determineSizeStep, toBigNum } from '@vegaprotocol/utils'; import BigNumber from 'bignumber.js'; import { useMemo } from 'react'; -interface UseMaxSizeProps { +export interface UseMaxSizeProps { accountDecimals?: number; - activeOrders?: OrderFieldsFragment[]; + activeOrders?: Pick[]; decimalPlaces: number; generalAccountBalance: string; marginAccountBalance: string; @@ -18,11 +18,13 @@ interface UseMaxSizeProps { positionDecimalPlaces: number; price?: string; riskFactors: MarketInfo['riskFactors']; - scalingFactors?: NonNullable< - MarketInfo['tradableInstrument']['marginCalculator'] - >['scalingFactors']; + scalingFactors?: Pick< + NonNullable< + MarketInfo['tradableInstrument']['marginCalculator'] + >['scalingFactors'], + 'initialMargin' + >; side: Side; - sizeStep: string; type: OrderType; } @@ -38,7 +40,6 @@ export const useMaxSize = ({ accountDecimals, price, decimalPlaces, - sizeStep, activeOrders, riskFactors, scalingFactors, @@ -52,7 +53,7 @@ export const useMaxSize = ({ (!openVolume.startsWith('-') && side === Side.SIDE_SELL); if (marginMode === MarginMode.MARGIN_MODE_ISOLATED_MARGIN) { if (!marginFactor || !price) { - return maxSize; + return 0; } const availableMargin = accountDecimals !== undefined @@ -72,13 +73,13 @@ export const useMaxSize = ({ !marketPrice || accountDecimals === undefined ) { - return maxSize; + return 0; } const availableMargin = toBigNum( generalAccountBalance, accountDecimals ).plus(toBigNum(marginAccountBalance, accountDecimals)); - // maxSize = availableMargin / scalingFactors.initialMargin / marketPrice + // maxSize = availableMargin / riskFactor / scalingFactors.initialMargin / marketPrice maxSize = availableMargin .div( BigNumber( @@ -99,7 +100,7 @@ export const useMaxSize = ({ ) ) .minus( - // subtract open volume + // subtract open volume if increasing position side === Side.SIDE_BUY ? volume.isGreaterThan(0) ? volume @@ -110,12 +111,14 @@ export const useMaxSize = ({ ); } // round to size step - maxSize = maxSize.minus(maxSize.mod(sizeStep)); + maxSize = maxSize.minus( + maxSize.mod(determineSizeStep({ positionDecimalPlaces })) + ); if (reducingPosition && type === OrderType.TYPE_MARKET) { // add open volume if position will be reduced maxSize = maxSize.plus(volume.abs()); } - return maxSize; + return maxSize.toNumber(); }, [ openVolume, positionDecimalPlaces, @@ -128,7 +131,6 @@ export const useMaxSize = ({ accountDecimals, price, decimalPlaces, - sizeStep, activeOrders, riskFactors, scalingFactors,