diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx index 434804979..4d43d714c 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { VegaWalletContext } from '@vegaprotocol/wallet'; import { fireEvent, render, screen, act } from '@testing-library/react'; -import type { MarketDealTicket } from '@vegaprotocol/market-list'; +import { generateMarket } from '../../test-helpers'; import { DealTicket } from './deal-ticket'; import { Schema } from '@vegaprotocol/types'; import type { OrderSubmissionBody } from '@vegaprotocol/wallet'; @@ -10,47 +10,7 @@ import { MockedProvider } from '@apollo/client/testing'; import type { ChainIdQuery } from '@vegaprotocol/react-helpers'; import { ChainIdDocument, addDecimal } from '@vegaprotocol/react-helpers'; -const market = { - __typename: 'Market', - id: 'market-id', - decimalPlaces: 2, - positionDecimalPlaces: 1, - tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS, - state: Schema.MarketState.STATE_ACTIVE, - tradableInstrument: { - __typename: 'TradableInstrument', - instrument: { - __typename: 'Instrument', - id: '1', - name: 'Instrument name', - product: { - __typename: 'Future', - quoteName: 'quote-name', - settlementAsset: { - __typename: 'Asset', - id: 'asset-id', - name: 'asset-name', - symbol: 'asset-symbol', - decimals: 2, - }, - }, - }, - }, - fees: { - factors: { - makerFee: '0.001', - infrastructureFee: '0.002', - liquidityFee: '0.003', - }, - }, - depth: { - __typename: 'MarketDepth', - lastTrade: { - __typename: 'Trade', - price: '100', - }, - }, -} as MarketDealTicket; +const market = generateMarket(); const submit = jest.fn(); const transactionStatus = 'default'; diff --git a/libs/deal-ticket/src/hooks/EstimateOrder.graphql b/libs/deal-ticket/src/hooks/EstimateOrder.graphql index 13e1827e2..88d3a6a4e 100644 --- a/libs/deal-ticket/src/hooks/EstimateOrder.graphql +++ b/libs/deal-ticket/src/hooks/EstimateOrder.graphql @@ -5,7 +5,7 @@ query EstimateOrder( $size: String! $side: Side! $timeInForce: OrderTimeInForce! - $expiration: String + $expiration: Timestamp $type: OrderType! ) { estimateOrder( diff --git a/libs/deal-ticket/src/hooks/__generated__/EstimateOrder.ts b/libs/deal-ticket/src/hooks/__generated__/EstimateOrder.ts index 2ae4855ac..d203ea732 100644 --- a/libs/deal-ticket/src/hooks/__generated__/EstimateOrder.ts +++ b/libs/deal-ticket/src/hooks/__generated__/EstimateOrder.ts @@ -10,7 +10,7 @@ export type EstimateOrderQueryVariables = Types.Exact<{ size: Types.Scalars['String']; side: Types.Side; timeInForce: Types.OrderTimeInForce; - expiration?: Types.InputMaybe; + expiration?: Types.InputMaybe; type: Types.OrderType; }>; @@ -19,7 +19,7 @@ export type EstimateOrderQuery = { __typename?: 'Query', estimateOrder: { __type export const EstimateOrderDocument = gql` - query EstimateOrder($marketId: ID!, $partyId: ID!, $price: String, $size: String!, $side: Side!, $timeInForce: OrderTimeInForce!, $expiration: String, $type: OrderType!) { + query EstimateOrder($marketId: ID!, $partyId: ID!, $price: String, $size: String!, $side: Side!, $timeInForce: OrderTimeInForce!, $expiration: Timestamp, $type: OrderType!) { estimateOrder( marketId: $marketId partyId: $partyId diff --git a/libs/deal-ticket/src/hooks/use-fee-deal-ticket-details.tsx b/libs/deal-ticket/src/hooks/use-fee-deal-ticket-details.tsx index 7c4ae58e7..e8b721f08 100644 --- a/libs/deal-ticket/src/hooks/use-fee-deal-ticket-details.tsx +++ b/libs/deal-ticket/src/hooks/use-fee-deal-ticket-details.tsx @@ -30,7 +30,7 @@ export const useFeeDealTicketDetails = ( const slippage = useCalculateSlippage({ marketId: market.id, order }); const price = useMemo(() => { - const estPrice = order.price || market.depth?.lastTrade?.price; + const estPrice = order.price || market.data.markPrice; if (estPrice) { if (slippage && parseFloat(slippage) !== 0) { const isLong = order.side === Schema.Side.SIDE_BUY; @@ -42,7 +42,7 @@ export const useFeeDealTicketDetails = ( return order.price; } return null; - }, [market.depth?.lastTrade?.price, order.price, order.side, slippage]); + }, [market.data.markPrice, order.price, order.side, slippage]); const estMargin: OrderMargin | null = useOrderMargin({ order, diff --git a/libs/deal-ticket/src/hooks/use-order-margin.spec.ts b/libs/deal-ticket/src/hooks/use-order-margin.spec.ts index e2918a725..477798377 100644 --- a/libs/deal-ticket/src/hooks/use-order-margin.spec.ts +++ b/libs/deal-ticket/src/hooks/use-order-margin.spec.ts @@ -1,10 +1,10 @@ import { renderHook } from '@testing-library/react'; import { useQuery } from '@apollo/client'; import { BigNumber } from 'bignumber.js'; -import type { OrderSubmissionBody } from '@vegaprotocol/wallet'; -import type { MarketDealTicket } from '@vegaprotocol/market-list'; import type { PositionMargin } from './use-market-positions'; +import type { Props } from './use-order-margin'; import { useOrderMargin } from './use-order-margin'; +import { Schema } from '@vegaprotocol/types'; let mockEstimateData = { estimateOrder: { @@ -18,6 +18,7 @@ let mockEstimateData = { }, }, }; + jest.mock('@apollo/client', () => ({ ...jest.requireActual('@apollo/client'), useQuery: jest.fn(() => ({ data: mockEstimateData })), @@ -39,73 +40,52 @@ jest.mock('./use-market-positions', () => ({ })); describe('useOrderMargin', () => { - const order = { - size: '2', - side: 'SIDE_BUY', - timeInForce: 'TIME_IN_FORCE_IOC', - type: 'TYPE_MARKET', - }; - const market = { - id: 'marketId', - depth: { - lastTrade: { - price: '1000000', - }, - }, - tradableInstrument: { - instrument: { - product: { - settlementAsset: { - decimals: 5, - }, - }, + const marketId = 'marketId'; + const args: Props = { + order: { + marketId, + size: '2', + side: Schema.Side.SIDE_BUY, + timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC, + type: Schema.OrderType.TYPE_MARKET, + }, + market: { + id: marketId, + decimalPlaces: 2, + positionDecimalPlaces: 0, + tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS, + data: { + indicativePrice: '100', + markPrice: '200', }, }, + partyId: 'partyId', }; - const partyId = 'partyId'; afterEach(() => { jest.clearAllMocks(); }); it('should calculate margin correctly', () => { - const { result } = renderHook(() => - useOrderMargin({ - order: order as OrderSubmissionBody['orderSubmission'], - market: market as MarketDealTicket, - partyId, - }) - ); + const { result } = renderHook(() => useOrderMargin(args)); expect(result.current?.margin).toEqual('100000'); - expect((useQuery as jest.Mock).mock.calls[1][1].variables.size).toEqual( - order.size + expect((useQuery as jest.Mock).mock.calls[0][1].variables.size).toEqual( + args.order.size ); }); it('should calculate fees correctly', () => { - const { result } = renderHook(() => - useOrderMargin({ - order: order as OrderSubmissionBody['orderSubmission'], - market: market as MarketDealTicket, - partyId, - }) - ); + const { result } = renderHook(() => useOrderMargin(args)); expect(result.current?.totalFees).toEqual('300000'); }); it('should not subtract initialMargin if there is no position', () => { mockMarketPositions = null; - const { result } = renderHook(() => - useOrderMargin({ - order: order as OrderSubmissionBody['orderSubmission'], - market: market as MarketDealTicket, - partyId, - }) - ); + const { result } = renderHook(() => useOrderMargin(args)); expect(result.current?.margin).toEqual('200000'); - expect((useQuery as jest.Mock).mock.calls[1][1].variables.size).toEqual( - order.size + expect((useQuery as jest.Mock).mock.calls[0][1].variables.size).toEqual( + args.order.size ); }); @@ -122,19 +102,13 @@ describe('useOrderMargin', () => { }, }, }; - const { result } = renderHook(() => - useOrderMargin({ - order: order as OrderSubmissionBody['orderSubmission'], - market: market as MarketDealTicket, - partyId, - }) - ); + const { result } = renderHook(() => useOrderMargin(args)); expect(result.current).toEqual(null); const calledSize = new BigNumber(mockMarketPositions?.openVolume || 0) - .plus(order.size) + .plus(args.order.size) .toString(); - expect((useQuery as jest.Mock).mock.calls[1][1].variables.size).toEqual( + expect((useQuery as jest.Mock).mock.calls[0][1].variables.size).toEqual( calledSize ); }); diff --git a/libs/deal-ticket/src/hooks/use-order-margin.ts b/libs/deal-ticket/src/hooks/use-order-margin.ts index 96e50c475..173534bf7 100644 --- a/libs/deal-ticket/src/hooks/use-order-margin.ts +++ b/libs/deal-ticket/src/hooks/use-order-margin.ts @@ -2,15 +2,25 @@ import { BigNumber } from 'bignumber.js'; import type { OrderSubmissionBody } from '@vegaprotocol/wallet'; import { Schema } from '@vegaprotocol/types'; import { removeDecimal } from '@vegaprotocol/react-helpers'; -import type { MarketDealTicket } from '@vegaprotocol/market-list'; import { useMarketPositions } from './use-market-positions'; -import { useMarketDataMarkPrice } from './use-market-data-mark-price'; import type { EstimateOrderQuery } from './__generated__/EstimateOrder'; import { useEstimateOrderQuery } from './__generated__/EstimateOrder'; +import { isMarketInAuction } from '../utils'; -interface Props { +interface Market { + id: string; + decimalPlaces: number; + positionDecimalPlaces: number; + tradingMode: Schema.MarketTradingMode; + data: { + indicativePrice: string; + markPrice: string; + }; +} + +export interface Props { order: OrderSubmissionBody['orderSubmission']; - market: MarketDealTicket; + market: Market; partyId: string; } @@ -36,28 +46,19 @@ export const useOrderMargin = ({ partyId, }: Props): OrderMargin | null => { const marketPositions = useMarketPositions({ marketId: market.id, partyId }); - const markPriceData = useMarketDataMarkPrice(market.id); + const priceForEstimate = getPriceForEstimate(order, market); const { data } = useEstimateOrderQuery({ variables: { marketId: market.id, partyId, - price: order.price - ? removeDecimal(order.price, market.decimalPlaces) - : markPriceData?.market?.data?.markPrice || '', + price: priceForEstimate, size: removeDecimal(order.size, market.positionDecimalPlaces), - side: - order.side === Schema.Side.SIDE_BUY - ? Schema.Side.SIDE_BUY - : Schema.Side.SIDE_SELL, + side: order.side, timeInForce: order.timeInForce, type: order.type, }, - skip: - !partyId || - !market.id || - !order.size || - !markPriceData?.market?.data?.markPrice, + skip: !partyId || !market.id || !order.size || !priceForEstimate, }); if (data?.estimateOrder.marginLevels.initialLevel) { @@ -69,8 +70,10 @@ export const useOrderMargin = ({ marketPositions?.balance || 0 ) ).toString(); + const { makerFee, liquidityFee, infrastructureFee } = data.estimateOrder.fee; + return { margin, totalFees: fees, @@ -83,3 +86,32 @@ export const useOrderMargin = ({ } return null; }; + +/** + * Gets a price to use for estimating order margin and fees. + * Market orders should use the markPrice or if in auction mode + * the indicative price. If its a limit order use user input price. + */ +const getPriceForEstimate = ( + order: { + type: Schema.OrderType; + price?: string | undefined; + }, + market: Market +) => { + // If order type is market we should use either the mark price + // or the uncrossing price. If order type is limit use the price + // the user has input + let price; + if (order.type === Schema.OrderType.TYPE_LIMIT && order.price) { + price = removeDecimal(order.price, market.decimalPlaces); + } else { + if (isMarketInAuction(market)) { + price = market.data.indicativePrice; + } else { + price = market.data.markPrice; + } + } + + return price === '0' ? undefined : price; +}; diff --git a/libs/deal-ticket/src/test-helpers.ts b/libs/deal-ticket/src/test-helpers.ts new file mode 100644 index 000000000..649f7789d --- /dev/null +++ b/libs/deal-ticket/src/test-helpers.ts @@ -0,0 +1,82 @@ +import type { MarketDealTicket } from '@vegaprotocol/market-list'; +import { Schema } from '@vegaprotocol/types'; +import merge from 'lodash/merge'; +import type { PartialDeep } from 'type-fest'; + +export function generateMarket( + override?: PartialDeep +): MarketDealTicket { + const defaultMarket: MarketDealTicket = { + __typename: 'Market', + id: 'market-id', + decimalPlaces: 2, + positionDecimalPlaces: 1, + tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS, + state: Schema.MarketState.STATE_ACTIVE, + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + id: '1', + name: 'Instrument name', + code: 'instrument-code', + metadata: { + __typename: 'InstrumentMetadata', + tags: [], + }, + product: { + __typename: 'Future', + quoteName: 'quote-name', + dataSourceSpecForTradingTermination: { + id: 'data-source-for-trading-termination-id', + }, + settlementAsset: { + __typename: 'Asset', + id: 'asset-id', + name: 'asset-name', + symbol: 'asset-symbol', + decimals: 2, + }, + }, + }, + }, + data: { + __typename: 'MarketData', + market: { + __typename: 'Market', + id: 'market-id', + }, + bestBidPrice: '100', + bestOfferPrice: '100', + markPrice: '200', + trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_BATCH, + staticMidPrice: '100', + marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS, + indicativePrice: '100', + indicativeVolume: '10', + bestStaticBidPrice: '100', + bestStaticOfferPrice: '100', + }, + marketTimestamps: { + __typename: 'MarketTimestamps', + open: null, + close: null, + }, + fees: { + factors: { + makerFee: '0.001', + infrastructureFee: '0.002', + liquidityFee: '0.003', + }, + }, + depth: { + __typename: 'MarketDepth', + lastTrade: { + __typename: 'Trade', + price: '100', + }, + }, + }; + + return merge(defaultMarket, override); +} diff --git a/libs/deal-ticket/src/utils/is-market-in-auction.ts b/libs/deal-ticket/src/utils/is-market-in-auction.ts index 0f3a1555c..535d0a916 100644 --- a/libs/deal-ticket/src/utils/is-market-in-auction.ts +++ b/libs/deal-ticket/src/utils/is-market-in-auction.ts @@ -1,7 +1,8 @@ import { Schema } from '@vegaprotocol/types'; -import type { MarketDealTicket } from '@vegaprotocol/market-list'; -export const isMarketInAuction = (market: MarketDealTicket) => { +export const isMarketInAuction = (market: { + tradingMode: Schema.MarketTradingMode; +}) => { return [ Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION, Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION, diff --git a/libs/deal-ticket/src/utils/validate-market-state.ts b/libs/deal-ticket/src/utils/validate-market-state.ts index c376955fa..8ce580921 100644 --- a/libs/deal-ticket/src/utils/validate-market-state.ts +++ b/libs/deal-ticket/src/utils/validate-market-state.ts @@ -16,12 +16,7 @@ export const validateMarketState = (state: Schema.MarketState) => { ); } - if ( - [ - Schema.MarketState.STATE_PROPOSED, - Schema.MarketState.STATE_PENDING, - ].includes(state) - ) { + if (state === Schema.MarketState.STATE_PROPOSED) { return t( `This market is ${marketTranslations( state