Skip to content

Commit

Permalink
add sdk classes for market & orderbook
Browse files Browse the repository at this point in the history
  • Loading branch information
mschneider committed Feb 18, 2024
1 parent eb9c479 commit 1fd7c1b
Show file tree
Hide file tree
Showing 9 changed files with 418 additions and 49 deletions.
158 changes: 158 additions & 0 deletions ts/client/src/accounts/bookSide.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { PublicKey } from "@solana/web3.js";
import { Market, BookSideAccount, SideUtils, Side, OpenBookV2Client, LeafNode, InnerNode, U64_MAX_BN } from "..";
import { BN } from "@coral-xyz/anchor";
import { Order } from "../structs/order";

export class BookSide {

public clusterTime: BN;

constructor(
public market: Market,
public pubkey: PublicKey,
public account: BookSideAccount,
public side: Side) {
this.clusterTime = new BN(0);
}

public *items(): Generator<Order> {
const fGen = this.fixedItems();
const oPegGen = this.oraclePeggedItems();

let fOrderRes = fGen.next();
let oPegOrderRes = oPegGen.next();

while (true) {
if (fOrderRes.value && oPegOrderRes.value) {
if (this.compareOrders(fOrderRes.value, oPegOrderRes.value)) {
yield fOrderRes.value;
fOrderRes = fGen.next();
} else {
yield oPegOrderRes.value;
oPegOrderRes = oPegGen.next();
}
} else if (fOrderRes.value && !oPegOrderRes.value) {
yield fOrderRes.value;
fOrderRes = fGen.next();
} else if (!fOrderRes.value && oPegOrderRes.value) {
yield oPegOrderRes.value;
oPegOrderRes = oPegGen.next();
} else if (!fOrderRes.value && !oPegOrderRes.value) {
break;
}
}
}

get rootFixed() {
return this.account.roots[0];
}

get rootOraclePegged() {
return this.account.roots[1];
}

public *fixedItems(): Generator<Order> {
if (this.rootFixed.leafCount === 0) {
return;
}
const stack = [this.rootFixed.maybeNode];
const [left, right] = this.side === SideUtils.Bid ? [1, 0] : [0, 1];

while (stack.length > 0) {
const index = stack.pop()!;
const node = this.account.nodes.nodes[index];
if (node.tag === BookSide.INNER_NODE_TAG) {
const innerNode = this.toInnerNode(node.data);
stack.push(innerNode.children[right], innerNode.children[left]);
} else if (node.tag === BookSide.LEAF_NODE_TAG) {
const leafNode = this.toLeafNode(node.data);
const expiryTimestamp = leafNode.timeInForce
? leafNode.timestamp.add(new BN(leafNode.timeInForce))
: U64_MAX_BN;

yield new Order(
this.market,
leafNode,
this.side,
this.clusterTime.gt(expiryTimestamp),
);
}
}
}

public *oraclePeggedItems(): Generator<Order> {
if (this.rootOraclePegged.leafCount === 0) {
return;
}
const stack = [this.rootOraclePegged.maybeNode];
const [left, right] = this.side === SideUtils.Bid ? [1, 0] : [0, 1];

while (stack.length > 0) {
const index = stack.pop()!;
const node = this.account.nodes.nodes[index];
if (node.tag === BookSide.INNER_NODE_TAG) {
const innerNode = this.toInnerNode(node.data);
stack.push(innerNode.children[right], innerNode.children[left]);
} else if (node.tag === BookSide.LEAF_NODE_TAG) {
const leafNode = this.toLeafNode(node.data);
const expiryTimestamp = leafNode.timeInForce
? leafNode.timestamp.add(new BN(leafNode.timeInForce))
: U64_MAX_BN;

yield new Order(
this.market,
leafNode,
this.side,
this.clusterTime.gt(expiryTimestamp),
true,
);
}
}
}

public compareOrders(a: Order, b: Order): boolean {
return a.priceLots.eq(b.priceLots)
? a.seqNum.lt(b.seqNum) // if prices are equal prefer orders in the order they are placed
: this.side === SideUtils.Bid // else compare the actual prices
? a.priceLots.gt(b.priceLots)
: b.priceLots.gt(a.priceLots);
}

public best(): Order | undefined {
return this.items().next().value;
}

public getL2(depth: number): [number, number, BN, BN][] {
const levels: [BN, BN][] = [];
for (const { priceLots, sizeLots } of this.items()) {
if (levels.length > 0 && levels[levels.length - 1][0].eq(priceLots)) {
levels[levels.length - 1][1].iadd(sizeLots);
} else if (levels.length === depth) {
break;
} else {
levels.push([priceLots, sizeLots]);
}
}
return levels.map(([priceLots, sizeLots]) => [
this.market.priceLotsToUi(priceLots),
this.market.baseLotsToUi(sizeLots),
priceLots,
sizeLots,
]);
}

private static INNER_NODE_TAG = 1;
private static LEAF_NODE_TAG = 2;

private toInnerNode(data: number[]): InnerNode {
return (this.market.client.program as any)._coder.types.typeLayouts
.get('InnerNode')
.decode(Buffer.from([BookSide.INNER_NODE_TAG].concat(data)));
}
private toLeafNode(data: number[]): LeafNode {
return (this.market.client.program as any)._coder.types.typeLayouts
.get('LeafNode')
.decode(Buffer.from([BookSide.LEAF_NODE_TAG].concat(data)));
}

}
126 changes: 126 additions & 0 deletions ts/client/src/accounts/market.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import Big from 'big.js';
import { BN } from '@coral-xyz/anchor';
import { PublicKey } from "@solana/web3.js";
import { toNative, MarketAccount, OpenBookV2Client, BookSideAccount, BookSide, SideUtils, nameToString } from '..';

