Skip to content
Open
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
13 changes: 10 additions & 3 deletions core/src/exchanges/gemini-titan/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,18 @@ export class GeminiFetcher implements IExchangeFetcher<GeminiRawEvent, GeminiRaw
}

async fetchRawOrderBook(instrumentSymbol: string): Promise<GeminiRawOrderBook | undefined> {
const eventTicker = this.getEventTickerForSymbol(instrumentSymbol);
let eventTicker = this.getEventTickerForSymbol(instrumentSymbol);
if (!eventTicker) {
// The symbol index is populated as a side effect of fetchRawEvents/
// fetchRawMarkets. A freshly constructed fetcher (e.g. a new instance
// created per server request for a credentialed client) has an empty
// index, so lazily build it once before giving up.
await this.fetchRawEvents({});
eventTicker = this.getEventTickerForSymbol(instrumentSymbol);
}
if (!eventTicker) {
throw new Error(
`Cannot fetch order book: no event ticker found for ${instrumentSymbol}. ` +
'Call fetchMarkets first to build the symbol index.',
`Cannot fetch order book: no event ticker found for ${instrumentSymbol}.`,
);
}

Expand Down
64 changes: 64 additions & 0 deletions core/test/exchanges/gemini-titan-fetcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,67 @@ describe('GeminiFetcher authenticated orders', () => {
await expect(fetcher.cancelRawOrder(123)).resolves.toBe(rawOrder);
});
});

describe('GeminiFetcher order book symbol index', () => {
const eventsResponse = {
data: [
{
ticker: 'EVT-1',
contracts: [
{
instrumentSymbol: 'ABC-YES',
ticker: 'ABC-YES',
prices: { bestBid: '0.60', bestAsk: '0.62' },
},
],
},
],
pagination: { total: 1 },
};

const singleEventResponse = {
ticker: 'EVT-1',
contracts: [
{
instrumentSymbol: 'ABC-YES',
prices: { bestBid: '0.60', bestAsk: '0.62' },
},
],
};

function makeGetFetcher(getResponses: unknown[]) {
const get = jest.fn(async () => ({ data: getResponses.shift() }));
const ctx: FetcherContext = {
http: { get } as any,
callApi: jest.fn() as any,
getHeaders: jest.fn(() => ({})),
};

return { fetcher: new GeminiFetcher(ctx, 'https://api.gemini.test'), get };
}

it('lazily builds the symbol index when fetchRawOrderBook is called first', async () => {
// Regression for #2037: a freshly constructed fetcher has an empty
// symbolToEventTicker index. fetchRawOrderBook must populate it lazily
// instead of throwing when fetchMarkets/fetchEvents was not called first.
const { fetcher, get } = makeGetFetcher([eventsResponse, singleEventResponse]);

const book = await fetcher.fetchRawOrderBook('ABC-YES');

expect(book).toEqual({
bids: [{ price: '0.60', size: '0' }],
asks: [{ price: '0.62', size: '0' }],
timestamp: expect.any(Number),
});
// One GET to list events (build the index) + one GET for the single event.
expect(get).toHaveBeenCalledTimes(2);
});

it('still throws when the symbol is unknown even after building the index', async () => {
const { fetcher } = makeGetFetcher([eventsResponse]);

await expect(fetcher.fetchRawOrderBook('UNKNOWN-YES')).rejects.toThrow(
/no event ticker found for UNKNOWN-YES/,
);
});
});