Skip to content

Commit

Permalink
feat(market): Add spread to market and getBest to semibook (#1719)
Browse files Browse the repository at this point in the history
  • Loading branch information
lnist authored Jan 11, 2024
1 parent ac46af2 commit 01be9b8
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 4 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

- feat: Moved MangroveJsDeploy from mangrove-strats to this package. Renamed script to EmptyChainDeployer
- fix: rounding when deducing tick from price for LiquidityProvider
- feat: Add spread to market
- feat: Add getBest to semibook

# 2.0.0

Expand Down
37 changes: 37 additions & 0 deletions src/market.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ for more on big.js vs decimals.js vs. bignumber.js (which is *not* ethers's BigN
*/
import Big from "big.js";
import { Density } from "./util/Density";
import TickPriceHelper from "./util/tickPriceHelper";

let canConstructMarket = false;

Expand Down Expand Up @@ -778,6 +779,42 @@ class Market {
};
}

/**
* Gets the absolute, relative, and tick spread between bids and asks on the market.
*/
async spread() {
const { asks, bids } = this.getBook();

const bestAsk = await asks.getBest();
const bestBid = await bids.getBest();

return Market.spread(this, bestAsk, bestBid);
}

/**
* Gets the absolute, relative, and tick spread between a bid and an ask on the market.
*/
static spread(
market: Market.KeyResolvedForCalculation,
bestAsk?: { price: Bigish; tick: number },
bestBid?: { price: Bigish; tick: number },
) {
if (!bestAsk || !bestBid) {
return {};
}
const lowestAskPrice = Big(bestAsk.price);
const highestBidPrice = Big(bestBid.price);
const absoluteSpread = lowestAskPrice.sub(highestBidPrice);
const tickSpread = bestAsk.tick + bestBid.tick;
// Intentionally using raw ratio as we do not want decimals scaling
// Rounding is irrelevant as ticks already respects tick spacing
const relativeSpread = new TickPriceHelper("asks", market)
.rawRatioFromTick(tickSpread, "roundUp")
.sub(1);

return { absoluteSpread, relativeSpread, tickSpread };
}

/**
* Is the market active?
* @returns Whether the market is active, i.e., whether both the asks and bids semibooks are active.
Expand Down
24 changes: 24 additions & 0 deletions src/semibook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,30 @@ class Semibook
return state.bestBinInCache?.firstOfferId;
}

/** Returns the best offer if any */
async getBest(): Promise<Market.Offer | undefined> {
const state = this.getLatestState();
const result = await this.#foldLeftUntil<{
offer: Market.Offer | undefined;
}>(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.lastSeenEventBlock!,
state,
{
offer: undefined,
},
(acc) => {
return acc.offer !== undefined;
},
(cur, acc) => {
acc.offer = cur;
return acc;
},
);

return result.offer;
}

/** Returns an iterator over the offers in the cache. */
[Symbol.iterator](): Semibook.CacheIterator {
const state = this.getLatestState();
Expand Down
74 changes: 73 additions & 1 deletion test/integration/market.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,78 @@ describe("Market integration tests suite", () => {
});
});

describe("spread", () => {
let market: Market;
const createBid = async () => {
const { response } = await market.buy({
limitPrice: 2,
total: 1,
restingOrder: {},
});
const tx = await waitForTransaction(response);
await waitForBlock(market.mgv, tx.blockNumber);
};

const createAsk = async () => {
const { response } = await market.sell({
limitPrice: 3,
volume: 1,
restingOrder: {},
});
const tx = await waitForTransaction(response);
await waitForBlock(market.mgv, tx.blockNumber);
};

beforeEach(async function () {
market = await mgv.market({
base: "TokenB",
quote: "TokenA",
tickSpacing: 1,
});

// Approve router
const orderLogic = mgv.offerLogic(mgv.orderContract.address);
const routerAddress = (await orderLogic.router())!.address;
await waitForTransaction(market.base.approve(routerAddress));
await waitForTransaction(market.quote.approve(routerAddress));
});

it("with offers", async () => {
// Arrange
await createBid();
await createAsk();

// Act
const { absoluteSpread, relativeSpread, tickSpread } =
await market.spread();

// Assert
helpers.assertApproxEqRel(absoluteSpread, 1, 0.0003);
helpers.assertApproxEqRel(relativeSpread, 0.5, 0.0004);
assert.equal(tickSpread, 4056);
});

["bids", "asks", "none"].forEach((ba) => {
it(`with ${ba} on book`, async () => {
// Arrange
if (ba === "bids") {
await createBid();
} else if (ba === "asks") {
await createAsk();
}

// Act
const { absoluteSpread, relativeSpread, tickSpread } =
await market.spread();

// Assert
assert.equal(absoluteSpread, undefined);
assert.equal(relativeSpread, undefined);
assert.equal(tickSpread, undefined);
});
});
});

describe("getOutboundInbound", () => {
it("returns base as outbound and quote as inbound, when asks", async function () {
//Arrange
Expand Down Expand Up @@ -336,7 +408,7 @@ describe("Market integration tests suite", () => {
expect(result).to.be.equal(true);
});

it("returns false, when gives is less than 1", async function () {
it("returns false, when gives is 0", async function () {
// Arrange
const market = await mgv.market({
base: "TokenB",
Expand Down
21 changes: 21 additions & 0 deletions test/integration/semibook.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,25 @@ describe("Semibook integration tests suite", function () {
const semibook = market.getSemibook("asks");
expect(semibook.size()).to.equal(0);
expect(semibook.getLatestState().isComplete).to.equal(true);
expect(await semibook.getBest()).to.equal(undefined);
});

it("does not fail with empty incomplete cache", async function () {
await createOffers(2);
const market = await mgv.market({
base: "TokenA",
quote: "TokenB",
tickSpacing: 1,
bookOptions: {
targetNumberOfTicks: 0,
chunkSize: 1,
},
});
const semibook = market.getSemibook("asks");
expect(semibook.size()).to.equal(0);
expect(semibook.getLatestState().isComplete).to.equal(false);
const best = await semibook.getBest();
expect(best?.tick).to.equal(1);
});

it("fetches only one chunk if the first contains the target number of ticks", async function () {
Expand Down Expand Up @@ -947,6 +966,8 @@ describe("Semibook integration tests suite", function () {
expect(semibook.size()).to.equal(2);
expect(semibook.getLatestState().binCache.size).to.equal(2);
expect(semibook.getLatestState().isComplete).to.equal(false);
const best = await semibook.getBest();
expect(best?.tick).to.equal(1);
});

it("fetches multiple chunks until at least target number of ticks have been fetched, then stops, ignoring partially fetched extra ticks", async function () {
Expand Down
9 changes: 6 additions & 3 deletions test/util/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,19 @@ export const assertApproxEqAbs = (
};

export const assertApproxEqRel = (
actual: Bigish,
actual: Bigish | undefined,
expected: Bigish,
deltaRel: Bigish,
message?: string,
) => {
if (!Big(actual).sub(Big(expected)).abs().div(expected).lte(Big(deltaRel))) {
const diff = actual
? Big(actual).sub(Big(expected)).abs().div(expected)
: undefined;
if (!diff || !diff.lte(Big(deltaRel))) {
assert.fail(
`${
message ? message + ": " : ""
}expected actual=${actual} to be within relative ${deltaRel} of expected=${expected}`,
}expected actual=${actual} to be within relative ${deltaRel} of expected=${expected} but was ${diff}`,
);
}
};
Expand Down

0 comments on commit 01be9b8

Please sign in to comment.