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:
parent
4269060c9c
commit
7b4e618689
@ -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)
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
||||
};
|
||||
|
@ -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(' - ');
|
||||
});
|
||||
});
|
106
apps/simple-trading-app/src/app/hooks/use-order-margin.spec.ts
Normal file
106
apps/simple-trading-app/src/app/hooks/use-order-margin.spec.ts
Normal 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
|
||||
);
|
||||
});
|
||||
});
|
@ -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
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user