feat: [console-lite] - fixes in order margin calcs (#891)

* feat: [console-lite] - fixes in order margin calcs

* feat: [console-lite] - fixes in order margin calcs - fix decimals

* feat: [console-lite] - fixes in order margin calcs - add unit tests

* feat: [console-lite] - fixes in order margin calcs - improve cals

* feat: [console-lite] - fixes in order margin calcs - improve cals

* feat: [console-lite] - fixes in order margin calcs - more improvements

Co-authored-by: maciek <maciek@vegaprotocol.io>
This commit is contained in:
macqbat 2022-08-01 11:32:18 +02:00 committed by GitHub
parent 4269060c9c
commit 7b4e618689
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 415 additions and 63 deletions

View File

@ -94,7 +94,7 @@ describe('Market trade', () => {
.find('dl') .find('dl')
.eq(1) .eq(1)
.find('dd div') .find('dd div')
.should('have.text', '3.44055'); .should('have.text', '25.78726');
cy.getByTestId('key-value-table') cy.getByTestId('key-value-table')
.find('dl') .find('dl')
.eq(2) .eq(2)

View File

@ -2,6 +2,28 @@ export const generateMarketPositions = () => {
return { return {
party: { party: {
id: '2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1', id: '2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
accounts: [
{
asset: {
decimals: 5,
},
balance: '400000000000000000000',
market: {
id: '2751c508f9759761f912890f37fb3f97a00300bf7685c02a56a86e05facfe221',
__typename: 'Market',
},
},
{
asset: {
decimals: 5,
},
balance: '265329',
market: {
id: 'first-btcusd-id',
__typename: 'Market',
},
},
],
positionsConnection: { positionsConnection: {
edges: [ edges: [
{ {
@ -9,10 +31,6 @@ export const generateMarketPositions = () => {
openVolume: '3', openVolume: '3',
market: { market: {
id: '2751c508f9759761f912890f37fb3f97a00300bf7685c02a56a86e05facfe221', id: '2751c508f9759761f912890f37fb3f97a00300bf7685c02a56a86e05facfe221',
accounts: [
{ balance: '0', __typename: 'Account' },
{ balance: '0', __typename: 'Account' },
],
__typename: 'Market', __typename: 'Market',
}, },
__typename: 'Position', __typename: 'Position',
@ -24,10 +42,6 @@ export const generateMarketPositions = () => {
openVolume: '12', openVolume: '12',
market: { market: {
id: 'first-btcusd-id', id: 'first-btcusd-id',
accounts: [
{ balance: '10', __typename: 'Account' },
{ balance: '15', __typename: 'Account' },
],
__typename: 'Market', __typename: 'Market',
}, },
__typename: 'Position', __typename: 'Position',

View File

@ -3,16 +3,46 @@
// @generated // @generated
// This file was automatically generated and should not be edited. // This file was automatically generated and should not be edited.
import { AccountType } from "@vegaprotocol/types";
// ==================================================== // ====================================================
// GraphQL query operation: MarketPositions // GraphQL query operation: MarketPositions
// ==================================================== // ====================================================
export interface MarketPositions_party_positionsConnection_edges_node_market_accounts { export interface MarketPositions_party_accounts_asset {
__typename: "Asset";
/**
* The precision of the asset
*/
decimals: number;
}
export interface MarketPositions_party_accounts_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
}
export interface MarketPositions_party_accounts {
__typename: "Account"; __typename: "Account";
/**
* Account type (General, Margin, etc)
*/
type: AccountType;
/** /**
* Balance as string - current account balance (approx. as balances can be updated several times per second) * Balance as string - current account balance (approx. as balances can be updated several times per second)
*/ */
balance: string; balance: string;
/**
* Asset, the 'currency'
*/
asset: MarketPositions_party_accounts_asset;
/**
* Market (only relevant to margin accounts)
*/
market: MarketPositions_party_accounts_market | null;
} }
export interface MarketPositions_party_positionsConnection_edges_node_market { export interface MarketPositions_party_positionsConnection_edges_node_market {
@ -21,10 +51,6 @@ export interface MarketPositions_party_positionsConnection_edges_node_market {
* Market ID * Market ID
*/ */
id: string; 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 { export interface MarketPositions_party_positionsConnection_edges_node {
@ -58,6 +84,10 @@ export interface MarketPositions_party {
* Party identifier * Party identifier
*/ */
id: string; id: string;
/**
* Collateral accounts relating to a party
*/
accounts: MarketPositions_party_accounts[] | null;
/** /**
* Trading positions relating to a party * Trading positions relating to a party
*/ */

View File

@ -0,0 +1,133 @@
import { renderHook } from '@testing-library/react-hooks';
import useMarketPositions from './use-market-positions';
let mockNotEmptyData = {
party: {
accounts: [
{
balance: '50001000000',
asset: {
decimals: 5,
},
market: {
id: 'marketId',
},
},
{
balance: '700000000000000000000000000000',
asset: {
decimals: 5,
},
market: {
id: 'someOtherMarketId',
},
},
],
positionsConnection: {
edges: [
{
node: {
openVolume: '100002',
market: {
id: 'marketId',
},
},
},
{
node: {
openVolume: '3',
market: {
id: 'someOtherMarketId',
},
},
},
],
},
},
};
jest.mock('@apollo/client', () => ({
...jest.requireActual('@apollo/client'),
useQuery: jest.fn(() => ({ data: mockNotEmptyData })),
}));
describe('useOrderPosition Hook', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should return proper positive value', () => {
const { result } = renderHook(() =>
useMarketPositions({ marketId: 'marketId', partyId: 'partyId' })
);
expect(result.current?.openVolume.toNumber()).toEqual(100002);
expect(result.current?.balance.toString()).toEqual('50001000000');
});
it('if balance equal 0 return null', () => {
mockNotEmptyData = {
party: {
accounts: [
{
balance: '0',
asset: {
decimals: 5,
},
market: {
id: 'marketId',
},
},
],
positionsConnection: {
edges: [
{
node: {
openVolume: '2',
market: {
id: 'marketId',
},
},
},
],
},
},
};
const { result } = renderHook(() =>
useMarketPositions({ marketId: 'marketId', partyId: 'partyId' })
);
expect(result.current).toBeNull();
});
it('if no markets return null', () => {
mockNotEmptyData = {
party: {
accounts: [
{
balance: '33330',
asset: {
decimals: 5,
},
market: {
id: 'otherMarketId',
},
},
],
positionsConnection: {
edges: [
{
node: {
openVolume: '2',
market: {
id: 'otherMarketId',
},
},
},
],
},
},
};
const { result } = renderHook(() =>
useMarketPositions({ marketId: 'marketId', partyId: 'partyId' })
);
expect(result.current).toBeNull();
});
});

View File

@ -9,15 +9,22 @@ const MARKET_POSITIONS_QUERY = gql`
query MarketPositions($partyId: ID!) { query MarketPositions($partyId: ID!) {
party(id: $partyId) { party(id: $partyId) {
id id
accounts {
type
balance
asset {
decimals
}
market {
id
}
}
positionsConnection { positionsConnection {
edges { edges {
node { node {
openVolume openVolume
market { market {
id id
accounts {
balance
}
} }
} }
} }
@ -31,40 +38,40 @@ interface Props {
partyId: string; partyId: string;
} }
type PositionMargin = { export type PositionMargin = {
openVolume: BigNumber; openVolume: BigNumber;
balanceSum: BigNumber; balance: BigNumber;
balanceDecimals?: number;
} | null; } | null;
export default ({ marketId, partyId }: Props): PositionMargin => { export default ({ marketId, partyId }: Props): PositionMargin => {
const { data } = useQuery<MarketPositions, MarketPositionsVariables>( const { data } = useQuery<MarketPositions, MarketPositionsVariables>(
MARKET_POSITIONS_QUERY, MARKET_POSITIONS_QUERY,
{ {
pollInterval: 15000, pollInterval: 5000,
variables: { partyId }, variables: { partyId },
skip: !partyId, skip: !partyId,
} }
); );
const markets = const account = data?.party?.accounts?.find(
data?.party?.positionsConnection?.edges (nodes) => nodes.market?.id === marketId
?.filter((nodes) => nodes.node.market.id === marketId) );
.map((nodes) => nodes.node) || [];
return markets.length if (account) {
? markets.reduce( const balance = new BigNumber(account.balance || 0);
(agg, item) => { const openVolume = new BigNumber(
const balance = item.market.accounts?.reduce( data?.party?.positionsConnection?.edges?.find(
(acagg, account) => acagg.plus(account.balance || 0), (nodes) => nodes.node.market.id === marketId
new BigNumber(0) )?.node.openVolume || 0
); );
if (balance) { if (!balance.isZero() && !openVolume.isZero()) {
agg.balanceSum = agg.balanceSum.plus(balance); return {
agg.openVolume = agg.openVolume.plus(item.openVolume); balance,
} balanceDecimals: account?.asset.decimals,
return agg; openVolume,
}, };
{ openVolume: new BigNumber(0), balanceSum: new BigNumber(0) } }
) }
: null; return null;
}; };

View File

@ -0,0 +1,71 @@
import { renderHook } from '@testing-library/react-hooks';
import useOrderCloseOut from './use-order-closeout';
import type { Order } from '@vegaprotocol/orders';
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
import type { PartyBalanceQuery } from '../components/deal-ticket/__generated__/PartyBalanceQuery';
describe('useOrderCloseOut Hook', () => {
const order = { size: '2', side: 'SIDE_BUY' };
const market = {
decimalPlaces: 5,
depth: {
lastTrade: {
price: '1000000',
},
},
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'assetId',
},
},
},
},
};
const partyData = {
party: {
accounts: [
{
balance: '200000',
asset: {
id: 'assetId',
decimals: 5,
},
},
],
},
};
it('return proper buy value', () => {
const { result } = renderHook(() =>
useOrderCloseOut({
order: order as Order,
market: market as DealTicketQuery_market,
partyData: partyData as PartyBalanceQuery,
})
);
expect(result.current).toEqual('9.00000');
});
it('return proper sell value', () => {
const { result } = renderHook(() =>
useOrderCloseOut({
order: { ...order, side: 'SIDE_SELL' } as Order,
market: market as DealTicketQuery_market,
partyData: partyData as PartyBalanceQuery,
})
);
expect(result.current).toEqual('11.00000');
});
it('return proper empty value', () => {
const { result } = renderHook(() =>
useOrderCloseOut({
order: { ...order, side: 'SIDE_SELL' } as Order,
market: market as DealTicketQuery_market,
})
);
expect(result.current).toEqual(' - ');
});
});

View File

@ -0,0 +1,106 @@
import { renderHook } from '@testing-library/react-hooks';
import { useQuery } from '@apollo/client';
import { BigNumber } from 'bignumber.js';
import type { Order } from '@vegaprotocol/orders';
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
import type { PositionMargin } from './use-market-positions';
import useOrderMargin from './use-order-margin';
let mockEstimateData = {
estimateOrder: {
marginLevels: {
initialLevel: '200000',
},
},
};
jest.mock('@apollo/client', () => ({
...jest.requireActual('@apollo/client'),
useQuery: jest.fn(() => ({ data: mockEstimateData })),
}));
let mockMarketPositions: PositionMargin = {
openVolume: new BigNumber(1),
balance: new BigNumber(100000),
};
jest.mock('./use-market-positions', () => jest.fn(() => mockMarketPositions));
describe('useOrderMargin Hook', () => {
const order = {
size: '2',
side: 'SIDE_BUY',
timeInForce: 'TIME_IN_FORCE_IOC',
type: 'TYPE_MARKET',
};
const market = {
id: 'marketId',
depth: {
lastTrade: {
price: '1000000',
},
},
};
const partyId = 'partyId';
afterEach(() => {
jest.clearAllMocks();
});
it('margin should be properly calculated', () => {
const { result } = renderHook(() =>
useOrderMargin({
order: order as Order,
market: market as DealTicketQuery_market,
partyId,
})
);
expect(result.current).toEqual('100000');
const calledSize = new BigNumber(mockMarketPositions?.openVolume || 0)
.plus(order.size)
.toString();
expect((useQuery as jest.Mock).mock.calls[0][1].variables.size).toEqual(
calledSize
);
});
it('if there is no positions initialMargin should not be subtracted', () => {
mockMarketPositions = null;
const { result } = renderHook(() =>
useOrderMargin({
order: order as Order,
market: market as DealTicketQuery_market,
partyId,
})
);
expect(result.current).toEqual('200000');
expect((useQuery as jest.Mock).mock.calls[0][1].variables.size).toEqual(
order.size
);
});
it('if api fails, should return empty value', () => {
mockEstimateData = {
estimateOrder: {
marginLevels: {
initialLevel: '',
},
},
};
const { result } = renderHook(() =>
useOrderMargin({
order: order as Order,
market: market as DealTicketQuery_market,
partyId,
})
);
expect(result.current).toEqual(' - ');
const calledSize = new BigNumber(mockMarketPositions?.openVolume || 0)
.plus(order.size)
.toString();
expect((useQuery as jest.Mock).mock.calls[0][1].variables.size).toEqual(
calledSize
);
});
});

View File

@ -1,3 +1,4 @@
import { BigNumber } from 'bignumber.js';
import type { Order } from '@vegaprotocol/orders'; import type { Order } from '@vegaprotocol/orders';
import { gql, useQuery } from '@apollo/client'; import { gql, useQuery } from '@apollo/client';
import type { import type {
@ -11,9 +12,8 @@ import {
VegaWalletOrderTimeInForce, VegaWalletOrderTimeInForce,
VegaWalletOrderType, VegaWalletOrderType,
} from '@vegaprotocol/wallet'; } from '@vegaprotocol/wallet';
import { addDecimal, formatNumber } from '@vegaprotocol/react-helpers'; import { addDecimal } from '@vegaprotocol/react-helpers';
import useMarketPositions from './use-market-positions'; import useMarketPositions from './use-market-positions';
import { BigNumber } from 'bignumber.js';
export const ESTIMATE_ORDER_QUERY = gql` export const ESTIMATE_ORDER_QUERY = gql`
query EstimateOrder( query EstimateOrder(
@ -66,15 +66,6 @@ const types: Record<VegaWalletOrderType, OrderType> = {
const useOrderMargin = ({ order, market, partyId }: Props) => { const useOrderMargin = ({ order, market, partyId }: Props) => {
const marketPositions = useMarketPositions({ marketId: market.id, partyId }); 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>( const { data } = useQuery<EstimateOrder, EstimateOrderVariables>(
ESTIMATE_ORDER_QUERY, ESTIMATE_ORDER_QUERY,
{ {
@ -82,27 +73,27 @@ const useOrderMargin = ({ order, market, partyId }: Props) => {
marketId: market.id, marketId: market.id,
partyId, partyId,
price: market.depth.lastTrade?.price, price: market.depth.lastTrade?.price,
size: order.size + (marketPositions?.openVolume.toNumber() || 0), size: new BigNumber(marketPositions?.openVolume || 0)
[order.side === VegaWalletOrderSide.Buy ? 'plus' : 'minus'](
order.size
)
.toString(),
side: order.side === VegaWalletOrderSide.Buy ? Side.Buy : Side.Sell, side: order.side === VegaWalletOrderSide.Buy ? Side.Buy : Side.Sell,
timeInForce: times[order.timeInForce], timeInForce: times[order.timeInForce],
type: types[order.type], type: types[order.type],
}, },
skip: !partyId || !market.id || !order.size, skip:
!partyId || !market.id || !order.size || !market.depth.lastTrade?.price,
} }
); );
if (data?.estimateOrder.marginLevels.initialLevel) { if (data?.estimateOrder.marginLevels.initialLevel) {
return formatNumber( return addDecimal(
Math.max( BigNumber.maximum(
0, 0,
new BigNumber( new BigNumber(data.estimateOrder.marginLevels.initialLevel).minus(
addDecimal( marketPositions?.balance || 0
data.estimateOrder.marginLevels.initialLevel,
market.decimalPlaces
)
) )
.minus(marketPositions?.balanceSum.toNumber() || 0) ).toString(),
.toNumber()
),
market.decimalPlaces market.decimalPlaces
); );
} }