Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release alpha #319

Open
wants to merge 49 commits into
base: beta
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
51d31a2
feat: calculate buy/sell panel estimate
fubar May 23, 2024
f71e556
chore: remove unused pipmath export
fubar May 23, 2024
808e858
Merge pull request #318 from idexio/feat/buy-sell-panel-estimate
fubar May 23, 2024
3d4bf20
chore(release): 4.0.0-alpha.8 [skip ci]
idex-bot May 23, 2024
a43e373
Merge branch 'beta' into chore/update-alpha
fubar May 23, 2024
f747654
Merge pull request #321 from idexio/chore/update-alpha
fubar May 23, 2024
79e5916
chore(release): 4.0.0-alpha.9 [skip ci]
idex-bot May 23, 2024
3510957
feat: buy/sell panel estimate by desired trade qty (base or quote)
fubar May 23, 2024
6759dbf
Merge pull request #322 from idexio/feat/buy-sell-estimate-accept-des…
jurosh May 23, 2024
231945e
chore(release): 4.0.0-alpha.10 [skip ci]
idex-bot May 23, 2024
4e967cd
feat: improve type declarations of mutually exclusive arguments
fubar May 24, 2024
d61cee5
Merge pull request #323 from idexio/feat/improve-buy-sell-estimate-ty…
fubar May 24, 2024
60248ce
chore(release): 4.0.0-alpha.11 [skip ci]
idex-bot May 24, 2024
a3b3617
feat(buy/sell panel): support trades that increase available collateral
fubar May 24, 2024
f032024
Merge pull request #324 from idexio/feat/buy-sell-estimate-support-in…
fubar May 24, 2024
ebee6bc
chore(release): 4.0.0-alpha.12 [skip ci]
idex-bot May 24, 2024
b10abf4
feat(buy/sell panel): support taker limit price input
fubar May 28, 2024
d3c7234
feat: calculate max maker order size for available collateral
fubar May 28, 2024
ce899d1
Merge pull request #326 from idexio/feat/orderbook-realtime-perf
bradennapier May 28, 2024
667a3a8
chore(release): 4.0.0-alpha.13 [skip ci]
idex-bot May 28, 2024
8bb0cc5
Merge pull request #327 from idexio/feat/orderbook-realtime-perf
bradennapier May 28, 2024
3e6f7b5
chore(release): 4.0.0-alpha.14 [skip ci]
idex-bot May 28, 2024
ba78b5f
feat: merge beta to alpha
bradennapier May 28, 2024
114c9b3
Merge branch 'alpha' of github.com:idexio/idex-sdk-js into alpha
bradennapier May 28, 2024
a5976fe
chore(release): 4.0.0-alpha.15 [skip ci]
idex-bot May 28, 2024
d48470f
feat(buy/sell panel): calculate maker order size (quantities)
fubar May 28, 2024
fe63ae3
Merge branch 'alpha' into feat/buy-sell-panel-estimate-extensions
fubar May 28, 2024
0195bba
Merge pull request #325 from idexio/feat/buy-sell-panel-estimate-exte…
fubar May 28, 2024
4991206
chore(release): 4.0.0-alpha.16 [skip ci]
idex-bot May 28, 2024
02f35e5
fix(buy/sell panel): maker order IMR is based on limit (not index) price
fubar May 29, 2024
9eb671f
feat(buy/sell panel): calculate maker order size by desired trade qty
fubar May 29, 2024
3957057
feat(buy/sell panel): return all values unsigned
fubar May 29, 2024
3357f19
feat(buy/sell panel): add cost to returned values
fubar May 29, 2024
de10932
Merge pull request #329 from idexio/feat/buy-sell-panel-estimate-exte…
fubar May 29, 2024
c11d8e3
chore(release): 4.0.0-alpha.17 [skip ci]
idex-bot May 29, 2024
46a5f14
fix(buy/sell panel): maker qtys are wrong for sells
fubar Jun 3, 2024
bdff536
Merge pull request #330 from idexio/fix/buy-sell-estimate-wrong-maker…
fubar Jun 3, 2024
9b098fa
chore(release): 4.0.0-alpha.18 [skip ci]
idex-bot Jun 3, 2024
a789721
fix: obs realtime client applies indexPrice changes
yennieb Jun 4, 2024
850e420
Merge pull request #331 from idexio/fix/obs-realtime-client-index-pri…
yennieb Jun 4, 2024
6f7e5de
chore(release): 4.0.0-alpha.19 [skip ci]
idex-bot Jun 4, 2024
761a306
Merge branch 'alpha' into chore/update-alpha
fubar Jun 28, 2024
cf45c83
Merge pull request #338 from idexio/chore/update-alpha
fubar Jun 28, 2024
90b0406
chore(release): 4.0.0-alpha.20 [skip ci]
idex-bot Jun 28, 2024
25f9f7e
test(buy/sell panel): cover reduction of positions
fubar Jul 1, 2024
cd7ab72
Merge pull request #339 from idexio/test/buy-sell-panel-estimate-redu…
fubar Jul 1, 2024
9c66abf
feat(buy/sell panel): reflect change in IMR for standing orders in cost
fubar Jul 30, 2024
44814e0
Merge pull request #344 from idexio/feat/buy-sell-panel-estimate-cost…
fubar Jul 30, 2024
07f96bb
chore(release): 4.0.0-alpha.21 [skip ci]
idex-bot Jul 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@idexio/idex-sdk",
"version": "4.0.0-beta.9",
"version": "4.0.0-alpha.8",
"description": "IDEX SDK for Javascript in the browser and Node.js",
"repository": "git@github.com:idexio/idex-sdk-js.git",
"license": "MIT",
Expand Down Expand Up @@ -103,6 +103,8 @@
"lint": "eslint --cache 'src/**/*.ts'",
"lint:fix": "eslint --cache 'src/**/*.ts' --fix",
"lint:fix:staged": "lint-staged",
"lint:fix:staged-and-modified": "eslint --cache --fix $(git diff --name-only HEAD | grep -E '\\.(ts|tsx)$' | xargs) && yarn lint:prettier:staged-and-modified",
"lint:prettier:staged-and-modified": "prettier --write $(git diff --name-only HEAD | grep -E '\\.(ts|tsx)$' | xargs)",
"lint:types": "yarn exec tsc --noEmit",
"on:commit": "npm-run-all build test --parallel lint:fix:staged lint:types --",
"orderbook:demo": "yarn node dist/orderbook/demo.js",
Expand Down
314 changes: 287 additions & 27 deletions src/orderbook/quantities.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { BigNumber } from 'bignumber.js';