export class Market {
public minOrderSize: number;
public tickSize: number;
public quoteLotFactor: number;

/**
* use async loadBids() or loadOrderBook() to populate bids
*/
public bids: BookSide | undefined;

/**
* use async loadAsks() or loadOrderBook() to populate asks
*/
public asks: BookSide | undefined;

constructor(public client: OpenBookV2Client, public pubkey: PublicKey, public account: MarketAccount) {
this.minOrderSize = new Big(10)
.pow(account.baseDecimals - account.quoteDecimals)
.mul(new Big(account.quoteLotSize.toString()))
.div(new Big(account.baseLotSize.toString()))
.toNumber();
this.quoteLotFactor = new Big(account.quoteLotSize.toString())
.div(new Big(10).pow(account.quoteDecimals))
.toNumber();
this.tickSize = new Big(10)
.pow(account.baseDecimals - account.quoteDecimals)
.mul(new Big(account.quoteLotSize.toString()))
.div(new Big(account.baseLotSize.toString()))
.toNumber();
}

public static async load(client: OpenBookV2Client, pubkey: PublicKey): Promise<Market> {
const account = await client.program.account.market.fetch(pubkey);
return new Market(client, pubkey, account);
}

public baseLotsToUi(lots: BN): number {
return Number(lots.toString()) * this.minOrderSize;
}
public quoteLotsToUi(lots: BN): number {
return Number(lots.toString()) * this.quoteLotFactor;
}
public priceLotsToUi(lots: BN): number {
return Number(lots.toString()) * this.tickSize;
}

public baseUiToLots(uiAmount: number): BN {
return toNative(uiAmount, this.account.baseDecimals).div(this.account.baseLotSize);
}
public quoteUiToLots(uiAmount: number): BN {
return toNative(uiAmount, this.account.quoteDecimals).div(this.account.quoteLotSize);
}
public priceUiToLots(uiAmount: number): BN {
return toNative(uiAmount, this.account.quoteDecimals)
.imul(this.account.baseLotSize)
.div(new BN(Math.pow(10, this.account.baseDecimals))
.imul(this.account.quoteLotSize));
}

public async loadBids(): Promise<BookSide> {
const bidSide = await this.client.program.account.bookSide.fetch(this.account.bids) as BookSideAccount;
this.bids = new BookSide(this, this.account.bids, bidSide, SideUtils.Bid);
return this.bids;
}

public async loadAsks(): Promise<BookSide> {
const askSide = await this.client.program.account.bookSide.fetch(this.account.asks) as BookSideAccount;
this.asks = new BookSide(this, this.account.asks, askSide, SideUtils.Ask);
return this.asks;
}

public async loadOrderBook(): Promise<this> {
await Promise.all([this.loadBids(), this.loadAsks()]);
return this;
}

public toPrettyString(): string {
const mkt = this.account;
let debug = `Market: ${nameToString(mkt.name)}\n`;
debug += ` authority: ${mkt.marketAuthority.toBase58()}\n`;
debug += ` collectFeeAdmin: ${mkt.collectFeeAdmin.toBase58()}\n`;
if (!mkt.openOrdersAdmin.key.equals(PublicKey.default))
debug += ` openOrdersAdmin: ${mkt.openOrdersAdmin.key.toBase58()}\n`;
if (!mkt.consumeEventsAdmin.key.equals(PublicKey.default))
debug += ` consumeEventsAdmin: ${mkt.consumeEventsAdmin.key.toBase58()}\n`;
if (!mkt.closeMarketAdmin.key.equals(PublicKey.default))
debug += ` closeMarketAdmin: ${mkt.closeMarketAdmin.key.toBase58()}\n`;

debug += ` baseMint: ${mkt.baseMint.toBase58()}\n`;
debug += ` quoteMint: ${mkt.quoteMint.toBase58()}\n`;
debug += ` marketBaseVault: ${mkt.marketBaseVault.toBase58()}\n`;
debug += ` marketQuoteVault: ${mkt.marketQuoteVault.toBase58()}\n`;

if (!mkt.oracleA.key.equals(PublicKey.default)) {
debug += ` oracleConfig: { confFilter: ${mkt.oracleConfig.confFilter}, maxStalenessSlots: ${mkt.oracleConfig.maxStalenessSlots.toString()} }\n`;
debug += ` oracleA: ${mkt.oracleA.key.toBase58()}\n`;
}
if (!mkt.oracleB.key.equals(PublicKey.default))
debug += ` oracleB: ${mkt.oracleB.key.toBase58()}\n`;

debug += ` bids: ${mkt.bids.toBase58()}\n`;
const bb = this.bids?.best();
if (bb) {
debug += ` best: ${bb.price} ${bb.size} ${bb.leafNode.owner.toBase58()}\n`;
}

debug += ` asks: ${mkt.asks.toBase58()}\n`;
const ba = this.asks?.best();
if (ba) {
debug += ` best: ${ba.price} ${ba.size} ${ba.leafNode.owner.toBase58()}\n`;
}

debug += ` eventHeap: ${mkt.eventHeap.toBase58()}\n`;

debug += ` minOrderSize: ${this.minOrderSize}\n`;
debug += ` tickSize: ${this.tickSize}\n`;

return debug;

}
}
71 changes: 71 additions & 0 deletions ts/client/src/accounts/openOrders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Keypair, PublicKey, Signer, TransactionInstruction } from "@solana/web3.js";
import { OpenOrdersAccount, PlaceOrderType, SelfTradeBehavior as SelfTradeBehaviorType, Side } from "../client";
import { Market } from "./market";
import { PlaceOrderTypeUtils, SelfTradeBehaviorUtils, U64_MAX_BN } from "../utils/utils";
import { BN } from "@coral-xyz/anchor";

