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')
.eq(1)
.find('dd div')
.should('have.text', '3.44055');
.should('have.text', '25.78726');
cy.getByTestId('key-value-table')
.find('dl')
.eq(2)

View File

@ -2,6 +2,28 @@ export const generateMarketPositions = () => {
return {
party: {
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: {
edges: [
{
@ -9,10 +31,6 @@ export const generateMarketPositions = () => {
openVolume: '3',
market: {
id: '2751c508f9759761f912890f37fb3f97a00300bf7685c02a56a86e05facfe221',
accounts: [
{ balance: '0', __typename: 'Account' },
{ balance: '0', __typename: 'Account' },
],
__typename: 'Market',
},
__typename: 'Position',
@ -24,10 +42,6 @@ export const generateMarketPositions = () => {
openVolume: '12',
market: {
id: 'first-btcusd-id',
accounts: [
{ balance: '10', __typename: 'Account' },
{ balance: '15', __typename: 'Account' },
],
__typename: 'Market',
},
__typename: 'Position',

View File

@ -3,16 +3,46 @@
// @generated
// This file was automatically generated and should not be edited.
import { AccountType } from "@vegaprotocol/types";
// ====================================================
// 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";
/**
* Account type (General, Margin, etc)
*/
type: AccountType;
/**
* Balance as string - current account balance (approx. as balances can be updated several times per second)
*/
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 {
@ -21,10 +51,6 @@ export interface MarketPositions_party_positionsConnection_edges_node_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 {
@ -58,6 +84,10 @@ export interface MarketPositions_party {
* Party identifier
*/
id: string;
/**
* Collateral accounts relating to a party
*/
accounts: MarketPositions_party_accounts[] | null;
/**
* 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!) {
party(id: $partyId) {
id
accounts {
type
balance
asset {
decimals
}
market {
id
}
}
positionsConnection {
edges {
node {
openVolume
market {
id
accounts {
balance
}
}
}
}
@ -31,40 +38,40 @@ interface Props {
partyId: string;
}
type PositionMargin = {
export type PositionMargin = {
openVolume: BigNumber;
balanceSum: BigNumber;
balance: BigNumber;
balanceDecimals?: number;
} | null;
export default ({ marketId, partyId }: Props): PositionMargin => {
const { data } = useQuery<MarketPositions, MarketPositionsVariables>(
MARKET_POSITIONS_QUERY,
{
pollInterval: 15000,
pollInterval: 5000,
variables: { partyId },
skip: !partyId,
}
);
const markets =
data?.party?.positionsConnection?.edges
?.filter((nodes) => nodes.node.market.id === marketId)
.map((nodes) => nodes.node) || [];
const account = data?.party?.accounts?.find(
(nodes) => nodes.market?.id === marketId
);
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;
if (account) {
const balance = new BigNumber(account.balance || 0);
const openVolume = new BigNumber(
data?.party?.positionsConnection?.edges?.find(
(nodes) => nodes.node.market.id === marketId
)?.node.openVolume || 0
);
if (!balance.isZero() && !openVolume.isZero()) {
return {
balance,
balanceDecimals: account?.asset.decimals,
openVolume,
};
}
}
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 { gql, useQuery } from '@apollo/client';
import type {
@ -11,9 +12,8 @@ import {
VegaWalletOrderTimeInForce,
VegaWalletOrderType,
} from '@vegaprotocol/wallet';
import { addDecimal, formatNumber } from '@vegaprotocol/react-helpers';
import { addDecimal } from '@vegaprotocol/react-helpers';
import useMarketPositions from './use-market-positions';
import { BigNumber } from 'bignumber.js';
export const ESTIMATE_ORDER_QUERY = gql`
query EstimateOrder(
@ -66,15 +66,6 @@ const types: Record<VegaWalletOrderType, OrderType> = {
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,
{
@ -82,27 +73,27 @@ const useOrderMargin = ({ order, market, partyId }: Props) => {
marketId: market.id,
partyId,
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,
timeInForce: times[order.timeInForce],
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) {
return formatNumber(
Math.max(
return addDecimal(
BigNumber.maximum(
0,
new BigNumber(
addDecimal(
data.estimateOrder.marginLevels.initialLevel,
market.decimalPlaces
)
new BigNumber(data.estimateOrder.marginLevels.initialLevel).minus(
marketPositions?.balance || 0
)
.minus(marketPositions?.balanceSum.toNumber() || 0)
.toNumber()
),
).toString(),
market.decimalPlaces
);
}