fix(#2137,#2197): correct price for estimate order query (#2159)
* fix: correct price for estimate order query * chore: remove unnecessary optional chaining, accidental added test file * chore: dont prevent orders for pending market state * chore: estimate order query so that expiration is set as timestamp otherwise query fails * chore: add test helper for market, fix tests and add type for order margin
This commit is contained in:
parent
f212f5bb28
commit
0ccf564ee7
@ -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';
|
||||
|
||||
|
@ -5,7 +5,7 @@ query EstimateOrder(
|
||||
$size: String!
|
||||
$side: Side!
|
||||
$timeInForce: OrderTimeInForce!
|
||||
$expiration: String
|
||||
$expiration: Timestamp
|
||||
$type: OrderType!
|
||||
) {
|
||||
estimateOrder(
|
||||
|
@ -10,7 +10,7 @@ export type EstimateOrderQueryVariables = Types.Exact<{
|
||||
size: Types.Scalars['String'];
|
||||
side: Types.Side;
|
||||
timeInForce: Types.OrderTimeInForce;
|
||||
expiration?: Types.InputMaybe<Types.Scalars['String']>;
|
||||
expiration?: Types.InputMaybe<Types.Scalars['Timestamp']>;
|
||||
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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
);
|
||||
});
|
||||
|
@ -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;
|
||||
};
|
||||
|
82
libs/deal-ticket/src/test-helpers.ts
Normal file
82
libs/deal-ticket/src/test-helpers.ts
Normal file
@ -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>
|
||||
): 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);
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user