-
Notifications
You must be signed in to change notification settings - Fork 98
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add sdk classes for market & orderbook
- Loading branch information
1 parent
eb9c479
commit 1fd7c1b
Showing
9 changed files
with
418 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
|
||
} | ||
|
Oops, something went wrong.