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 */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { VegaWalletContext } from '@vegaprotocol/wallet'; import { VegaWalletContext } from '@vegaprotocol/wallet';
import { fireEvent, render, screen, act } from '@testing-library/react'; 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 { DealTicket } from './deal-ticket';
import { Schema } from '@vegaprotocol/types'; import { Schema } from '@vegaprotocol/types';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet'; import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
@ -10,47 +10,7 @@ import { MockedProvider } from '@apollo/client/testing';
import type { ChainIdQuery } from '@vegaprotocol/react-helpers'; import type { ChainIdQuery } from '@vegaprotocol/react-helpers';
import { ChainIdDocument, addDecimal } from '@vegaprotocol/react-helpers'; import { ChainIdDocument, addDecimal } from '@vegaprotocol/react-helpers';
const market = { const market = generateMarket();
__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 submit = jest.fn(); const submit = jest.fn();
const transactionStatus = 'default'; const transactionStatus = 'default';

View File

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

View File

@ -10,7 +10,7 @@ export type EstimateOrderQueryVariables = Types.Exact<{
size: Types.Scalars['String']; size: Types.Scalars['String'];
side: Types.Side; side: Types.Side;
timeInForce: Types.OrderTimeInForce; timeInForce: Types.OrderTimeInForce;
expiration?: Types.InputMaybe<Types.Scalars['String']>; expiration?: Types.InputMaybe<Types.Scalars['Timestamp']>;
type: Types.OrderType; type: Types.OrderType;
}>; }>;
@ -19,7 +19,7 @@ export type EstimateOrderQuery = { __typename?: 'Query', estimateOrder: { __type
export const EstimateOrderDocument = gql` 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( estimateOrder(
marketId: $marketId marketId: $marketId
partyId: $partyId partyId: $partyId

View File

@ -30,7 +30,7 @@ export const useFeeDealTicketDetails = (
const slippage = useCalculateSlippage({ marketId: market.id, order }); const slippage = useCalculateSlippage({ marketId: market.id, order });
const price = useMemo(() => { const price = useMemo(() => {
const estPrice = order.price || market.depth?.lastTrade?.price; const estPrice = order.price || market.data.markPrice;
if (estPrice) { if (estPrice) {
if (slippage && parseFloat(slippage) !== 0) { if (slippage && parseFloat(slippage) !== 0) {
const isLong = order.side === Schema.Side.SIDE_BUY; const isLong = order.side === Schema.Side.SIDE_BUY;
@ -42,7 +42,7 @@ export const useFeeDealTicketDetails = (
return order.price; return order.price;
} }
return null; return null;
}, [market.depth?.lastTrade?.price, order.price, order.side, slippage]); }, [market.data.markPrice, order.price, order.side, slippage]);
const estMargin: OrderMargin | null = useOrderMargin({ const estMargin: OrderMargin | null = useOrderMargin({
order, order,

View File

@ -1,10 +1,10 @@
import { renderHook } from '@testing-library/react'; import { renderHook } from '@testing-library/react';
import { useQuery } from '@apollo/client'; import { useQuery } from '@apollo/client';
import { BigNumber } from 'bignumber.js'; 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 { PositionMargin } from './use-market-positions';
import type { Props } from './use-order-margin';
import { useOrderMargin } from './use-order-margin'; import { useOrderMargin } from './use-order-margin';
import { Schema } from '@vegaprotocol/types';
let mockEstimateData = { let mockEstimateData = {
estimateOrder: { estimateOrder: {
@ -18,6 +18,7 @@ let mockEstimateData = {
}, },
}, },
}; };
jest.mock('@apollo/client', () => ({ jest.mock('@apollo/client', () => ({
...jest.requireActual('@apollo/client'), ...jest.requireActual('@apollo/client'),
useQuery: jest.fn(() => ({ data: mockEstimateData })), useQuery: jest.fn(() => ({ data: mockEstimateData })),
@ -39,73 +40,52 @@ jest.mock('./use-market-positions', () => ({
})); }));
describe('useOrderMargin', () => { describe('useOrderMargin', () => {
const order = { const marketId = 'marketId';
size: '2', const args: Props = {
side: 'SIDE_BUY', order: {
timeInForce: 'TIME_IN_FORCE_IOC', marketId,
type: 'TYPE_MARKET', size: '2',
}; side: Schema.Side.SIDE_BUY,
const market = { timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
id: 'marketId', type: Schema.OrderType.TYPE_MARKET,
depth: { },
lastTrade: { market: {
price: '1000000', id: marketId,
}, decimalPlaces: 2,
}, positionDecimalPlaces: 0,
tradableInstrument: { tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
instrument: { data: {
product: { indicativePrice: '100',
settlementAsset: { markPrice: '200',
decimals: 5,
},
},
}, },
}, },
partyId: 'partyId',
}; };
const partyId = 'partyId';
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
it('should calculate margin correctly', () => { it('should calculate margin correctly', () => {
const { result } = renderHook(() => const { result } = renderHook(() => useOrderMargin(args));
useOrderMargin({
order: order as OrderSubmissionBody['orderSubmission'],
market: market as MarketDealTicket,
partyId,
})
);
expect(result.current?.margin).toEqual('100000'); expect(result.current?.margin).toEqual('100000');
expect((useQuery as jest.Mock).mock.calls[1][1].variables.size).toEqual( expect((useQuery as jest.Mock).mock.calls[0][1].variables.size).toEqual(
order.size args.order.size
); );
}); });
it('should calculate fees correctly', () => { it('should calculate fees correctly', () => {
const { result } = renderHook(() => const { result } = renderHook(() => useOrderMargin(args));
useOrderMargin({
order: order as OrderSubmissionBody['orderSubmission'],
market: market as MarketDealTicket,
partyId,
})
);
expect(result.current?.totalFees).toEqual('300000'); expect(result.current?.totalFees).toEqual('300000');
}); });
it('should not subtract initialMargin if there is no position', () => { it('should not subtract initialMargin if there is no position', () => {
mockMarketPositions = null; mockMarketPositions = null;
const { result } = renderHook(() => const { result } = renderHook(() => useOrderMargin(args));
useOrderMargin({
order: order as OrderSubmissionBody['orderSubmission'],
market: market as MarketDealTicket,
partyId,
})
);
expect(result.current?.margin).toEqual('200000'); expect(result.current?.margin).toEqual('200000');
expect((useQuery as jest.Mock).mock.calls[1][1].variables.size).toEqual( expect((useQuery as jest.Mock).mock.calls[0][1].variables.size).toEqual(
order.size args.order.size
); );
}); });
@ -122,19 +102,13 @@ describe('useOrderMargin', () => {
}, },
}, },
}; };
const { result } = renderHook(() => const { result } = renderHook(() => useOrderMargin(args));
useOrderMargin({
order: order as OrderSubmissionBody['orderSubmission'],
market: market as MarketDealTicket,
partyId,
})
);
expect(result.current).toEqual(null); expect(result.current).toEqual(null);
const calledSize = new BigNumber(mockMarketPositions?.openVolume || 0) const calledSize = new BigNumber(mockMarketPositions?.openVolume || 0)
.plus(order.size) .plus(args.order.size)
.toString(); .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 calledSize
); );
}); });

View File

@ -2,15 +2,25 @@ import { BigNumber } from 'bignumber.js';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet'; import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { Schema } from '@vegaprotocol/types'; import { Schema } from '@vegaprotocol/types';
import { removeDecimal } from '@vegaprotocol/react-helpers'; import { removeDecimal } from '@vegaprotocol/react-helpers';
import type { MarketDealTicket } from '@vegaprotocol/market-list';
import { useMarketPositions } from './use-market-positions'; import { useMarketPositions } from './use-market-positions';
import { useMarketDataMarkPrice } from './use-market-data-mark-price';
import type { EstimateOrderQuery } from './__generated__/EstimateOrder'; import type { EstimateOrderQuery } from './__generated__/EstimateOrder';
import { useEstimateOrderQuery } 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']; order: OrderSubmissionBody['orderSubmission'];
market: MarketDealTicket; market: Market;
partyId: string; partyId: string;
} }
@ -36,28 +46,19 @@ export const useOrderMargin = ({
partyId, partyId,
}: Props): OrderMargin | null => { }: Props): OrderMargin | null => {
const marketPositions = useMarketPositions({ marketId: market.id, partyId }); const marketPositions = useMarketPositions({ marketId: market.id, partyId });
const markPriceData = useMarketDataMarkPrice(market.id); const priceForEstimate = getPriceForEstimate(order, market);
const { data } = useEstimateOrderQuery({ const { data } = useEstimateOrderQuery({
variables: { variables: {
marketId: market.id, marketId: market.id,
partyId, partyId,
price: order.price price: priceForEstimate,
? removeDecimal(order.price, market.decimalPlaces)
: markPriceData?.market?.data?.markPrice || '',
size: removeDecimal(order.size, market.positionDecimalPlaces), size: removeDecimal(order.size, market.positionDecimalPlaces),
side: side: order.side,
order.side === Schema.Side.SIDE_BUY
? Schema.Side.SIDE_BUY
: Schema.Side.SIDE_SELL,
timeInForce: order.timeInForce, timeInForce: order.timeInForce,
type: order.type, type: order.type,
}, },
skip: skip: !partyId || !market.id || !order.size || !priceForEstimate,
!partyId ||
!market.id ||
!order.size ||
!markPriceData?.market?.data?.markPrice,
}); });
if (data?.estimateOrder.marginLevels.initialLevel) { if (data?.estimateOrder.marginLevels.initialLevel) {
@ -69,8 +70,10 @@ export const useOrderMargin = ({
marketPositions?.balance || 0 marketPositions?.balance || 0
) )
).toString(); ).toString();
const { makerFee, liquidityFee, infrastructureFee } = const { makerFee, liquidityFee, infrastructureFee } =
data.estimateOrder.fee; data.estimateOrder.fee;
return { return {
margin, margin,
totalFees: fees, totalFees: fees,
@ -83,3 +86,32 @@ export const useOrderMargin = ({
} }
return null; 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 { Schema } from '@vegaprotocol/types';
import type { MarketDealTicket } from '@vegaprotocol/market-list';
export const isMarketInAuction = (market: MarketDealTicket) => { export const isMarketInAuction = (market: {
tradingMode: Schema.MarketTradingMode;
}) => {
return [ return [
Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION, Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION, Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,

View File

@ -16,12 +16,7 @@ export const validateMarketState = (state: Schema.MarketState) => {
); );
} }
if ( if (state === Schema.MarketState.STATE_PROPOSED) {
[
Schema.MarketState.STATE_PROPOSED,
Schema.MarketState.STATE_PENDING,
].includes(state)
) {
return t( return t(
`This market is ${marketTranslations( `This market is ${marketTranslations(
state state