import * as pipmath from '#pipmath';
import {
ROUNDING,
absBigInt,
arraySumBigInt,
decimalToPip,
divideBigInt,
maxBigInt,
minBigInt,
multiplyPips,
oneInPips,
} from '#pipmath';

import { OrderSide } from '#types/enums/request';

Expand All @@ -9,6 +19,19 @@ import type {
OrderBookLevelL1,
OrderBookLevelL2,
} from '#types/orderBook';
import type { IDEXMarket, IDEXPosition } from '#types/rest/endpoints/index';

type LeverageParameters = Pick<
IDEXMarket,
| 'maximumPositionSize'
| 'initialMarginFraction'
| 'maintenanceMarginFraction'
| 'basePositionSize'
| 'incrementalPositionSize'
| 'incrementalInitialMarginFraction'
>;

export type LeverageParametersBigInt = Record<keyof LeverageParameters, bigint>;

/**
* Price and Size values form the {@link OrderBookLevelL1} type
Expand All @@ -27,37 +50,274 @@ export const nullLevel: OrderBookLevelL2 = {
};

/**
* Helper function to convert from quote to base quantities
* see: {quantitiesAvailableFromPoolAtAskPrice}
* Determines the liquidity (expressed in the market's base asset) that can be
* taken off the given order book (asks or bids) by spending the specified
* fraction of a wallet's available collateral and taking into account the
* margin requirement for the newly acquired position balance.
*
* Also returns the cost basis of the newly acquired position balance (i.e. the
* quote quantity that is exchanged to acquire the position balance).
*
* Both values are signed (positive for buys, negative for sells).
*
* The provided list of orders or price levels (asks or bids) is expected to be
* sorted by best price (ascending for asks (lowest first), descending for bids
* (highest first)). Multiple orders per price level are supported.
*/
export function calculateGrossBaseValueOfBuyQuantities(
baseAssetQuantity: bigint,
quoteAssetQuantity: bigint,
grossQuoteQuantity: bigint,
): bigint {
return (
baseAssetQuantity -
(baseAssetQuantity * quoteAssetQuantity) /
(quoteAssetQuantity + grossQuoteQuantity)
export function calculateBuySellPanelEstimate(args: {
/** All the wallet's open positions, including any in the current market */
allWalletPositions: IDEXPosition[];
/** Free collateral committed to open limit orders (unsigned) */
heldCollateral: bigint;
initialMarginFractionOverride: bigint | null;
leverageParameters: LeverageParameters;
makerSideOrders: Iterable<PriceAndSize>;
market: Pick<IDEXMarket, 'market' | 'indexPrice'>;
/** Quote token balance (USDC) (signed) */
quoteBalance: bigint;
/**
* Floating point number between 0 and 1 that indicates the amount of the
* available collateral to be spent.
*/
sliderFactor: number;
takerSide: OrderSide;
}): {
baseQuantity: bigint;
quoteQuantity: bigint;
} {
const {
allWalletPositions,
market,
heldCollateral,
initialMarginFractionOverride,
leverageParameters,
makerSideOrders,
quoteBalance,
sliderFactor,
takerSide,
} = args;

/*
* Slider calculations
*/
const accountValue =
quoteBalance + calculateNotionalQuoteValueOfPositions(allWalletPositions);

const initialMarginRequirementOfAllPositions = arraySumBigInt(
allWalletPositions.map((position) =>
decimalToPip(position.marginRequirement),
),
);
const initialAvailableCollateral =
accountValue - initialMarginRequirementOfAllPositions - heldCollateral;

if (initialAvailableCollateral <= BigInt(0) || sliderFactor === 0) {
return {
baseQuantity: BigInt(0),
quoteQuantity: BigInt(0),
};
}
if (sliderFactor < 0 || sliderFactor > 1) {
throw new Error(
'sliderFactor must be a floating point number between 0 and 1',
);
}
const sliderFactorInPips = decimalToPip(sliderFactor.toString());

const remainingAvailableCollateral =
initialAvailableCollateral * (oneInPips - sliderFactorInPips);

/*
* Execute against order book
*/
const indexPrice = decimalToPip(market.indexPrice);
const indexPrice2p = indexPrice * oneInPips;

const heldCollateral2p = heldCollateral * oneInPips;

const currentPosition = allWalletPositions.find(
(position) => position.market === market.market,
);
const otherPositions = allWalletPositions.filter(
(position) => position.market !== market.market,
);

const quoteValueOfOtherPositions =
calculateNotionalQuoteValueOfPositions(otherPositions);
const quoteValueOfOtherPositions2p = quoteValueOfOtherPositions * oneInPips;

const initialMarginRequirementOfOtherPositions = arraySumBigInt(
otherPositions.map((position) => decimalToPip(position.marginRequirement)),
);
const initialMarginRequirementOfOtherPositions2p =
initialMarginRequirementOfOtherPositions * oneInPips;

const leverageParametersBigInt =
convertToLeverageParametersBigInt(leverageParameters);

let additionalPositionQty = BigInt(0); // Signed
let additionalPositionCostBasis = BigInt(0); // Signed
let quoteBalance2p = quoteBalance * oneInPips; // Signed

for (const makerOrder of makerSideOrders) {
if (!doOrdersMatch(makerOrder, { side: takerSide })) {
return {
baseQuantity: additionalPositionQty,
quoteQuantity: additionalPositionCostBasis,
};
}
if (
takerSide === 'buy' ?
indexPrice >= makerOrder.price
: indexPrice <= makerOrder.price
) {
return {
baseQuantity: additionalPositionQty,
quoteQuantity: additionalPositionCostBasis,
};
}
const makerOrderPrice2p = makerOrder.price * oneInPips;

const positionBalance =
(currentPosition ? decimalToPip(currentPosition.quantity) : BigInt(0)) +
additionalPositionQty;

const quoteValueOfPosition2p = positionBalance * indexPrice; // Signed

const initialMarginFraction = calculateInitialMarginFractionWithOverride({
initialMarginFractionOverride,
leverageParameters: leverageParametersBigInt,
positionBalance,
});

// Unsigned
const initialMarginRequirementOfPosition2p = multiplyPips(
absBigInt(quoteValueOfPosition2p),
initialMarginFraction,
);

// Signed
const maxTakerBaseQty =
((-quoteBalance2p -
quoteValueOfPosition2p -
quoteValueOfOtherPositions2p +
heldCollateral2p +
remainingAvailableCollateral +
initialMarginRequirementOfPosition2p +
initialMarginRequirementOfOtherPositions2p) *
oneInPips) /
(indexPrice2p -
makerOrderPrice2p +
BigInt(takerSide === 'buy' ? -1 : 1) *
indexPrice *
initialMarginFraction);

if (absBigInt(maxTakerBaseQty) < makerOrder.size) {
additionalPositionQty += maxTakerBaseQty;
additionalPositionCostBasis += multiplyPips(
maxTakerBaseQty,
makerOrder.price,
);
return {
baseQuantity: additionalPositionQty,
quoteQuantity: additionalPositionCostBasis,
};
}
const tradeBaseQty = makerOrder.size * BigInt(takerSide === 'buy' ? 1 : -1);
const tradeQuoteQty = multiplyPips(tradeBaseQty, makerOrder.price);
additionalPositionQty += tradeBaseQty;
additionalPositionCostBasis += tradeQuoteQty;

quoteBalance2p -= tradeBaseQty * makerOrder.price;
}
return {
baseQuantity: additionalPositionQty,
quoteQuantity: additionalPositionCostBasis,
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor calculateBuySellPanelEstimate to improve readability by breaking down complex logic into smaller, more manageable functions.

-  const accountValue = quoteBalance + calculateNotionalQuoteValueOfPositions(allWalletPositions);
-  const initialMarginRequirementOfAllPositions = arraySumBigInt(allWalletPositions.map((position) => decimalToPip(position.marginRequirement)));
+  const accountValue = calculateAccountValue(quoteBalance, allWalletPositions);
+  const initialMarginRequirementOfAllPositions = calculateTotalMarginRequirements(allWalletPositions);

Committable suggestion was skipped due low confidence.

}

/**
* Helper function to convert from base to quote quantities
* see: {quantitiesAvailableFromPoolAtBidPrice}
* @private
*/
export function calculateGrossQuoteValueOfSellQuantities(
baseAssetQuantity: bigint,
quoteAssetQuantity: bigint,
grossBaseQuantity: bigint,
function calculateInitialMarginFraction(
leverageParameters: LeverageParametersBigInt,
positionBalance: bigint,
): bigint {
const absPositionBalance = absBigInt(positionBalance);
if (absPositionBalance <= leverageParameters.basePositionSize) {
return leverageParameters.initialMarginFraction;
}
return (
quoteAssetQuantity -
(baseAssetQuantity * quoteAssetQuantity) /
(baseAssetQuantity + grossBaseQuantity)
leverageParameters.initialMarginFraction +
divideBigInt(
absPositionBalance - leverageParameters.basePositionSize,
leverageParameters.incrementalPositionSize,
ROUNDING.RoundUp,
) *
leverageParameters.incrementalInitialMarginFraction
);
}

/**
* @private
*/
function calculateInitialMarginFractionWithOverride(args: {
initialMarginFractionOverride: bigint | null;
leverageParameters: LeverageParametersBigInt;
positionBalance: bigint;
}): bigint {
const { initialMarginFractionOverride, leverageParameters, positionBalance } =
args;

return maxBigInt(
calculateInitialMarginFraction(leverageParameters, positionBalance),
initialMarginFractionOverride ?? BigInt(0),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review the use of maxBigInt in calculateInitialMarginFractionWithOverride.

The use of maxBigInt in calculateInitialMarginFractionWithOverride assumes that the override value is always greater or equal to the calculated margin. This might not always be the case, depending on business logic. Verify that this assumption holds in all use cases, or adjust the logic to handle different scenarios.

);
}

/**
* @private
*/
function calculateNotionalQuoteValueOfPosition(position: IDEXPosition): bigint {
return multiplyPips(
decimalToPip(position.quantity),
decimalToPip(position.indexPrice),
);
}

/**
* @private
*/
function calculateNotionalQuoteValueOfPositions(
positions: IDEXPosition[],
): bigint {
return arraySumBigInt(positions.map(calculateNotionalQuoteValueOfPosition));
}

/**
* @private
*/
function convertToLeverageParametersBigInt(
leverageParameters: LeverageParameters,
): LeverageParametersBigInt {
return {
maximumPositionSize: decimalToPip(leverageParameters.maximumPositionSize),
initialMarginFraction: decimalToPip(
leverageParameters.initialMarginFraction,
),
maintenanceMarginFraction: decimalToPip(
leverageParameters.maintenanceMarginFraction,
),
basePositionSize: decimalToPip(leverageParameters.basePositionSize),
incrementalPositionSize: decimalToPip(
leverageParameters.incrementalPositionSize,
),
incrementalInitialMarginFraction: decimalToPip(
leverageParameters.incrementalInitialMarginFraction,
),
};
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

Codebase Verification

The functions calculateInitialMarginFraction and convertToLeverageParametersBigInt are used within the src/orderbook/quantities.ts file but are not directly tested in the identified test files. This indicates that these functions might not have dedicated unit tests.

  • Ensure that the functions calculateInitialMarginFraction and convertToLeverageParametersBigInt are thoroughly unit tested to verify their correctness.
Analysis chain

Ensure that private functions like calculateInitialMarginFraction and convertToLeverageParametersBigInt are thoroughly unit tested to verify their correctness.

Scripts Executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Description: Verify unit tests for private functions in `quantities.ts`.

# Test: Search for unit tests covering these functions. Expect: Comprehensive test coverage.
rg --type python $'calculateInitialMarginFraction|convertToLeverageParametersBigInt'

Length of output: 116



Script:

#!/bin/bash
# Description: Verify unit tests for private functions in `quantities.ts`.

# Test: Search for unit tests covering these functions. Expect: Comprehensive test coverage.
rg --type js 'calculateInitialMarginFraction|convertToLeverageParametersBigInt'

Length of output: 230



Script:

#!/bin/bash
# Description: Debug ripgrep to understand why files are being skipped.

# Test: Run ripgrep with --debug to identify why no files are being searched.
rg --type js 'calculateInitialMarginFraction|convertToLeverageParametersBigInt' --debug

Length of output: 43689



Script:

#!/bin/bash
# Description: Verify unit tests for private functions in `quantities.ts`.

# Test: Search for unit tests covering these functions. Expect: Comprehensive test coverage.
rg --type js 'calculateInitialMarginFraction|convertToLeverageParametersBigInt' --no-ignore

Length of output: 242



Script:

#!/bin/bash
# Description: Verify unit tests for private functions in `quantities.ts`.

# Step 1: Find all test files.
test_files=$(fd --type f --extension js --extension ts --glob '*test*')

# Step 2: Search for the function calls within the test files.
for file in $test_files; do
  echo "Searching in $file"
  rg 'calculateInitialMarginFraction|convertToLeverageParametersBigInt' "$file"
done

Length of output: 1407



Script:

#!/bin/bash
# Description: Verify usage of private functions in the entire codebase.

# Step 1: Search for the usage of the functions in the entire codebase.
rg 'calculateInitialMarginFraction|convertToLeverageParametersBigInt'

Length of output: 595

Comment on lines +984 to +1003
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimize convertToLeverageParametersBigInt by caching repeated conversions.

The function converts multiple parameters using decimalToPip. If these conversions are costly and the input parameters do not change frequently, consider caching the results to improve performance.


/**
* Determines the liquidity available in the given order book (asks or bids)
* for a given taker quantity (which may be expressed in base or quote asset)
Expand Down Expand Up @@ -93,7 +353,7 @@ export function calculateGrossFillQuantities(
baseQuantity: bigint;
quoteQuantity: bigint;
} {
const takerQuantity2p = takerOrder.quantity * pipmath.oneInPips;
const takerQuantity2p = takerOrder.quantity * oneInPips;

let filledBaseQty2p = BigInt(0);
let filledQuoteQty2p = BigInt(0);
Expand All @@ -109,8 +369,8 @@ export function calculateGrossFillQuantities(
};
}
return {
baseQuantity: filledBaseQty2p / pipmath.oneInPips,
quoteQuantity: filledQuoteQty2p / pipmath.oneInPips,
baseQuantity: filledBaseQty2p / oneInPips,
quoteQuantity: filledQuoteQty2p / oneInPips,
};
};

Expand Down Expand Up @@ -153,15 +413,15 @@ function determineTradeQuantities(
baseQuantity2p: bigint;
quoteQuantity2p: bigint;
} {
const makerQuantity2p = makerOrder.size * pipmath.oneInPips;
const makerQuantity2p = makerOrder.size * oneInPips;

// Limit by base
const fillBaseQty2p =
isMaxTakerQuantityInQuote ? makerQuantity2p : (
pipmath.minBigInt(maxTakerQuantity2p, makerQuantity2p)
minBigInt(maxTakerQuantity2p, makerQuantity2p)
);

const fillQuoteQty2p = pipmath.multiplyPips(fillBaseQty2p, makerOrder.price);
const fillQuoteQty2p = multiplyPips(fillBaseQty2p, makerOrder.price);
Comment on lines +1113 to +1121
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle division by zero in determineTradeQuantities.

The function does not explicitly handle division by zero, which could potentially lead to runtime errors if such a situation arises.

+ if (maxTakerQuantity2p === BigInt(0)) {
+   throw new Error('Division by zero error');
+ }
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
const makerQuantity2p = makerOrder.size * oneInPips;
// Limit by base
const fillBaseQty2p =
isMaxTakerQuantityInQuote ? makerQuantity2p : (
pipmath.minBigInt(maxTakerQuantity2p, makerQuantity2p)
minBigInt(maxTakerQuantity2p, makerQuantity2p)
);
const fillQuoteQty2p = pipmath.multiplyPips(fillBaseQty2p, makerOrder.price);
const fillQuoteQty2p = multiplyPips(fillBaseQty2p, makerOrder.price);
const makerQuantity2p = makerOrder.size * oneInPips;
// Limit by base
const fillBaseQty2p =
isMaxTakerQuantityInQuote ? makerQuantity2p : (
minBigInt(maxTakerQuantity2p, makerQuantity2p)
);
if (maxTakerQuantity2p === BigInt(0)) {
throw new Error('Division by zero error');
}
const fillQuoteQty2p = multiplyPips(fillBaseQty2p, makerOrder.price);


// Limit by quote
if (isMaxTakerQuantityInQuote && maxTakerQuantity2p < fillQuoteQty2p) {
Expand Down
Loading
Loading