Feat/1003 update size (#1041)

* feat(console-lite): add notional position size to trade size selector

* feat(console-lite): add estimated fees to size selector

* feat(console-lite): add estimated fees to review-trade and create shared estimates component

* feat(console-lite): add estimated fees as percentage
This commit is contained in:
Elmar 2022-08-15 14:41:52 +01:00 committed by GitHub
parent 83982ee3d8
commit 900205ed12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 299 additions and 89 deletions

View File

@ -152,26 +152,68 @@ describe('Market trade', () => {
}
});
it('notional position size should be present', () => {
if (markets?.length) {
cy.visit(`/trading/${markets[1].id}`);
connectVegaWallet();
cy.get('#step-1-control [aria-label^="Selected value"]').click();
cy.get('button[aria-label="Open short position"]').click();
cy.get('#step-2-control').click();
cy.get('#step-2-panel')
.find('dd')
.eq(0)
.find('button')
.should('have.text', '1');
cy.get('#step-2-panel').find('dd').eq(0).find('button').click();
cy.get('#step-2-panel')
.find('dd')
.eq(0)
.find('input')
.type('{backspace}2');
cy.get('#step-2-panel').find('dd').eq(0).find('button').click();
cy.get('#step-2-panel')
.find('dt')
.eq(2)
.should('have.text', 'Est. Position Size (tDAI)');
cy.get('#step-2-panel').find('dd').eq(2).should('have.text', '197.86012');
}
});
it('total fees should be displayed', () => {
if (markets?.length) {
cy.visit(`/trading/${markets[1].id}`);
connectVegaWallet();
cy.get('#step-1-control [aria-label^="Selected value"]').click();
cy.get('button[aria-label="Open short position"]').click();
cy.get('#step-2-control').click();
cy.get('#step-2-panel')
.find('dt')
.eq(3)
.should('have.text', 'Est. Fees (tDAI)');
cy.get('#step-2-panel')
.find('dd')
.eq(3)
.should('have.text', '3.00000 (3.03%)');
}
});
it('order review should display proper calculations', () => {
if (markets?.length) {
cy.visit(`/trading/${markets[0].id}`);
connectVegaWallet();
cy.get('h3').contains('Review Trade').click();
cy.getByTestId('key-value-table')
.find('dl')
.eq(1)
.find('dd div')
.should('have.text', '25.78726');
cy.getByTestId('key-value-table')
.find('dl')
.eq(2)
.find('dd div')
.should('have.text', '1.00000');
cy.getByTestId('key-value-table')
.find('dl')
cy.get('#step-3-panel').find('dd').eq(1).should('have.text', '1');
cy.get('#step-3-panel').find('dd').eq(2).should('have.text', '98.93006');
cy.get('#step-3-panel')
.find('dd')
.eq(3)
.find('dd div')
.should('have.text', ' - ');
.should('have.text', '3.00000 (3.03%)');
cy.get('#step-3-panel').find('dd').eq(4).should('have.text', ' - ');
cy.getByTestId('place-order').click();
cy.getByTestId('dialog-title').should(
'have.text',

View File

@ -1,7 +1,12 @@
export const generateEstimateOrder = () => {
return {
estimateOrder: {
totalFeeAmount: '16085.09240212.7380425.46',
fee: {
__typename: 'TradeFee',
makerFee: '100000',
liquidityFee: '100000',
infrastructureFee: '100000',
},
marginLevels: {
initialLevel: '2844054.80937741220203',
__typename: 'MarginLevels',

View File

@ -0,0 +1,78 @@
import React from 'react';
import type { ReactNode } from 'react';
import { t } from '@vegaprotocol/react-helpers';
interface DealTicketEstimatesProps {
quoteName?: string;
price?: string;
estCloseOut?: string;
estMargin?: string;
fees?: string;
notionalSize?: string;
size?: string;
}
interface DataTitleProps {
children: ReactNode;
quoteName?: string;
}
const DataTitle = ({ children, quoteName = '' }: DataTitleProps) => (
<dt>
{children}
{quoteName && <small> ({quoteName})</small>}
</dt>
);
export const DealTicketEstimates = ({
price,
quoteName,
estCloseOut,
estMargin,
fees,
notionalSize,
size,
}: DealTicketEstimatesProps) => (
<dl className="text-black dark:text-white">
{size && (
<div className="flex justify-between mb-8">
<DataTitle>{t('No. of Contracts')}</DataTitle>
<dd>{size}</dd>
</div>
)}
{price && (
<div className="flex justify-between mb-8">
<DataTitle>{t('Est. Price')}</DataTitle>
<dd>{price}</dd>
</div>
)}
{notionalSize && (
<div className="flex justify-between mb-8">
<DataTitle quoteName={quoteName}>{t('Est. Position Size')}</DataTitle>
<dd>{notionalSize}</dd>
</div>
)}
{fees && (
<div className="flex justify-between mb-8">
<DataTitle quoteName={quoteName}>{t('Est. Fees')}</DataTitle>
<dd>{fees}</dd>
</div>
)}
{estMargin && (
<div className="flex justify-between mb-8">
<DataTitle quoteName={quoteName}>{t('Est. Margin')}</DataTitle>
<dd>{estMargin}</dd>
</div>
)}
{estCloseOut && (
<div className="flex justify-between">
<dt>
<span>{t('Est. Close out')}</span>
&nbsp;
<small>({quoteName})</small>
</dt>
<dd>{estCloseOut}</dd>
</div>
)}
</dl>
);

View File

@ -10,6 +10,7 @@ import {
FormGroup,
} from '@vegaprotocol/ui-toolkit';
import { BigNumber } from 'bignumber.js';
import { DealTicketEstimates } from './deal-ticket-estimates';
interface DealTicketSizeProps {
step: number;
@ -22,7 +23,9 @@ interface DealTicketSizeProps {
price: string;
estCloseOut: string;
estMargin: string;
fees: string;
positionDecimalPlaces: number;
notionalSize: string;
}
const getSizeLabel = (value: number): string => {
@ -47,6 +50,8 @@ export const DealTicketSize = ({
onValueChange,
estCloseOut,
positionDecimalPlaces,
fees,
notionalSize,
}: DealTicketSizeProps) => {
const sizeRatios = [0, 25, 50, 75, 100];
const [inputValue, setInputValue] = useState(value);
@ -142,11 +147,7 @@ export const DealTicketSize = ({
<dl className="text-black dark:text-white">
<div className="flex items-center justify-between mb-8">
<dt>
<span>{t('Size')}</span>
&nbsp;
<small>({quoteName})</small>
</dt>
<dt>{t('Contracts')}</dt>
<dd className="flex justify-end w-full">
<FormGroup
className="mb-0 flex items-center"
@ -187,15 +188,14 @@ export const DealTicketSize = ({
</FormGroup>
</dd>
</div>
<div className="flex justify-between mb-8">
<dt>{t('Est. price')}</dt>
<dd>{price}</dd>
</div>
<div className="flex justify-between">
<dt>{t('Est. close out')}</dt>
<dd>{estCloseOut}</dd>
</div>
</dl>
<DealTicketEstimates
quoteName={quoteName}
fees={fees}
estCloseOut={estCloseOut}
price={price}
notionalSize={notionalSize}
/>
</div>
);
};

View File

@ -72,6 +72,14 @@ export const DealTicketSteps = ({
market,
partyId: keypair?.pub || '',
});
const value = new BigNumber(orderSize).toNumber();
const price =
market.depth.lastTrade &&
addDecimal(market.depth.lastTrade.price, market.decimalPlaces);
const emptyString = ' - ';
const [notionalSize, setNotionalSize] = useState<string | null>(null);
const [fees, setFees] = useState<string | null>(null);
const maxTrade = useMaximumPositionSize({
partyId: keypair?.pub || '',
@ -112,6 +120,28 @@ export const DealTicketSteps = ({
}
};
useEffect(() => {
if (market?.depth?.lastTrade?.price) {
const size = new BigNumber(market.depth.lastTrade.price)
.multipliedBy(value)
.toNumber();
setNotionalSize(addDecimal(size, market.decimalPlaces));
}
}, [market, value]);
useEffect(() => {
if (estMargin?.fees && notionalSize) {
const percentage = new BigNumber(estMargin?.fees)
.dividedBy(notionalSize)
.multipliedBy(100)
.decimalPlaces(2)
.toString();
setFees(`${estMargin.fees} (${percentage}%)`);
}
}, [estMargin, notionalSize]);
const transactionStatus =
transaction.status === VegaTxStatus.Requested ||
transaction.status === VegaTxStatus.Pending
@ -163,15 +193,16 @@ export const DealTicketSteps = ({
onValueChange={onSizeChange}
value={new BigNumber(orderSize).toNumber()}
name="size"
price={
market.depth.lastTrade
? addDecimal(market.depth.lastTrade.price, market.decimalPlaces)
: ''
}
price={price || emptyString}
positionDecimalPlaces={market.positionDecimalPlaces}
quoteName={market.tradableInstrument.instrument.product.quoteName}
quoteName={
market.tradableInstrument.instrument.product.settlementAsset
.symbol
}
notionalSize={notionalSize || emptyString}
estCloseOut={estCloseOut}
estMargin={estMargin || ' - '}
fees={fees || emptyString}
estMargin={estMargin?.margin || emptyString}
/>
) : (
'loading...'
@ -193,7 +224,14 @@ export const DealTicketSteps = ({
transactionStatus={transactionStatus}
order={order}
estCloseOut={estCloseOut}
estMargin={estMargin || ' - '}
estMargin={estMargin?.margin || emptyString}
price={price || emptyString}
quoteName={
market.tradableInstrument.instrument.product.settlementAsset
.symbol
}
notionalSize={notionalSize || emptyString}
fees={fees || emptyString}
/>
<TransactionDialog
title={getOrderDialogTitle(finalizedOrder?.status)}

View File

@ -1,7 +1,6 @@
import { addDecimal, formatNumber, t } from '@vegaprotocol/react-helpers';
import { t } from '@vegaprotocol/react-helpers';
import {
Button,
Icon,
KeyValueTable,
KeyValueTableRow,
} from '@vegaprotocol/ui-toolkit';
@ -17,7 +16,7 @@ import type {
MarketTags,
MarketTagsVariables,
} from './__generated__/MarketTags';
import { IconNames } from '@blueprintjs/icons';
import { DealTicketEstimates } from './deal-ticket-estimates';
export const MARKET_TAGS_QUERY = gql`
query MarketTags($marketId: ID!) {
@ -40,6 +39,10 @@ interface Props {
order: Order;
estCloseOut: string;
estMargin: string;
quoteName: string;
price: string;
fees: string;
notionalSize: string;
}
export default ({
@ -48,7 +51,10 @@ export default ({
order,
transactionStatus,
estCloseOut,
estMargin,
quoteName,
fees,
price,
notionalSize,
}: Props) => {
const { data: tagsData } = useQuery<MarketTags, MarketTagsVariables>(
MARKET_TAGS_QUERY,
@ -88,41 +94,20 @@ export default ({
</div>
</div>
<div className="text-blue">
@{' '}
{market.depth.lastTrade
? addDecimal(market.depth.lastTrade.price, market.decimalPlaces)
: ' - '}{' '}
{`@ ${price} `}
<span className="text-ui-small inline">(EST)</span>
</div>
</KeyValueTableRow>
<KeyValueTableRow noBorder>
<>{t('Est. margin')}</>
<div className="text-black dark:text-white flex gap-x-5 items-center">
{estMargin}
<Icon name={IconNames.ISSUE} className="rotate-180" />
</div>
</KeyValueTableRow>
<KeyValueTableRow noBorder>
<>
{t('Size')}{' '}
<div className="text-ui-small inline">
({market.tradableInstrument.instrument.product.quoteName})
</div>
</>
<div className="text-black dark:text-white flex gap-x-5 items-center">
{formatNumber(order.size, market.decimalPlaces)}
<Icon name={IconNames.ISSUE} className="rotate-180" />
</div>
</KeyValueTableRow>
<KeyValueTableRow noBorder>
<>{t('Est. close out')}</>
<div className="text-black dark:text-white flex gap-x-5 items-center">
{estCloseOut}
<Icon name={IconNames.ISSUE} className="rotate-180" />
</div>
</KeyValueTableRow>
</KeyValueTable>
<DealTicketEstimates
size={order.size}
quoteName={quoteName}
fees={fees}
estCloseOut={estCloseOut}
notionalSize={notionalSize}
/>
<Button
className="w-full !py-8 mt-64 max-w-sm"
boxShadow={false}

View File

@ -9,6 +9,22 @@ import { Side, OrderTimeInForce, OrderType } from "@vegaprotocol/types";
// GraphQL query operation: EstimateOrder
// ====================================================
export interface EstimateOrder_estimateOrder_fee {
__typename: "TradeFee";
/**
* The maker fee, aggressive party to the other party (the one who had an order in the book)
*/
makerFee: string;
/**
* The infrastructure fee, a fee paid to the node runner to maintain the vega network
*/
infrastructureFee: string;
/**
* The fee paid to the market makers to provide liquidity in the market
*/
liquidityFee: string;
}
export interface EstimateOrder_estimateOrder_marginLevels {
__typename: "MarginLevels";
/**
@ -20,9 +36,9 @@ export interface EstimateOrder_estimateOrder_marginLevels {
export interface EstimateOrder_estimateOrder {
__typename: "OrderEstimate";
/**
* The total estimated amount of fee if the order was to trade
* The estimated fee if the order was to trade
*/
totalFeeAmount: string;
fee: EstimateOrder_estimateOrder_fee;
/**
* The margin requirement for this order
*/

View File

@ -8,6 +8,11 @@ import useOrderMargin from './use-order-margin';
let mockEstimateData = {
estimateOrder: {
fee: {
makerFee: '100000.000',
infrastructureFee: '100000.000',
liquidityFee: '100000.000',
},
marginLevels: {
initialLevel: '200000',
},
@ -53,7 +58,7 @@ describe('useOrderMargin Hook', () => {
partyId,
})
);
expect(result.current).toEqual('100000');
expect(result.current?.margin).toEqual('100000');
const calledSize = new BigNumber(mockMarketPositions?.openVolume || 0)
.plus(order.size)
@ -63,6 +68,17 @@ describe('useOrderMargin Hook', () => {
);
});
it('fees should be properly calculated', () => {
const { result } = renderHook(() =>
useOrderMargin({
order: order as Order,
market: market as DealTicketQuery_market,
partyId,
})
);
expect(result.current?.fees).toEqual('300000');
});
it('if there is no positions initialMargin should not be subtracted', () => {
mockMarketPositions = null;
const { result } = renderHook(() =>
@ -72,7 +88,7 @@ describe('useOrderMargin Hook', () => {
partyId,
})
);
expect(result.current).toEqual('200000');
expect(result.current?.margin).toEqual('200000');
expect((useQuery as jest.Mock).mock.calls[1][1].variables.size).toEqual(
order.size
@ -82,6 +98,11 @@ describe('useOrderMargin Hook', () => {
it('if api fails, should return empty value', () => {
mockEstimateData = {
estimateOrder: {
fee: {
makerFee: '100000.000',
infrastructureFee: '100000.000',
liquidityFee: '100000.000',
},
marginLevels: {
initialLevel: '',
},
@ -94,7 +115,7 @@ describe('useOrderMargin Hook', () => {
partyId,
})
);
expect(result.current).toEqual(' - ');
expect(result.current).toEqual(null);
const calledSize = new BigNumber(mockMarketPositions?.openVolume || 0)
.plus(order.size)

View File

@ -4,6 +4,7 @@ import { gql, useQuery } from '@apollo/client';
import type {
EstimateOrder,
EstimateOrderVariables,
EstimateOrder_estimateOrder_fee,
} from './__generated__/estimateOrder';
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
import { OrderTimeInForce, OrderType, Side } from '@vegaprotocol/types';
@ -37,7 +38,11 @@ export const ESTIMATE_ORDER_QUERY = gql`
expiration: $expiration
type: $type
) {
totalFeeAmount
fee {
makerFee
infrastructureFee
liquidityFee
}
marginLevels {
initialLevel
}
@ -65,7 +70,22 @@ const types: Record<VegaWalletOrderType, OrderType> = {
[VegaWalletOrderType.Limit]: OrderType.Limit,
};
const useOrderMargin = ({ order, market, partyId }: Props) => {
const addFees = (feeObj: EstimateOrder_estimateOrder_fee) => {
return new BigNumber(feeObj.makerFee)
.plus(feeObj.liquidityFee)
.plus(feeObj.infrastructureFee);
};
export interface OrderMargin {
margin: string;
fees: string | null;
}
const useOrderMargin = ({
order,
market,
partyId,
}: Props): OrderMargin | null => {
const marketPositions = useMarketPositions({ marketId: market.id, partyId });
const markPriceData = useMarketData(market.id);
const { data } = useQuery<EstimateOrder, EstimateOrderVariables>(
@ -97,7 +117,10 @@ const useOrderMargin = ({ order, market, partyId }: Props) => {
);
if (data?.estimateOrder.marginLevels.initialLevel) {
return addDecimal(
const fees =
data?.estimateOrder?.fee && addFees(data.estimateOrder.fee).toString();
return {
margin: addDecimal(
BigNumber.maximum(
0,
new BigNumber(data.estimateOrder.marginLevels.initialLevel).minus(
@ -105,9 +128,11 @@ const useOrderMargin = ({ order, market, partyId }: Props) => {
)
).toString(),
market.decimalPlaces
);
),
fees: addDecimal(fees, market.decimalPlaces),
};
}
return ' - ';
return null;
};
export default useOrderMargin;