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

Merge master into develop #1722

Merged
merged 1 commit into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading