Skip to content

Commit

Permalink
implemt event heap iterators (#241)
Browse files Browse the repository at this point in the history
* implemt event heap iterators

* fee math for events

* simplify

* fmt

* add event watcher
  • Loading branch information
mschneider authored Mar 25, 2024
1 parent 4e02fa8 commit 9fc3e83
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 20 deletions.
42 changes: 42 additions & 0 deletions ts/client/src/accounts/eventHeap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { PublicKey } from '@solana/web3.js';
import { AnyEvent, EventHeapAccount, FillEvent, Market, OutEvent } from '..';

export enum EventType {
Fill = 0,
Out = 1,
}

export class EventHeap {
constructor(
public pubkey: PublicKey,
public account: EventHeapAccount,
public market: Market,
) {}

public *rawEvents(): Generator<AnyEvent> {
let currentIndex = this.account.header.usedHead;
for (let i = 0; i < this.account.header.count; ++i) {
const { event, next } = this.account.nodes[currentIndex];
yield event;
currentIndex = next;
}
}

public *parsedEvents(): Generator<FillEvent | OutEvent> {
const { decode } = this.market.client.program.coder.types;
for (const event of this.rawEvents()) {
// TODO find out how not to re-allocate
const buffer = Buffer.from([event.eventType].concat(event.padding));
switch (event.eventType) {
case EventType.Fill: {
yield decode('FillEvent', buffer);
continue;
}
case EventType.Out: {
yield decode('OutEvent', buffer);
continue;
}
}
}
}
}
53 changes: 51 additions & 2 deletions ts/client/src/accounts/market.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,19 @@ import {
BookSide,
SideUtils,
nameToString,
EventHeapAccount,
EventHeap,
EventType,
} from '..';

const FEES_SCALE_FACTOR = new BN(1_000_000);

export class Market {
public minOrderSize: Big;
public tickSize: Big;
public quoteLotFactor: Big;
public baseNativeFactor: Big;
public quoteNativeFactor: Big;

/**
* use async loadBids() or loadOrderBook() to populate bids
Expand All @@ -25,17 +32,20 @@ export class Market {
* use async loadAsks() or loadOrderBook() to populate asks
*/
public asks: BookSide | undefined;
public eventHeap: EventHeap | undefined;

constructor(
public client: OpenBookV2Client,
public pubkey: PublicKey,
public account: MarketAccount,
) {
this.baseNativeFactor = new Big(10).pow(-account.baseDecimals);
this.quoteNativeFactor = new Big(10).pow(-account.quoteDecimals);
this.minOrderSize = new Big(account.baseLotSize.toString()).mul(
new Big(10).pow(-account.baseDecimals),
this.baseNativeFactor,
);
this.quoteLotFactor = new Big(account.quoteLotSize.toString()).mul(
new Big(10).pow(-account.quoteDecimals),
this.quoteNativeFactor,
);
this.tickSize = new Big(10)
.pow(account.baseDecimals - account.quoteDecimals)
Expand All @@ -54,9 +64,15 @@ export class Market {
public baseLotsToUi(lots: BN): number {
return new Big(lots.toString()).mul(this.minOrderSize).toNumber();
}
public baseNativeToUi(native: BN): number {
return new Big(native.toString()).mul(this.baseNativeFactor).toNumber();
}
public quoteLotsToUi(lots: BN): number {
return new Big(lots.toString()).mul(this.quoteLotFactor).toNumber();
}
public quoteNativeToUi(native: BN): number {
return new Big(native.toString()).mul(this.quoteNativeFactor).toNumber();
}
public priceLotsToUi(lots: BN): number {
return new Big(lots.toString()).mul(this.tickSize).toNumber();
}
Expand All @@ -81,6 +97,10 @@ export class Market {
);
}

public makerFeeFloor(quoteNative: BN): BN {
return quoteNative.mul(this.account.makerFee).div(FEES_SCALE_FACTOR);
}

public async loadBids(): Promise<BookSide> {
const bidSide = (await this.client.program.account.bookSide.fetch(
this.account.bids,
Expand All @@ -97,6 +117,14 @@ export class Market {
return this.asks;
}

public async loadEventHeap(): Promise<EventHeap> {
const eventHeap = (await this.client.program.account.eventHeap.fetch(
this.account.eventHeap,
)) as EventHeapAccount;
this.eventHeap = new EventHeap(this.account.eventHeap, eventHeap, this);
return this.eventHeap;
}

public async loadOrderBook(): Promise<this> {
await Promise.all([this.loadBids(), this.loadAsks()]);
return this;
Expand Down Expand Up @@ -145,6 +173,27 @@ export class Market {
}

debug += ` eventHeap: ${mkt.eventHeap.toBase58()}\n`;
if (this.eventHeap) {
let fillEvents = 0;
let outEvents = 0;
for (const event of this.eventHeap.parsedEvents()) {
switch (event.eventType) {
case EventType.Fill: {
fillEvents += 1;
continue;
}
case EventType.Out: {
outEvents += 1;
continue;
}
}
}

debug += ` fillEvents: ${fillEvents}\n`;
debug += ` outEvents: ${outEvents}\n`;
} else {
debug += ` loaded: false\n`;
}

debug += ` minOrderSize: ${this.minOrderSize}\n`;
debug += ` tickSize: ${this.tickSize}\n`;
Expand Down
98 changes: 88 additions & 10 deletions ts/client/src/accounts/openOrders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,29 @@ import {
Keypair,
PublicKey,
Signer,
Transaction,
TransactionInstruction,
} from '@solana/web3.js';
import { BN } from '@coral-xyz/anchor';
import { createAssociatedTokenAccountIdempotentInstruction } from '@solana/spl-token';
import {
FillEvent,
OpenBookV2Client,
OpenOrdersAccount,
OutEvent,
PlaceOrderType,
SelfTradeBehavior as SelfTradeBehaviorType,
Side,
nameToString,
} from '../client';
import { Market } from './market';
import {
I64_MAX_BN,
PlaceOrderTypeUtils,
SelfTradeBehaviorUtils,
SideUtils,
U64_MAX_BN,
getAssociatedTokenAddress,
} from '../utils/utils';
import { BN } from '@coral-xyz/anchor';
import { OpenOrdersIndexer } from './openOrdersIndexer';
import { Order } from '../structs/order';
import { createAssociatedTokenAccountIdempotentInstruction } from '@solana/spl-token';
EventType,
Order,
OpenOrdersIndexer,
Market,
} from '..';

export interface OrderToPlace {
side: Side;
Expand Down Expand Up @@ -104,6 +103,55 @@ export class OpenOrders {
return this;
}

public getBalanceNative(): [BN, BN] {
const {
asksBaseLots,
bidsQuoteLots,
baseFreeNative,
quoteFreeNative,
lockedMakerFees,
} = this.account.position;
const { baseLotSize, quoteLotSize } = this.market.account;

// TODO count in lots to save compute
const base = asksBaseLots.mul(baseLotSize).iadd(baseFreeNative);
const quote = bidsQuoteLots
.mul(quoteLotSize)
.iadd(quoteFreeNative)
.iadd(lockedMakerFees);

if (this.market.eventHeap) {
for (const event of this.market.eventHeap.parsedEvents()) {
switch (event.eventType) {
case EventType.Fill: {
const { maker, quantity, price, takerSide } = event as FillEvent;
if (maker.equals(this.account.owner)) {
const baseNative = quantity.mul(baseLotSize);
const quoteNative = quantity.mul(price).imul(quoteLotSize);
const quoteFeesNative = this.market.makerFeeFloor(quoteNative);
if (takerSide === 1) {
// buy
base.iadd(baseNative);
quote.isub(quoteNative.iadd(quoteFeesNative));
} else {
// sell
base.isub(baseNative);
quote.iadd(quoteNative.isub(quoteFeesNative));
}
}
continue;
}
case EventType.Out: {
// out events don't change balances
continue;
}
}
}
}

return [base, quote];
}

public setDelegate(delegate: Keypair): this {
this.delegate = delegate;
return this;
Expand Down Expand Up @@ -290,6 +338,36 @@ export class OpenOrders {
debug += ` ${order.toPrettyString()}\n`;
}

if (this.market.eventHeap) {
debug += ` events:\n`;
for (const event of this.market.eventHeap.parsedEvents()) {
switch (event.eventType) {
case EventType.Fill: {
const { maker, quantity, price, takerSide } = event as FillEvent;
if (maker.equals(this.pubkey)) {
const fillBase = this.market.baseLotsToUi(quantity);
const fillPrice = this.market.priceLotsToUi(price);
debug += ` fill side=${
takerSide === 1 ? 'Bid' : 'Ask'
} qty=${fillBase} price=${fillPrice}\n`;
}
continue;
}
case EventType.Out: {
const { owner } = event as OutEvent;
if (owner.equals(this.pubkey))
debug += ` out ${JSON.stringify(event)}\n`;
continue;
}
}
}

debug += ` balance:\n`;
const [base, quote] = this.getBalanceNative();
debug += ` base: ${this.market.baseNativeToUi(base)}\n`;
debug += ` quote: ${this.market.quoteNativeToUi(quote)}\n`;
}

return debug;
}

Expand Down
9 changes: 5 additions & 4 deletions ts/client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,13 @@ export type OpenOrdersAccount = IdlAccounts<OpenbookV2>['openOrdersAccount'];
export type OpenOrdersIndexerAccount =
IdlAccounts<OpenbookV2>['openOrdersIndexer'];
export type EventHeapAccount = IdlAccounts<OpenbookV2>['eventHeap'];
export type BookSideAccount = IdlAccounts<OpenbookV2>['bookSide'];
export type LeafNode = IdlTypes<OpenbookV2>['LeafNode'];
export type InnerNode = IdlTypes<OpenbookV2>['InnerNode'];
export type AnyNode = IdlTypes<OpenbookV2>['AnyNode'];
export type AnyEvent = IdlTypes<OpenbookV2>['AnyEvent'];
export type FillEvent = IdlTypes<OpenbookV2>['FillEvent'];
export type OutEvent = IdlTypes<OpenbookV2>['OutEvent'];
export type BookSideAccount = IdlAccounts<OpenbookV2>['bookSide'];
export type AnyNode = IdlTypes<OpenbookV2>['AnyNode'];
export type InnerNode = IdlTypes<OpenbookV2>['InnerNode'];
export type LeafNode = IdlTypes<OpenbookV2>['LeafNode'];

export interface OpenBookClientOptions {
idsSource?: IdsSource;
Expand Down
3 changes: 3 additions & 0 deletions ts/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { IDL, type OpenbookV2 } from './openbook_v2';

export * from './client';
export * from './accounts/bookSide';
export * from './accounts/eventHeap';
export * from './accounts/market';
export * from './accounts/openOrders';
export * from './accounts/openOrdersIndexer';
export * from './market';
export * from './structs/order';
export * from './utils/utils';
export * from './utils/watcher';

Expand Down
3 changes: 2 additions & 1 deletion ts/client/src/test/market.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ async function testFindAllMarkets(): Promise<void> {
async function testDecodeMarket(): Promise<void> {
const client = initReadOnlyOpenbookClient();
const marketPk = new PublicKey(
'8wjNUxS1oQpu6YXnG85WBBJtUNH29p8cXdjP4nFqrJTo',
'BU3EaRVo9WN44muCBy3mwkCQ4uYQWiuqay1whEmeSXK3',
);
const market = await Market.load(client, marketPk);
await market.loadOrderBook();
await market.loadEventHeap();

console.log(market.toPrettyString());
}
Expand Down
1 change: 1 addition & 0 deletions ts/client/src/test/openOrders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ async function testLoadOOForMarket(): Promise<void> {
const [oo] = await Promise.all([
OpenOrders.loadNullableForMarketAndOwner(market),
market.loadOrderBook(),
market.loadEventHeap(),
]);
console.log(oo?.toPrettyString());
}
Expand Down
23 changes: 20 additions & 3 deletions ts/client/src/utils/watcher.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Connection } from '@solana/web3.js';
import { BookSide, Market, OpenOrders } from '..';
import { BookSide, EventHeap, Market, OpenOrders } from '..';

export class Watcher {
accountSubs: { [pk: string]: number } = {};

constructor(public connection: Connection) {}

addMarket(market: Market, includeBook = true): this {
const { client, asks, bids, pubkey } = market;
addMarket(market: Market, includeBook = true, includeEvents = true): this {
const { client, asks, bids, eventHeap, pubkey } = market;

this.accountSubs[pubkey.toBase58()] = this.connection.onAccountChange(
pubkey,
Expand All @@ -25,6 +25,9 @@ export class Watcher {
if (includeBook && bids) {
this.addBookSide(bids);
}
if (includeEvents && eventHeap) {
this.addEventHeap(eventHeap);
}
return this;
}

Expand All @@ -42,6 +45,20 @@ export class Watcher {
return this;
}

addEventHeap(eventHeap: EventHeap): this {
const { market, pubkey } = eventHeap;
this.accountSubs[pubkey.toBase58()] = this.connection.onAccountChange(
pubkey,
(ai) => {
eventHeap.account = market.client.program.coder.accounts.decode(
'eventHeap',
ai.data,
);
},
);
return this;
}

addOpenOrders(openOrders: OpenOrders): this {
const { market, pubkey } = openOrders;
this.accountSubs[pubkey.toBase58()] = this.connection.onAccountChange(
Expand Down

0 comments on commit 9fc3e83

Please sign in to comment.