Skip to content

Commit

Permalink
fix: rework position rendering logic (#242)
Browse files Browse the repository at this point in the history
* math(positions): add comment about asset id encoding

* ui(positions): don't render trailing zeroes

* model(positions): fix `DisplayPosition` rendering

Co-Authored-By: Jason M. Hasperhoven <[email protected]>

* math(tests): scaffolding for rendering calculation tests

Co-Authored-By: Jason M. Hasperhoven <[email protected]>

* trade: fix current value rendering

Co-Authored-By: Jason M. Hasperhoven <[email protected]>

* chore: run linter

* Add tests for getOrdersByBaseQuoteAssets

* Remove eslint disable comment

* Add canonical order assertion before all position tests

* Add comment to ExecutionPosition interface

* Refactor getDirectionalOrders & getOrderValueViews

* Add tests for getOrderValueViews and convert units to use BigNumber

* Fix unit conversions in current positions

* Add tests for getDirectionalOrders

---------

Co-authored-by: Jason M. Hasperhoven <[email protected]>
Co-authored-by: Jason M. Hasperhoven <[email protected]>
  • Loading branch information
3 people authored Jan 13, 2025
1 parent ae87a30 commit c9d77e3
Show file tree
Hide file tree
Showing 6 changed files with 634 additions and 170 deletions.
301 changes: 301 additions & 0 deletions src/pages/trade/model/positions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
import { beforeAll, describe, expect, it } from 'vitest';
import {
Position,
PositionState_PositionStateEnum,
} from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb';
import { ExecutedPosition, positionsStore } from './positions';
import {
Metadata,
AssetId,
DenomUnit,
} from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
import { compareAssetId } from '@/shared/math/position';
import { pnum } from '@penumbra-zone/types/pnum';
import { BigNumber } from 'bignumber.js';

describe('positionsStore', () => {
const id1 = new Uint8Array(Array(32).fill(0xaa));
const id2 = new Uint8Array(Array(32).fill(0xbb));
const id3 = new Uint8Array(Array(32).fill(0xcc));
const id4 = new Uint8Array(Array(32).fill(0xdd));
const stableId = new Uint8Array(Array(32).fill(0xee));
const p1 = 2;
const p2 = 1;
const exponent1 = 6;
const exponent2 = 9;

const createMetaData = (id: Uint8Array, display: string, exponent: number) => {
return new Metadata({
penumbraAssetId: new AssetId({ inner: id }),
symbol: display,
display,
denomUnits: [new DenomUnit({ denom: display, exponent })],
});
};

const metadataWithId1 = createMetaData(id1, 'asset1', exponent1);
const metadataWithId2 = createMetaData(id2, 'asset2', exponent2);
const metadataWithId3 = createMetaData(id3, 'asset2', exponent2);
const metadataWithId4 = createMetaData(id4, 'asset2', exponent2);
const metadataWithStableCoin = createMetaData(stableId, 'USDC', exponent2);

const feeBps = 25;

const createPosition = ({
r1,
r2,
asset1Id = id1,
asset2Id = id2,
}: {
r1: bigint;
r2: bigint;
asset1Id?: Uint8Array;
asset2Id?: Uint8Array;
}) => {
return new Position({
phi: {
component: {
fee: feeBps,
p: {
lo: BigInt(p1),
hi: 0n,
},
q: {
lo: BigInt(p2),
hi: 0n,
},
},
pair: {
asset1: {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
inner: asset1Id ?? id1,
},
asset2: {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
inner: asset2Id ?? id2,
},
},
},
nonce: new Uint8Array(Array(32).fill(0xcc)),
state: {
state: PositionState_PositionStateEnum.OPENED,
sequence: 0n,
},
reserves: {
r1: {
lo: r1,
hi: 0n,
},
r2: {
lo: r2,
hi: 0n,
},
},
closeOnFill: false,
});
};

beforeAll(() => {
positionsStore.setAssets([
metadataWithId1,
metadataWithId2,
metadataWithId3,
metadataWithId4,
metadataWithStableCoin,
]);

// Assert that id1 and id2 are in canonical order
expect(
compareAssetId(new AssetId({ inner: id1 }), new AssetId({ inner: id2 })),
).toBeLessThanOrEqual(0);
});

describe('getOrdersByBaseQuoteAssets', () => {
it('should return a buy and sell order when both assets have reserves', () => {
const position = createPosition({
r1: 100n,
r2: 100n,
});

const [asset1, asset2] = positionsStore.getCalculatedAssets(position as ExecutedPosition);
const orders = positionsStore.getOrdersByBaseQuoteAssets(asset1, asset2);
expect(orders[0]?.direction).toEqual('Buy');
expect(orders[1]?.direction).toEqual('Sell');
});

it('should return a buy order when only the quote asset has reserves', () => {
const position = createPosition({
r1: 0n,
r2: 100n,
});

const [asset1, asset2] = positionsStore.getCalculatedAssets(position as ExecutedPosition);
const orders = positionsStore.getOrdersByBaseQuoteAssets(asset1, asset2);
expect(orders[0]?.direction).toEqual('Buy');
expect(orders[1]).toEqual(undefined);
});

it('should return a sell order when only the base asset has reserves', () => {
const position = createPosition({
r1: 100n,
r2: 0n,
});

const [asset1, asset2] = positionsStore.getCalculatedAssets(position as ExecutedPosition);
const orders = positionsStore.getOrdersByBaseQuoteAssets(asset1, asset2);
expect(orders[0]?.direction).toEqual('Sell');
expect(orders[1]).toEqual(undefined);
});
});

describe('getOrderValueViews', () => {
it('should return the correct value views for a buy order', () => {
const position = createPosition({
r1: 0n,
r2: pnum(12.123, exponent2).toBigInt(),
});

const [asset1, asset2] = positionsStore.getCalculatedAssets(position as ExecutedPosition);
const orders = positionsStore.getOrdersByBaseQuoteAssets(asset1, asset2);
const buyOrder = orders[0];

const valueViews = positionsStore.getOrderValueViews(buyOrder!);

const basePrice = (p1 / p2) * 10 ** (exponent1 - exponent2);
const effectivePrice = BigNumber(basePrice)
.minus(BigNumber(basePrice).times(feeBps).div(10000))
.toNumber();

expect(pnum(valueViews.amount).toNumber()).toEqual(
Number(
(
buyOrder!.quoteAsset.amount.toNumber() *
buyOrder!.quoteAsset.effectivePrice.toNumber() *
10 ** (exponent2 - exponent1)
).toFixed(exponent1),
),
);
expect(pnum(valueViews.basePrice).toNumber()).toEqual(basePrice);
expect(pnum(valueViews.effectivePrice).toNumber()).toEqual(effectivePrice);
});

it('should return the correct value views for a sell order', () => {
const position = createPosition({
r1: pnum(4.567, exponent1).toBigInt(),
r2: 0n,
});

const [asset1, asset2] = positionsStore.getCalculatedAssets(position as ExecutedPosition);
const orders = positionsStore.getOrdersByBaseQuoteAssets(asset1, asset2);
const sellOrder = orders[0];

const valueViews = positionsStore.getOrderValueViews(sellOrder!);
const basePrice = (p1 / p2) * 10 ** (exponent1 - exponent2);
const effectivePrice = BigNumber(basePrice)
.minus(BigNumber(basePrice).times(feeBps).div(10000))
.toNumber();

expect(pnum(valueViews.amount).toNumber()).toEqual(4.567);
expect(pnum(valueViews.basePrice).toNumber()).toEqual(basePrice);
expect(pnum(valueViews.effectivePrice).toNumber()).toEqual(effectivePrice);
});
});

describe('getDirectionalOrders', () => {
it('should return with asset 1/2 as base/quote asset when its the provided base/quote assets', () => {
const position = createPosition({
r1: 0n,
r2: 100n,
});

const [asset1, asset2] = positionsStore.getCalculatedAssets(position as ExecutedPosition);
const directionalOrders = positionsStore.getDirectionalOrders({
asset1,
asset2,
baseAsset: metadataWithId1,
quoteAsset: metadataWithId2,
});
const positionOrder = directionalOrders[0]!;

expect(
positionOrder.baseAsset.asset.penumbraAssetId?.equals(asset1.asset.penumbraAssetId),
).toBe(true);
expect(
positionOrder.quoteAsset.asset.penumbraAssetId?.equals(asset2.asset.penumbraAssetId),
).toBe(true);
});

it('should return with asset 2/1 as base/quote asset when its the provided base/quote assets', () => {
const position = createPosition({
r1: 0n,
r2: 100n,
});

const [asset1, asset2] = positionsStore.getCalculatedAssets(position as ExecutedPosition);
const directionalOrders = positionsStore.getDirectionalOrders({
asset1,
asset2,
baseAsset: metadataWithId2,
quoteAsset: metadataWithId1,
});
const positionOrder = directionalOrders[0]!;

expect(
positionOrder.baseAsset.asset.penumbraAssetId?.equals(asset2.asset.penumbraAssetId),
).toBe(true);
expect(
positionOrder.quoteAsset.asset.penumbraAssetId?.equals(asset1.asset.penumbraAssetId),
).toBe(true);
});

it('should return with asset 1 as quote asset when asset 1 is a stable coin', () => {
const position = createPosition({
r1: 0n,
r2: 100n,
asset1Id: stableId,
asset2Id: id2,
});

const [asset1, asset2] = positionsStore.getCalculatedAssets(position as ExecutedPosition);
const directionalOrders = positionsStore.getDirectionalOrders({
asset1,
asset2,
baseAsset: metadataWithId1,
quoteAsset: metadataWithId2,
});
const positionOrder = directionalOrders[0]!;

expect(
positionOrder.baseAsset.asset.penumbraAssetId?.equals(asset2.asset.penumbraAssetId),
).toBe(true);
expect(
positionOrder.quoteAsset.asset.penumbraAssetId?.equals(asset1.asset.penumbraAssetId),
).toBe(true);
});

it('should return with asset 2 as quote asset when asset 2 is a stable coin', () => {
const position = createPosition({
r1: 0n,
r2: 100n,
asset1Id: id1,
asset2Id: stableId,
});

const [asset1, asset2] = positionsStore.getCalculatedAssets(position as ExecutedPosition);
const directionalOrders = positionsStore.getDirectionalOrders({
asset1,
asset2,
baseAsset: metadataWithId1,
quoteAsset: metadataWithId2,
});
const positionOrder = directionalOrders[0]!;

expect(
positionOrder.baseAsset.asset.penumbraAssetId?.equals(asset1.asset.penumbraAssetId),
).toBe(true);
expect(
positionOrder.quoteAsset.asset.penumbraAssetId?.equals(asset2.asset.penumbraAssetId),
).toBe(true);
});
});
});
Loading

0 comments on commit c9d77e3

Please sign in to comment.