export interface OrderToPlace {
side: Side;
price: number;
size: number;
quoteLimit?: number;
clientOrderId?: number;
orderType?: PlaceOrderType;
expiryTimestamp?: number;
selfTradeBehavior?: SelfTradeBehaviorType;
matchLoopLimit?: number;
}

export class OpenOrders {
public delegate: Keypair | undefined;

constructor(
public pubkey: PublicKey,
public account: OpenOrdersAccount,
public market: Market) {}

public setDelegate(delegate: Keypair): this {
this.delegate = delegate;
return this;
}

public async placeOrderIx(
order: OrderToPlace,
userTokenAccount: PublicKey,
remainingAccounts: PublicKey[] = [],
): Promise<[TransactionInstruction, Signer[]]> {
const priceLots = this.market.priceUiToLots(order.price);
const maxBaseLots = this.market.baseUiToLots(order.size);
const maxQuoteLotsIncludingFees = order.quoteLimit ? new BN(order.quoteLimit) : U64_MAX_BN;
const clientOrderId = new BN(order.clientOrderId || Date.now());
const orderType = order.orderType || PlaceOrderTypeUtils.Limit;
const expiryTimestamp = new BN(order.expiryTimestamp || -1);
const selfTradeBehavior = order.selfTradeBehavior || SelfTradeBehaviorUtils.DecrementTake;
const limit = order.matchLoopLimit || 16;

const args = {
side: order.side,
priceLots,
maxBaseLots,
maxQuoteLotsIncludingFees,
clientOrderId,
orderType,
expiryTimestamp,
selfTradeBehavior,
limit,
};

return await this.market.client.placeOrderIx(
this.pubkey,
this.market.pubkey,
this.market.account,
userTokenAccount,
null,
args,
remainingAccounts,
this.delegate);
}


}

Loading

0 comments on commit 1fd7c1b

Please sign in to comment.