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:
Matthew Russell 2022-11-23 08:33:37 -06:00 committed by GitHub
parent f212f5bb28
commit 0ccf564ee7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 173 additions and 129 deletions

View File

@ -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';

View File

@ -5,7 +5,7 @@ query EstimateOrder(
$size: String!
$side: Side!
$timeInForce: OrderTimeInForce!
$expiration: String
$expiration: Timestamp
$type: OrderType!
) {
estimateOrder(

View File

@ -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

View File

@ -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,

View File

@ -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
);
});

View File

@ -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;
};

View 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);
}

View File

@ -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,

View File

@ -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