feat: [console-lite] - trading - order review (#842)
* feat: [console-lite] - trading - order review * feat: [console-lite] - trading - order review - fix margin calcs * feat: [console-lite] - add order margin calcs * feat: [console-lite] - add order margin calcs - improve margin calcs * feat: [console-lite] - add order margin calcs - add int test * feat: [console-lite] - add order margin calcs - improve int test Co-authored-by: maciek <maciek@vegaprotocol.io>
This commit is contained in:
parent
f1c3eab914
commit
6c976a0da3
@ -24,3 +24,4 @@ NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
|
||||
NX_VEGA_URL=https://lb.testnet.vega.xyz/query
|
||||
NX_VEGA_ENV=TESTNET
|
||||
NX_VEGA_REST=https://lb.testnet.vega.xyz/datanode/rest
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789/api/v1
|
||||
|
@ -18,4 +18,22 @@ module.exports = defineConfig({
|
||||
viewportWidth: 1440,
|
||||
viewportHeight: 900,
|
||||
},
|
||||
env: {
|
||||
TRADING_TEST_VEGA_WALLET_NAME: 'UI_Trading_Test',
|
||||
ETHEREUM_PROVIDER_URL:
|
||||
'https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8',
|
||||
VEGA_PUBLIC_KEY:
|
||||
'47836c253520d2661bf5bed6339c0de08fd02cf5d4db0efee3b4373f20c7d278',
|
||||
VEGA_PUBLIC_KEY2:
|
||||
'1a18cdcaaa4f44a57b35a4e9b77e0701c17a476f2b407620f8c17371740cf2e4',
|
||||
TRUNCATED_VEGA_PUBLIC_KEY: '47836c…c7d278',
|
||||
TRUNCATED_VEGA_PUBLIC_KEY2: '1a18cd…0cf2e4',
|
||||
ETHEREUM_WALLET_ADDRESS: '0x265Cc6d39a1B53d0d92068443009eE7410807158',
|
||||
ETHERSCAN_URL: 'https://ropsten.etherscan.io',
|
||||
tsConfig: 'tsconfig.json',
|
||||
TAGS: 'not @todo and not @ignore and not @manual',
|
||||
TRADING_TEST_VEGA_WALLET_PASSPHRASE: '123',
|
||||
ETH_WALLET_MNEMONIC:
|
||||
'ugly gallery notice network true range brave clarify flat logic someone chunk',
|
||||
},
|
||||
});
|
||||
|
@ -1,6 +1,22 @@
|
||||
import { aliasQuery } from '@vegaprotocol/cypress';
|
||||
import { generateSimpleMarkets } from '../support/mocks/generate-markets';
|
||||
import { generateDealTicket } from '../support/mocks/generate-deal-ticket';
|
||||
import { generateMarketTags } from '../support/mocks/generate-market-tags';
|
||||
import { generateMarketPositions } from '../support/mocks/generate-market-positions';
|
||||
import { generateEstimateOrder } from '../support/mocks/generate-estimate-order';
|
||||
import { generatePartyBalance } from '../support/mocks/generate-party-balance';
|
||||
|
||||
const connectVegaWallet = () => {
|
||||
const form = 'rest-connector-form';
|
||||
const walletName = Cypress.env('TRADING_TEST_VEGA_WALLET_NAME');
|
||||
const walletPassphrase = Cypress.env('TRADING_TEST_VEGA_WALLET_PASSPHRASE');
|
||||
|
||||
cy.getByTestId('connect-vega-wallet').click();
|
||||
cy.getByTestId('connectors-list').find('button').click();
|
||||
cy.getByTestId(form).find('#wallet').click().type(walletName);
|
||||
cy.getByTestId(form).find('#passphrase').click().type(walletPassphrase);
|
||||
cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
|
||||
};
|
||||
|
||||
describe('Market trade', () => {
|
||||
let markets;
|
||||
@ -8,6 +24,10 @@ describe('Market trade', () => {
|
||||
cy.mockGQL((req) => {
|
||||
aliasQuery(req, 'SimpleMarkets', generateSimpleMarkets());
|
||||
aliasQuery(req, 'DealTicketQuery', generateDealTicket());
|
||||
aliasQuery(req, 'MarketTags', generateMarketTags());
|
||||
aliasQuery(req, 'MarketPositions', generateMarketPositions());
|
||||
aliasQuery(req, 'EstimateOrder', generateEstimateOrder());
|
||||
aliasQuery(req, 'PartyBalanceQuery', generatePartyBalance());
|
||||
});
|
||||
cy.visit('/markets');
|
||||
cy.wait('@SimpleMarkets').then((response) => {
|
||||
@ -64,4 +84,32 @@ describe('Market trade', () => {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
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', '3.44055');
|
||||
cy.getByTestId('key-value-table')
|
||||
.find('dl')
|
||||
.eq(2)
|
||||
.find('dd div')
|
||||
.should('have.text', '1.00000');
|
||||
cy.getByTestId('key-value-table')
|
||||
.find('dl')
|
||||
.eq(3)
|
||||
.find('dd div')
|
||||
.should('have.text', '-785.81045');
|
||||
cy.getByTestId('place-order').click();
|
||||
cy.getByTestId('dialog-title').should(
|
||||
'have.text',
|
||||
'Confirm transaction in wallet'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,12 @@
|
||||
export const generateEstimateOrder = () => {
|
||||
return {
|
||||
estimateOrder: {
|
||||
totalFeeAmount: '16085.09240212.7380425.46',
|
||||
marginLevels: {
|
||||
initialLevel: '2844054.80937741220203',
|
||||
__typename: 'MarginLevels',
|
||||
},
|
||||
__typename: 'OrderEstimate',
|
||||
},
|
||||
};
|
||||
};
|
@ -0,0 +1,43 @@
|
||||
export const generateMarketPositions = () => {
|
||||
return {
|
||||
party: {
|
||||
id: '2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
|
||||
positionsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
openVolume: '3',
|
||||
market: {
|
||||
id: '2751c508f9759761f912890f37fb3f97a00300bf7685c02a56a86e05facfe221',
|
||||
accounts: [
|
||||
{ balance: '0', __typename: 'Account' },
|
||||
{ balance: '0', __typename: 'Account' },
|
||||
],
|
||||
__typename: 'Market',
|
||||
},
|
||||
__typename: 'Position',
|
||||
},
|
||||
__typename: 'PositionEdge',
|
||||
},
|
||||
{
|
||||
node: {
|
||||
openVolume: '12',
|
||||
market: {
|
||||
id: 'first-btcusd-id',
|
||||
accounts: [
|
||||
{ balance: '10', __typename: 'Account' },
|
||||
{ balance: '15', __typename: 'Account' },
|
||||
],
|
||||
__typename: 'Market',
|
||||
},
|
||||
__typename: 'Position',
|
||||
},
|
||||
__typename: 'PositionEdge',
|
||||
},
|
||||
],
|
||||
__typename: 'PositionConnection',
|
||||
},
|
||||
__typename: 'Party',
|
||||
},
|
||||
};
|
||||
};
|
@ -0,0 +1,25 @@
|
||||
export const generateMarketTags = () => {
|
||||
return {
|
||||
market: {
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
metadata: {
|
||||
tags: [
|
||||
'formerly:2839D9B2329C9E70',
|
||||
'base:AAVE',
|
||||
'quote:DAI',
|
||||
'class:fx/crypto',
|
||||
'monthly',
|
||||
'sector:defi',
|
||||
'settlement:2022-08-01',
|
||||
],
|
||||
__typename: 'InstrumentMetadata',
|
||||
},
|
||||
__typename: 'Instrument',
|
||||
},
|
||||
__typename: 'TradableInstrument',
|
||||
},
|
||||
__typename: 'Market',
|
||||
},
|
||||
};
|
||||
};
|
@ -0,0 +1,53 @@
|
||||
export const generatePartyBalance = () => {
|
||||
return {
|
||||
party: {
|
||||
accounts: [
|
||||
{
|
||||
balance: '88474051',
|
||||
asset: {
|
||||
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
|
||||
symbol: 'tDAI',
|
||||
name: 'tDAI TEST',
|
||||
decimals: 5,
|
||||
__typename: 'Asset',
|
||||
},
|
||||
__typename: 'Account',
|
||||
},
|
||||
{
|
||||
balance: '100000000',
|
||||
asset: {
|
||||
id: '8b52d4a3a4b0ffe733cddbc2b67be273816cfeb6ca4c8b339bac03ffba08e4e4',
|
||||
symbol: 'tEURO',
|
||||
name: 'tEURO TEST',
|
||||
decimals: 5,
|
||||
__typename: 'Asset',
|
||||
},
|
||||
__typename: 'Account',
|
||||
},
|
||||
{
|
||||
balance: '3412867',
|
||||
asset: {
|
||||
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
|
||||
symbol: 'tDAI',
|
||||
name: 'tDAI TEST',
|
||||
decimals: 5,
|
||||
__typename: 'Asset',
|
||||
},
|
||||
__typename: 'Account',
|
||||
},
|
||||
{
|
||||
balance: '70007',
|
||||
asset: {
|
||||
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
|
||||
symbol: 'tDAI',
|
||||
name: 'tDAI TEST',
|
||||
decimals: 5,
|
||||
__typename: 'Asset',
|
||||
},
|
||||
__typename: 'Account',
|
||||
},
|
||||
],
|
||||
__typename: 'Party',
|
||||
},
|
||||
};
|
||||
};
|
@ -21,3 +21,4 @@ NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
|
||||
NX_VEGA_CONFIG_URL="https://static.vega.xyz/assets/testnet-network.json"
|
||||
NX_VEGA_ENV = 'TESTNET'
|
||||
NX_VEGA_URL="https://lb.testnet.vega.xyz/query"
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789/api/v1
|
||||
|
51
apps/simple-trading-app/src/app/components/deal-ticket/__generated__/MarketTags.ts
generated
Normal file
51
apps/simple-trading-app/src/app/components/deal-ticket/__generated__/MarketTags.ts
generated
Normal file
@ -0,0 +1,51 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: MarketTags
|
||||
// ====================================================
|
||||
|
||||
export interface MarketTags_market_tradableInstrument_instrument_metadata {
|
||||
__typename: "InstrumentMetadata";
|
||||
/**
|
||||
* An arbitrary list of tags to associated to associate to the Instrument (string list)
|
||||
*/
|
||||
tags: string[] | null;
|
||||
}
|
||||
|
||||
export interface MarketTags_market_tradableInstrument_instrument {
|
||||
__typename: "Instrument";
|
||||
/**
|
||||
* Metadata for this instrument
|
||||
*/
|
||||
metadata: MarketTags_market_tradableInstrument_instrument_metadata;
|
||||
}
|
||||
|
||||
export interface MarketTags_market_tradableInstrument {
|
||||
__typename: "TradableInstrument";
|
||||
/**
|
||||
* An instance of or reference to a fully specified instrument.
|
||||
*/
|
||||
instrument: MarketTags_market_tradableInstrument_instrument;
|
||||
}
|
||||
|
||||
export interface MarketTags_market {
|
||||
__typename: "Market";
|
||||
/**
|
||||
* An instance of or reference to a tradable instrument.
|
||||
*/
|
||||
tradableInstrument: MarketTags_market_tradableInstrument;
|
||||
}
|
||||
|
||||
export interface MarketTags {
|
||||
/**
|
||||
* An instrument that is trading on the VEGA network
|
||||
*/
|
||||
market: MarketTags_market | null;
|
||||
}
|
||||
|
||||
export interface MarketTagsVariables {
|
||||
marketId: string;
|
||||
}
|
@ -61,7 +61,7 @@ export const DealTicketContainer = () => {
|
||||
isWalletConnected={!!keypair?.pub}
|
||||
/>
|
||||
)}
|
||||
<DealTicketSteps market={data.market} />
|
||||
<DealTicketSteps market={data.market} partyData={partyData} />
|
||||
</DealTicketManager>
|
||||
)}
|
||||
</Container>
|
||||
|
@ -1,9 +1,16 @@
|
||||
import * as React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { Stepper } from '../stepper';
|
||||
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
|
||||
import { Button, InputError } from '@vegaprotocol/ui-toolkit';
|
||||
import { DealTicketAmount, MarketSelector } from '@vegaprotocol/deal-ticket';
|
||||
import { InputError } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
DealTicketAmount,
|
||||
getDialogTitle,
|
||||
getDialogIntent,
|
||||
getDialogIcon,
|
||||
MarketSelector,
|
||||
} from '@vegaprotocol/deal-ticket';
|
||||
import type { Order } from '@vegaprotocol/orders';
|
||||
import { VegaTxStatus } from '@vegaprotocol/wallet';
|
||||
import { t, addDecimal, toDecimal } from '@vegaprotocol/react-helpers';
|
||||
@ -11,17 +18,22 @@ import {
|
||||
getDefaultOrder,
|
||||
useOrderValidation,
|
||||
useOrderSubmit,
|
||||
OrderFeedback,
|
||||
} from '@vegaprotocol/orders';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import MarketNameRenderer from '../simple-market-list/simple-market-renderer';
|
||||
import SideSelector, { SIDE_NAMES } from './side-selector';
|
||||
import ReviewTrade from './review-trade';
|
||||
import type { PartyBalanceQuery } from './__generated__/PartyBalanceQuery';
|
||||
|
||||
interface DealTicketMarketProps {
|
||||
market: DealTicketQuery_market;
|
||||
partyData?: PartyBalanceQuery;
|
||||
}
|
||||
|
||||
export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
|
||||
export const DealTicketSteps = ({
|
||||
market,
|
||||
partyData,
|
||||
}: DealTicketMarketProps) => {
|
||||
const navigate = useNavigate();
|
||||
const setMarket = useCallback(
|
||||
(marketId) => {
|
||||
@ -45,6 +57,7 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
|
||||
const orderType = watch('type');
|
||||
const orderTimeInForce = watch('timeInForce');
|
||||
const orderSide = watch('side');
|
||||
const order = watch();
|
||||
|
||||
const { message: invalidText, isDisabled } = useOrderValidation({
|
||||
step,
|
||||
@ -54,7 +67,8 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
|
||||
fieldErrors: errors,
|
||||
});
|
||||
|
||||
const { submit, transaction } = useOrderSubmit(market);
|
||||
const { submit, transaction, finalizedOrder, TransactionDialog } =
|
||||
useOrderSubmit(market);
|
||||
|
||||
const transactionStatus =
|
||||
transaction.status === VegaTxStatus.Requested ||
|
||||
@ -101,7 +115,7 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
|
||||
component: (
|
||||
<DealTicketAmount
|
||||
orderType={orderType}
|
||||
step={0.02}
|
||||
step={step}
|
||||
register={register}
|
||||
price={
|
||||
market.depth.lastTrade
|
||||
@ -121,17 +135,20 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
|
||||
{invalidText}
|
||||
</InputError>
|
||||
)}
|
||||
<Button
|
||||
className="w-full mb-8"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
disabled={transactionStatus === 'pending' || isDisabled}
|
||||
data-testid="place-order"
|
||||
<ReviewTrade
|
||||
market={market}
|
||||
isDisabled={isDisabled}
|
||||
transactionStatus={transactionStatus}
|
||||
order={order}
|
||||
partyData={partyData}
|
||||
/>
|
||||
<TransactionDialog
|
||||
title={getDialogTitle(finalizedOrder?.status)}
|
||||
intent={getDialogIntent(finalizedOrder?.status)}
|
||||
icon={getDialogIcon(finalizedOrder?.status)}
|
||||
>
|
||||
{transactionStatus === 'pending'
|
||||
? t('Pending...')
|
||||
: t('Place order')}
|
||||
</Button>
|
||||
<OrderFeedback transaction={transaction} order={finalizedOrder} />
|
||||
</TransactionDialog>
|
||||
</div>
|
||||
),
|
||||
disabled: true,
|
||||
|
@ -0,0 +1,146 @@
|
||||
import { addDecimal, formatNumber, t } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
|
||||
import type { Order } from '@vegaprotocol/orders';
|
||||
import { SIDE_NAMES } from './side-selector';
|
||||
import { useVegaWallet, VegaWalletOrderSide } from '@vegaprotocol/wallet';
|
||||
import SimpleMarketExpires from '../simple-market-list/simple-market-expires';
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import type {
|
||||
MarketTags,
|
||||
MarketTagsVariables,
|
||||
} from './__generated__/MarketTags';
|
||||
import useOrderMargin from '../../hooks/use-order-margin';
|
||||
import useOrderCloseOut from '../../hooks/use-order-closeout';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import type { PartyBalanceQuery } from './__generated__/PartyBalanceQuery';
|
||||
|
||||
export const MARKET_TAGS_QUERY = gql`
|
||||
query MarketTags($marketId: ID!) {
|
||||
market(id: $marketId) {
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
metadata {
|
||||
tags
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
market: DealTicketQuery_market;
|
||||
isDisabled: boolean;
|
||||
transactionStatus?: string;
|
||||
order: Order;
|
||||
partyData?: PartyBalanceQuery;
|
||||
}
|
||||
|
||||
export default ({
|
||||
isDisabled,
|
||||
market,
|
||||
order,
|
||||
transactionStatus,
|
||||
partyData,
|
||||
}: Props) => {
|
||||
const { keypair } = useVegaWallet();
|
||||
const { data: tagsData } = useQuery<MarketTags, MarketTagsVariables>(
|
||||
MARKET_TAGS_QUERY,
|
||||
{
|
||||
variables: { marketId: market.id },
|
||||
}
|
||||
);
|
||||
const estMargin = useOrderMargin({
|
||||
order,
|
||||
market,
|
||||
partyId: keypair?.pub || '',
|
||||
});
|
||||
const estCloseOut = useOrderCloseOut({ order, market, partyData });
|
||||
return (
|
||||
<div className="mb-8 text-black dark:text-white">
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow noBorder>
|
||||
<div className="flex flex-none gap-x-5 items-center">
|
||||
<div
|
||||
className={classNames(
|
||||
{
|
||||
'buyButton dark:buyButtonDark':
|
||||
order.side === VegaWalletOrderSide.Buy,
|
||||
'sellButton dark:sellButtonDark':
|
||||
order.side === VegaWalletOrderSide.Sell,
|
||||
},
|
||||
'px-8 py-4 inline text-ui-small'
|
||||
)}
|
||||
>
|
||||
{SIDE_NAMES[order.side]}
|
||||
</div>
|
||||
<div>{market.tradableInstrument.instrument.product.quoteName}</div>
|
||||
<div>
|
||||
{tagsData?.market?.tradableInstrument.instrument.metadata
|
||||
.tags && (
|
||||
<SimpleMarketExpires
|
||||
tags={
|
||||
tagsData?.market.tradableInstrument.instrument.metadata.tags
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-blue">
|
||||
@{' '}
|
||||
{market.depth.lastTrade
|
||||
? addDecimal(market.depth.lastTrade.price, market.decimalPlaces)
|
||||
: ' - '}{' '}
|
||||
<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>
|
||||
|
||||
<Button
|
||||
className="w-full !py-8 mt-64 max-w-sm"
|
||||
boxShadow={false}
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
disabled={transactionStatus === 'pending' || isDisabled}
|
||||
data-testid="place-order"
|
||||
appendIconName="arrow-top-right"
|
||||
>
|
||||
{transactionStatus === 'pending' ? t('Pending...') : t('Submit')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -29,6 +29,7 @@ export default ({ value, onSelect }: SideSelectorProps) => {
|
||||
variant="inline-link"
|
||||
aria-label={t('Open long position')}
|
||||
className={classNames(
|
||||
'py-8',
|
||||
'buyButton hover:buyButton dark:buyButtonDark dark:hover:buyButtonDark',
|
||||
{ selected: value === VegaWalletOrderSide.Buy }
|
||||
)}
|
||||
@ -40,6 +41,7 @@ export default ({ value, onSelect }: SideSelectorProps) => {
|
||||
variant="inline-link"
|
||||
aria-label={t('Open short position')}
|
||||
className={classNames(
|
||||
'py-8',
|
||||
'sellButton hover:sellButton dark:sellButtonDark dark:hover:sellButtonDark',
|
||||
{ selected: value === VegaWalletOrderSide.Sell }
|
||||
)}
|
||||
|
@ -23,7 +23,7 @@ describe('SimpleMarketExpires', () => {
|
||||
'settlement-date:2022-04-25T1200',
|
||||
];
|
||||
render(<SimpleMarketExpires tags={tags} />);
|
||||
expect(screen.getByText('April 25')).toBeInTheDocument();
|
||||
expect(screen.getByText('Apr 25')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('last one proper tag should matter', () => {
|
||||
@ -33,7 +33,7 @@ describe('SimpleMarketExpires', () => {
|
||||
'settlement-expiry-date:2022-03-25T12:00:00',
|
||||
];
|
||||
render(<SimpleMarketExpires tags={tags} />);
|
||||
expect(screen.getByText('March 25')).toBeInTheDocument();
|
||||
expect(screen.getByText('Mar 25')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('when no proper tag nor date should be null', () => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { SimpleMarkets_markets } from '../components/simple-market-list/__generated__/SimpleMarkets';
|
||||
|
||||
export const DATE_FORMAT = 'dd MMMM yyyy HH:mm';
|
||||
export const EXPIRE_DATE_FORMAT = 'MMMM dd';
|
||||
export const EXPIRE_DATE_FORMAT = 'MMM dd';
|
||||
|
||||
export const TRADABLE_STATES = {
|
||||
Active: true,
|
||||
|
48
apps/simple-trading-app/src/app/hooks/__generated__/estimateOrder.ts
generated
Normal file
48
apps/simple-trading-app/src/app/hooks/__generated__/estimateOrder.ts
generated
Normal file
@ -0,0 +1,48 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { Side, OrderTimeInForce, OrderType } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: EstimateOrder
|
||||
// ====================================================
|
||||
|
||||
export interface EstimateOrder_estimateOrder_marginLevels {
|
||||
__typename: "MarginLevels";
|
||||
/**
|
||||
* this is the minimal margin required for a party to place a new order on the network (unsigned int actually)
|
||||
*/
|
||||
initialLevel: string;
|
||||
}
|
||||
|
||||
export interface EstimateOrder_estimateOrder {
|
||||
__typename: "OrderEstimate";
|
||||
/**
|
||||
* The total estimated amount of fee if the order was to trade
|
||||
*/
|
||||
totalFeeAmount: string;
|
||||
/**
|
||||
* The margin requirement for this order
|
||||
*/
|
||||
marginLevels: EstimateOrder_estimateOrder_marginLevels;
|
||||
}
|
||||
|
||||
export interface EstimateOrder {
|
||||
/**
|
||||
* return an estimation of the potential cost for a new order
|
||||
*/
|
||||
estimateOrder: EstimateOrder_estimateOrder;
|
||||
}
|
||||
|
||||
export interface EstimateOrderVariables {
|
||||
marketId: string;
|
||||
partyId: string;
|
||||
price?: string | null;
|
||||
size: string;
|
||||
side: Side;
|
||||
timeInForce: OrderTimeInForce;
|
||||
expiration?: string | null;
|
||||
type: OrderType;
|
||||
}
|
76
apps/simple-trading-app/src/app/hooks/__generated__/marketPositions.ts
generated
Normal file
76
apps/simple-trading-app/src/app/hooks/__generated__/marketPositions.ts
generated
Normal file
@ -0,0 +1,76 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: MarketPositions
|
||||
// ====================================================
|
||||
|
||||
export interface MarketPositions_party_positionsConnection_edges_node_market_accounts {
|
||||
__typename: "Account";
|
||||
/**
|
||||
* Balance as string - current account balance (approx. as balances can be updated several times per second)
|
||||
*/
|
||||
balance: string;
|
||||
}
|
||||
|
||||
export interface MarketPositions_party_positionsConnection_edges_node_market {
|
||||
__typename: "Market";
|
||||
/**
|
||||
* Market ID
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Get account for a party or market
|
||||
*/
|
||||
accounts: MarketPositions_party_positionsConnection_edges_node_market_accounts[] | null;
|
||||
}
|
||||
|
||||
export interface MarketPositions_party_positionsConnection_edges_node {
|
||||
__typename: "Position";
|
||||
/**
|
||||
* Open volume (uint64)
|
||||
*/
|
||||
openVolume: string;
|
||||
/**
|
||||
* Market relating to this position
|
||||
*/
|
||||
market: MarketPositions_party_positionsConnection_edges_node_market;
|
||||
}
|
||||
|
||||
export interface MarketPositions_party_positionsConnection_edges {
|
||||
__typename: "PositionEdge";
|
||||
node: MarketPositions_party_positionsConnection_edges_node;
|
||||
}
|
||||
|
||||
export interface MarketPositions_party_positionsConnection {
|
||||
__typename: "PositionConnection";
|
||||
/**
|
||||
* The positions in this connection
|
||||
*/
|
||||
edges: MarketPositions_party_positionsConnection_edges[] | null;
|
||||
}
|
||||
|
||||
export interface MarketPositions_party {
|
||||
__typename: "Party";
|
||||
/**
|
||||
* Party identifier
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Trading positions relating to a party
|
||||
*/
|
||||
positionsConnection: MarketPositions_party_positionsConnection;
|
||||
}
|
||||
|
||||
export interface MarketPositions {
|
||||
/**
|
||||
* An entity that is trading on the VEGA network
|
||||
*/
|
||||
party: MarketPositions_party | null;
|
||||
}
|
||||
|
||||
export interface MarketPositionsVariables {
|
||||
partyId: string;
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import type {
|
||||
MarketPositions,
|
||||
MarketPositionsVariables,
|
||||
} from './__generated__/marketPositions';
|
||||
|
||||
const MARKET_POSITIONS_QUERY = gql`
|
||||
query MarketPositions($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
id
|
||||
positionsConnection {
|
||||
edges {
|
||||
node {
|
||||
openVolume
|
||||
market {
|
||||
id
|
||||
accounts {
|
||||
balance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
marketId: string;
|
||||
partyId: string;
|
||||
}
|
||||
|
||||
type PositionMargin = {
|
||||
openVolume: BigNumber;
|
||||
balanceSum: BigNumber;
|
||||
} | null;
|
||||
|
||||
export default ({ marketId, partyId }: Props): PositionMargin => {
|
||||
const { data } = useQuery<MarketPositions, MarketPositionsVariables>(
|
||||
MARKET_POSITIONS_QUERY,
|
||||
{
|
||||
pollInterval: 15000,
|
||||
variables: { partyId },
|
||||
skip: !partyId,
|
||||
}
|
||||
);
|
||||
|
||||
const markets =
|
||||
data?.party?.positionsConnection?.edges
|
||||
?.filter((nodes) => nodes.node.market.id === marketId)
|
||||
.map((nodes) => nodes.node) || [];
|
||||
|
||||
return markets.length
|
||||
? markets.reduce(
|
||||
(agg, item) => {
|
||||
const balance = item.market.accounts?.reduce(
|
||||
(acagg, account) => acagg.plus(account.balance || 0),
|
||||
new BigNumber(0)
|
||||
);
|
||||
if (balance) {
|
||||
agg.balanceSum = agg.balanceSum.plus(balance);
|
||||
agg.openVolume = agg.openVolume.plus(item.openVolume);
|
||||
}
|
||||
return agg;
|
||||
},
|
||||
{ openVolume: new BigNumber(0), balanceSum: new BigNumber(0) }
|
||||
)
|
||||
: null;
|
||||
};
|
39
apps/simple-trading-app/src/app/hooks/use-order-closeout.ts
Normal file
39
apps/simple-trading-app/src/app/hooks/use-order-closeout.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import type { Order } from '@vegaprotocol/orders';
|
||||
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
|
||||
import type { PartyBalanceQuery } from '../components/deal-ticket/__generated__/PartyBalanceQuery';
|
||||
import { useSettlementAccount } from './use-settlement-account';
|
||||
import { VegaWalletOrderSide } from '@vegaprotocol/wallet';
|
||||
import { addDecimal, formatNumber } from '@vegaprotocol/react-helpers';
|
||||
|
||||
interface Props {
|
||||
order: Order;
|
||||
market: DealTicketQuery_market;
|
||||
partyData?: PartyBalanceQuery;
|
||||
}
|
||||
|
||||
const useOrderCloseOut = ({ order, market, partyData }: Props): string => {
|
||||
const account = useSettlementAccount(
|
||||
market.tradableInstrument.instrument.product.settlementAsset.id,
|
||||
partyData?.party?.accounts || []
|
||||
);
|
||||
if (account?.balance && market.depth.lastTrade) {
|
||||
const price = new BigNumber(
|
||||
addDecimal(market.depth.lastTrade.price, market.decimalPlaces)
|
||||
);
|
||||
const balance = new BigNumber(
|
||||
addDecimal(account.balance, account.asset.decimals)
|
||||
);
|
||||
const { size, side } = order;
|
||||
const bigOne = new BigNumber(1);
|
||||
return formatNumber(
|
||||
side === VegaWalletOrderSide.Buy
|
||||
? bigOne.minus(balance.div(price.times(size))).times(price)
|
||||
: bigOne.plus(balance.div(price.times(size))).times(price),
|
||||
market.decimalPlaces
|
||||
);
|
||||
}
|
||||
return ' - ';
|
||||
};
|
||||
|
||||
export default useOrderCloseOut;
|
112
apps/simple-trading-app/src/app/hooks/use-order-margin.ts
Normal file
112
apps/simple-trading-app/src/app/hooks/use-order-margin.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import type { Order } from '@vegaprotocol/orders';
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import type {
|
||||
EstimateOrder,
|
||||
EstimateOrderVariables,
|
||||
} from './__generated__/estimateOrder';
|
||||
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
|
||||
import { OrderTimeInForce, OrderType, Side } from '@vegaprotocol/types';
|
||||
import {
|
||||
VegaWalletOrderSide,
|
||||
VegaWalletOrderTimeInForce,
|
||||
VegaWalletOrderType,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { addDecimal, formatNumber } from '@vegaprotocol/react-helpers';
|
||||
import useMarketPositions from './use-market-positions';
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
|
||||
export const ESTIMATE_ORDER_QUERY = gql`
|
||||
query EstimateOrder(
|
||||
$marketId: ID!
|
||||
$partyId: ID!
|
||||
$price: String
|
||||
$size: String!
|
||||
$side: Side!
|
||||
$timeInForce: OrderTimeInForce!
|
||||
$expiration: String
|
||||
$type: OrderType!
|
||||
) {
|
||||
estimateOrder(
|
||||
marketId: $marketId
|
||||
partyId: $partyId
|
||||
price: $price
|
||||
size: $size
|
||||
side: $side
|
||||
timeInForce: $timeInForce
|
||||
expiration: $expiration
|
||||
type: $type
|
||||
) {
|
||||
totalFeeAmount
|
||||
marginLevels {
|
||||
initialLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
order: Order;
|
||||
market: DealTicketQuery_market;
|
||||
partyId: string;
|
||||
}
|
||||
|
||||
const times: Record<VegaWalletOrderTimeInForce, OrderTimeInForce> = {
|
||||
[VegaWalletOrderTimeInForce.GTC]: OrderTimeInForce.GTC,
|
||||
[VegaWalletOrderTimeInForce.GTT]: OrderTimeInForce.GTT,
|
||||
[VegaWalletOrderTimeInForce.IOC]: OrderTimeInForce.IOC,
|
||||
[VegaWalletOrderTimeInForce.FOK]: OrderTimeInForce.FOK,
|
||||
[VegaWalletOrderTimeInForce.GFN]: OrderTimeInForce.GFN,
|
||||
[VegaWalletOrderTimeInForce.GFA]: OrderTimeInForce.GFA,
|
||||
};
|
||||
|
||||
const types: Record<VegaWalletOrderType, OrderType> = {
|
||||
[VegaWalletOrderType.Market]: OrderType.Market,
|
||||
[VegaWalletOrderType.Limit]: OrderType.Limit,
|
||||
};
|
||||
|
||||
const useOrderMargin = ({ order, market, partyId }: Props) => {
|
||||
const marketPositions = useMarketPositions({ marketId: market.id, partyId });
|
||||
console.log('marketPositions', marketPositions);
|
||||
console.log(
|
||||
'marketPositions.openVolume',
|
||||
marketPositions?.openVolume.toNumber()
|
||||
);
|
||||
console.log(
|
||||
'marketPositions.balanceSum',
|
||||
marketPositions?.balanceSum.toNumber()
|
||||
);
|
||||
const { data } = useQuery<EstimateOrder, EstimateOrderVariables>(
|
||||
ESTIMATE_ORDER_QUERY,
|
||||
{
|
||||
variables: {
|
||||
marketId: market.id,
|
||||
partyId,
|
||||
price: market.depth.lastTrade?.price,
|
||||
size: order.size + (marketPositions?.openVolume.toNumber() || 0),
|
||||
side: order.side === VegaWalletOrderSide.Buy ? Side.Buy : Side.Sell,
|
||||
timeInForce: times[order.timeInForce],
|
||||
type: types[order.type],
|
||||
},
|
||||
skip: !partyId || !market.id || !order.size,
|
||||
}
|
||||
);
|
||||
if (data?.estimateOrder.marginLevels.initialLevel) {
|
||||
return formatNumber(
|
||||
Math.max(
|
||||
0,
|
||||
new BigNumber(
|
||||
addDecimal(
|
||||
data.estimateOrder.marginLevels.initialLevel,
|
||||
market.decimalPlaces
|
||||
)
|
||||
)
|
||||
.minus(marketPositions?.balanceSum.toNumber() || 0)
|
||||
.toNumber()
|
||||
),
|
||||
market.decimalPlaces
|
||||
);
|
||||
}
|
||||
return ' - ';
|
||||
};
|
||||
|
||||
export default useOrderMargin;
|
@ -44,7 +44,7 @@ export const DealTicketManager = ({
|
||||
);
|
||||
};
|
||||
|
||||
const getDialogTitle = (status?: OrderStatus): string | undefined => {
|
||||
export const getDialogTitle = (status?: OrderStatus): string | undefined => {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
@ -63,7 +63,7 @@ const getDialogTitle = (status?: OrderStatus): string | undefined => {
|
||||
}
|
||||
};
|
||||
|
||||
const getDialogIntent = (status?: OrderStatus): Intent | undefined => {
|
||||
export const getDialogIntent = (status?: OrderStatus): Intent | undefined => {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
@ -81,7 +81,7 @@ const getDialogIntent = (status?: OrderStatus): Intent | undefined => {
|
||||
}
|
||||
};
|
||||
|
||||
const getDialogIcon = (status?: OrderStatus): ReactNode | undefined => {
|
||||
export const getDialogIcon = (status?: OrderStatus): ReactNode | undefined => {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
|
@ -45,8 +45,6 @@ const vegaCustomClassesLite = plugin(function ({ addUtilities }) {
|
||||
backgroundColor: 'rgba(0, 143, 74, 0.1)',
|
||||
border: `1px solid ${theme.colors.darkerGreen}`,
|
||||
color: theme.colors.darkerGreen,
|
||||
paddingTop: '0.5rem',
|
||||
paddingBottom: '0.5rem',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.colors.darkerGreen,
|
||||
color: theme.colors.white.DEFAULT,
|
||||
@ -68,8 +66,6 @@ const vegaCustomClassesLite = plugin(function ({ addUtilities }) {
|
||||
'.sellButton': {
|
||||
textTransform: 'uppercase',
|
||||
textDecoration: 'none',
|
||||
paddingTop: '0.5rem',
|
||||
paddingBottom: '0.5rem',
|
||||
backgroundColor: 'rgba(255, 8, 126, 0.1)',
|
||||
border: `1px solid ${theme.colors.pink}`,
|
||||
color: theme.colors.pink,
|
||||
|
Loading…
Reference in New Issue
Block a user