feat: add fee information to stop order

This commit is contained in:
Bartłomiej Głownia
2023-07-25 11:34:35 +02:00
parent 7e242b567c
commit a53e75575e
7 changed files with 184 additions and 76 deletions
@@ -4,7 +4,11 @@ import {
useDealTicketTypeStore,
} from '../../hooks/use-type-store';
import { StopOrder } from './deal-ticket-stop-order';
import { useStaticMarketData, useMarket } from '@vegaprotocol/markets';
import {
useStaticMarketData,
useMarket,
useMarketPrice,
} from '@vegaprotocol/markets';
import { AsyncRenderer, Splash } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { DealTicket } from './deal-ticket';
@@ -33,6 +37,7 @@ export const DealTicketContainer = ({
loading: marketDataLoading,
reload,
} = useStaticMarketData(marketId);
const { data: marketPrice } = useMarketPrice(market?.id);
const create = useVegaTransactionStore((state) => state.create);
return (
<AsyncRenderer
@@ -46,12 +51,14 @@ export const DealTicketContainer = ({
type === DealTicketType.StopMarket ? (
<StopOrder
market={market}
marketPrice={marketPrice}
submit={(stopOrdersSubmission) => create({ stopOrdersSubmission })}
/>
) : (
<DealTicket
{...props}
market={market}
marketPrice={marketPrice}
marketData={marketData}
submit={(orderSubmission) => create({ orderSubmission })}
/>
@@ -4,6 +4,7 @@ import classnames from 'classnames';
import type { ReactNode } from 'react';
import { t } from '@vegaprotocol/i18n';
import { FeesBreakdown } from '@vegaprotocol/markets';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { Market } from '@vegaprotocol/markets';
@@ -24,6 +25,7 @@ import {
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
MARGIN_ACCOUNT_TOOLTIP_TEXT,
} from '../../constants';
import { useEstimateFees } from '../../hooks';
const emptyValue = '-';
@@ -76,26 +78,82 @@ export const DealTicketFeeDetail = ({
};
export interface DealTicketFeeDetailsProps {
assetSymbol: string;
order: OrderSubmissionBody['orderSubmission'];
market: Market;
notionalSize: string | null;
}
export const DealTicketFeeDetails = ({
assetSymbol,
order,
market,
notionalSize,
}: DealTicketFeeDetailsProps) => {
const feeEstimate = useEstimateFees(order);
const { settlementAsset: asset } =
market.tradableInstrument.instrument.product;
const { decimals: assetDecimals, quantum } = asset;
const marketDecimals = market.decimalPlaces;
const quoteName = market.tradableInstrument.instrument.product.quoteName;
return (
<>
<DealTicketFeeDetail
label={t('Notional')}
value={formatValue(notionalSize, marketDecimals)}
formattedValue={formatValue(notionalSize, marketDecimals)}
symbol={quoteName}
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
/>
<DealTicketFeeDetail
label={t('Fees')}
value={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
}
formattedValue={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`
}
labelDescription={
<>
<span>
{t(
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
)}
</span>
<FeesBreakdown
fees={feeEstimate?.fees}
feeFactors={market.fees.factors}
symbol={assetSymbol}
decimals={assetDecimals}
/>
</>
}
symbol={assetSymbol}
/>
</>
);
};
export interface DealTicketMarginDetailsProps {
generalAccountBalance?: string;
marginAccountBalance?: string;
market: Market;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
assetSymbol: string;
notionalSize: string | null;
feeEstimate: EstimateFeesQuery['estimateFees'] | undefined;
positionEstimate: EstimatePositionQuery['estimatePosition'];
}
export const DealTicketFeeDetails = ({
export const DealTicketMarginDetails = ({
marginAccountBalance,
generalAccountBalance,
assetSymbol,
feeEstimate,
market,
onMarketClick,
notionalSize,
positionEstimate,
}: DealTicketFeeDetailsProps) => {
}: DealTicketMarginDetailsProps) => {
const [breakdownDialog, setBreakdownDialog] = useState(false);
const { pubKey: partyId } = useVegaWallet();
const { data: currentMargins } = useDataProvider({
@@ -110,7 +168,6 @@ export const DealTicketFeeDetails = ({
const { settlementAsset: asset } =
market.tradableInstrument.instrument.product;
const { decimals: assetDecimals, quantum } = asset;
const marketDecimals = market.decimalPlaces;
let marginRequiredBestCase: string | undefined = undefined;
let marginRequiredWorstCase: string | undefined = undefined;
if (marginEstimate) {
@@ -251,41 +308,7 @@ export const DealTicketFeeDetails = ({
const quoteName = market.tradableInstrument.instrument.product.quoteName;
return (
<div>
<DealTicketFeeDetail
label={t('Notional')}
value={formatValue(notionalSize, marketDecimals)}
formattedValue={formatValue(notionalSize, marketDecimals)}
symbol={quoteName}
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
/>
<DealTicketFeeDetail
label={t('Fees')}
value={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
}
formattedValue={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`
}
labelDescription={
<>
<span>
{t(
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
)}
</span>
<FeesBreakdown
fees={feeEstimate?.fees}
feeFactors={market.fees.factors}
symbol={assetSymbol}
decimals={assetDecimals}
/>
</>
}
symbol={assetSymbol}
/>
<>
<DealTicketFeeDetail
label={t('Margin required')}
value={formatRange(
@@ -351,6 +374,6 @@ export const DealTicketFeeDetails = ({
onClose={onAccountBreakdownDialogClose}
/>
)}
</div>
</>
);
};
@@ -1,7 +1,12 @@
import { useRef, useCallback, useEffect } from 'react';
import { useRef, useCallback, useEffect, useMemo } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { StopOrdersSubmission } from '@vegaprotocol/wallet';
import { formatNumber, toDecimal, validateAmount } from '@vegaprotocol/utils';
import {
formatNumber,
removeDecimal,
toDecimal,
validateAmount,
} from '@vegaprotocol/utils';
import { useForm, Controller } from 'react-hook-form';
import * as Schema from '@vegaprotocol/types';
import {
@@ -14,12 +19,16 @@ import {
Select,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import type { Market } from '@vegaprotocol/markets';
import { getDerivedPrice, type Market } from '@vegaprotocol/markets';
import { t } from '@vegaprotocol/i18n';
import { ExpirySelector } from './expiry-selector';
import { SideSelector } from './side-selector';
import { timeInForceLabel, useOrder } from '@vegaprotocol/orders';
import { NoWalletWarning, REDUCE_ONLY_TOOLTIP } from './deal-ticket';
import {
NoWalletWarning,
REDUCE_ONLY_TOOLTIP,
useNotionalSize,
} from './deal-ticket';
import { TypeToggle } from './type-selector';
import {
useStopOrderFormValues,
@@ -32,22 +41,25 @@ import {
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-stop-order-submission';
import { DealTicketButton } from './deal-ticket-button';
import noop from 'lodash/noop';
import { useEstimateFees } from '../../hooks';
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
export interface StopOrderProps {
market: Market;
marketPrice?: string | null;
submit: (order: StopOrdersSubmission) => void;
}
const defaultValues: Partial<StopOrderFormValues> = {
type: Schema.OrderType.TYPE_MARKET,
side: Schema.Side.SIDE_SELL,
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
trigger: 'price',
direction: 'risesAbove',
expiryStrategy: 'submit',
};
export const StopOrder = ({ market, submit }: StopOrderProps) => {
export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
const { pubKey, isReadOnly } = useVegaWallet();
const setDealTicketType = useDealTicketTypeStore((state) => state.set);
const [, updateOrder] = useOrder(market.id);
@@ -91,7 +103,42 @@ export const StopOrder = ({ market, submit }: StopOrderProps) => {
const expire = watch('expire');
const trigger = watch('trigger');
const triggerPrice = watch('triggerPrice');
const timeInForce = watch('timeInForce');
const type = watch('type');
const rawPrice = watch('price');
const rawSize = watch('size');
const size = removeDecimal(rawSize, market.positionDecimalPlaces);
const price = useMemo(() => {
return (
marketPrice &&
getDerivedPrice(
{
type,
price: rawPrice && removeDecimal(rawPrice, market.decimalPlaces),
},
type === Schema.OrderType.TYPE_MARKET &&
trigger === 'price' &&
triggerPrice
? removeDecimal(triggerPrice, market.decimalPlaces)
: marketPrice
)
);
}, [
market.decimalPlaces,
marketPrice,
rawPrice,
trigger,
triggerPrice,
type,
]);
const notionalSize = useNotionalSize(
price,
size,
market.decimalPlaces,
market.positionDecimalPlaces
);
useEffect(() => {
const subscription = watch((value, { name, type }) => {
@@ -235,7 +282,10 @@ export const StopOrder = ({ market, submit }: StopOrderProps) => {
/>
</FormGroup>
) : (
<div className="text-sm text-right pt-5" data-testid="price">
<div
className="text-sm text-right pt-7 leading-10"
data-testid="price"
>
{priceFormatted && quoteName
? `~${priceFormatted} ${quoteName}`
: '-'}
@@ -506,6 +556,19 @@ export const StopOrder = ({ market, submit }: StopOrderProps) => {
)}
<NoWalletWarning pubKey={pubKey} isReadOnly={isReadOnly} asset={asset} />
<DealTicketButton side={side} label={t('Submit Stop Order')} />
<DealTicketFeeDetails
order={{
marketId: market.id,
price: price || undefined,
side,
size,
timeInForce,
type,
}}
notionalSize={notionalSize}
assetSymbol={asset.symbol}
market={market}
/>
</form>
);
};
@@ -4,7 +4,10 @@ import { memo, useCallback, useEffect, useState, useRef, useMemo } from 'react';
import { Controller } from 'react-hook-form';
import { DealTicketAmount } from './deal-ticket-amount';
import { DealTicketButton } from './deal-ticket-button';
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
import {
DealTicketFeeDetails,
DealTicketMarginDetails,
} from './deal-ticket-fee-details';
import { ExpirySelector } from './expiry-selector';
import { SideSelector } from './side-selector';
import { TimeInForceSelector } from './time-in-force-selector';
@@ -71,16 +74,36 @@ export const REDUCE_ONLY_TOOLTIP =
export interface DealTicketProps {
market: Market;
marketData: StaticMarketData;
marketPrice?: string | null;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
submit: (order: OrderSubmission) => void;
onClickCollateral?: () => void;
onDeposit: (assetId: string) => void;
}
export const useNotionalSize = (
price: string | null | undefined,
size: string | undefined,
decimalPlaces: number,
positionDecimalPlaces: number
) =>
useMemo(() => {
if (price && size) {
return removeDecimal(
toBigNum(size, positionDecimalPlaces).multipliedBy(
toBigNum(price, decimalPlaces)
),
decimalPlaces
);
}
return null;
}, [price, size, decimalPlaces, positionDecimalPlaces]);
export const DealTicket = ({
market,
onMarketClick,
marketData,
marketPrice,
submit,
onClickCollateral,
onDeposit,
@@ -90,7 +113,6 @@ export const DealTicket = ({
const updateStopOrderFormValues = useStopOrderFormValues(
(state) => state.update
);
const { data: marketPrice } = useMarketPrice(market.id);
// store last used tif for market so that when changing OrderType the previous TIF
// selection for that type is used when switching back
@@ -141,27 +163,13 @@ export const DealTicket = ({
);
}, [normalizedOrder, marketPrice]);
const notionalSize = useMemo(() => {
if (price && normalizedOrder?.size) {
return removeDecimal(
toBigNum(
normalizedOrder.size,
market.positionDecimalPlaces
).multipliedBy(toBigNum(price, market.decimalPlaces)),
market.decimalPlaces
);
}
return null;
}, [
const notionalSize = useNotionalSize(
price,
normalizedOrder?.size,
market.decimalPlaces,
market.positionDecimalPlaces,
]);
const feeEstimate = useEstimateFees(
normalizedOrder && { ...normalizedOrder, price: price || undefined }
market.positionDecimalPlaces
);
const { data: activeOrders } = useDataProvider({
dataProvider: activeOrdersProvider,
variables: { partyId: pubKey || '' },
@@ -582,10 +590,16 @@ export const DealTicket = ({
/>
<DealTicketButton side={order.side} />
<DealTicketFeeDetails
onMarketClick={onMarketClick}
feeEstimate={feeEstimate}
order={
normalizedOrder && { ...normalizedOrder, price: price || undefined }
}
notionalSize={notionalSize}
assetSymbol={assetSymbol}
market={market}
/>
<DealTicketMarginDetails
onMarketClick={onMarketClick}
assetSymbol={assetSymbol}
marginAccountBalance={marginAccountBalance}
generalAccountBalance={generalAccountBalance}
positionEstimate={positionEstimate?.estimatePosition}
@@ -1,6 +1,7 @@
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
import { formatForInput } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useRef } from 'react';
interface ExpirySelectorProps {
value?: string;
@@ -13,7 +14,8 @@ export const ExpirySelector = ({
onSelect,
errorMessage,
}: ExpirySelectorProps) => {
const date = value ? new Date(value) : new Date();
const now = useRef(new Date());
const date = value ? new Date(value) : now.current;
const dateFormatted = formatForInput(date);
const minDate = formatForInput(date);
return (
@@ -5,7 +5,7 @@ import type {
import { normalizeOrderSubmission } from '@vegaprotocol/wallet';
import type { StopOrderFormValues } from '../hooks/use-stop-order-form-values';
import * as Schema from '@vegaprotocol/types';
import { toNanoSeconds } from '@vegaprotocol/utils';
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
export const mapFormValuesToStopOrdersSubmission = (
data: StopOrderFormValues,
@@ -30,7 +30,7 @@ export const mapFormValuesToStopOrdersSubmission = (
),
};
if (data.trigger === 'price') {
stopOrderSetup.price = data.triggerPrice;
stopOrderSetup.price = removeDecimal(data.triggerPrice, decimalPlaces);
} else if (data.trigger === 'trailingPercentOffset') {
stopOrderSetup.trailingPercentOffset = (
Number(data.triggerTrailingPercentOffset) / 100
-1
View File
@@ -82,7 +82,6 @@ export const marketProvider = makeDerivedDataProvider<
);
export const useMarket = (marketId?: string) => {
console.log(marketId);
const variables = useMemo(() => ({ marketId: marketId || '' }), [marketId]);
return useDataProvider({
dataProvider: marketProvider,