stoqey/ibkr

What's a good way to get price data (high, low, open, close, etc)?

ellis opened this issue · 4 comments

ellis commented

I want to get the high, low, open, close, etc prices for my portfolio. I didn't see a good way to do this with @stoqey/ibkr, so I wrote the following self-contained prototype with @stoqey/ib to figure out how thinks work on a lower level. It grabs the price data once for AAPL and prints it to the screen. I'd like to do something similar with ibkr, but the current implementation of IBKREVENTS.SUBSCRIBE_PRICE_UPDATES only seems to emit events for a single element of the price. Can you give me a tip for how to use ibrk and tie into it's low-level interface when necessary?

import IB from "@stoqey/ib";

type Exchange =
	| "SMART"
	/** Forex cash exchange */
	| "IDEALPRO"
	/** Briefing Trader News */
	| "BRF";

type PrimaryExchange = "IDEALPRO" | "NASDAQ" | "NYSE";

type SecType = "CASH" | "FUT" | "NEWS" | "OPT" | "STK";

interface MktData {
	/** The last available closing price for the previous day. For US Equities, we use corporate action processing to get the closing price, so the close price is adjusted to reflect forward and reverse splits and cash and stock dividends. (9) */
	readonly closePrice?: number;
	/** Delayed bid price. (66) */
	readonly delayedBid?: number;
	/** Delayed ask price. (67) */
	readonly delayedAsk?: number;
	/** Delayed last traded price. (68) */
	readonly delayedLast?: number;
	/** Delayed highest price of the day. (72) */
	readonly delayedHighPrice?: number;
	/** Delayed lowest price of the day. (73) */
	readonly delayedLowPrice?: number;
	/** Delayed traded volume of the day. (74) */
	readonly delayedVolume?: number;
	/** The prior day's closing price. (75) */
	readonly delayedClose?: number;
}

type ResultHandlerCallbackFunction = (requestId: number, args: readonly any[]) => void;

interface ResultHandlerCallbackMap {
	[event: string]: {
		[requestId: string]: ResultHandlerCallbackFunction;
	};
}

let globalRequestIdNext = 10000;

const resultHandlerCallbacks: ResultHandlerCallbackMap = {};

async function run(): Promise<void> {
	const ib = new IB({
		// clientId: 0,
		// host: '127.0.0.1',
		// port: 7496
	})
		.on("connected", function () {
			console.log("CONNECTED");
		})
		.on("disconnected", function () {
			console.log("DISCONNECTED");
		})
		.on("received", function (tokens: any) {
			console.info("%s %s", "<<< RECV <<<", JSON.stringify(tokens));
		})
		.on("sent", function (tokens: any) {
			console.info("%s %s", ">>> SENT >>>", JSON.stringify(tokens));
		})
		.on("server", function (version: any, connectionTime: any) {
			console.log(`Server Version: ${version}`);
			console.log(`Server Connection Time: ${connectionTime}`);
		})
		.on("error", function (err: any) {
			console.error(`@@@ ERROR: ${err.message} @@@`);
		})
		.on("result", function (event: string, args: readonly any[]) {
			console.log(`======= ${event} =======`);
			args.forEach(function (arg: any, i: number) {
				console.log("%s %s", `[${i + 1}]`, JSON.stringify(arg));
			});

			if (typeof args[0] === "number") {
				const requestId = args[0];
				const [, ...rest] = args;
				const callback = resultHandlerCallbacks[event]?.[requestId.toString()];
				if (callback) {
					callback(requestId, rest);
				}
			}
		});

	ib.connect();
	ib.reqMarketDataType(4); // marketDataType (1=live, 2=frozen, 3=delayed, 4=delayed/frozen)

	const result = await getMktData(ib, {
		exchange: "SMART",
		primaryExch: "NASDAQ",
		secType: "STK",
		symbol: "AAPL",
		currency: "USD",
	});
	console.log("result:", result);
}

/**
 * Requests market data (price, bid, ask)
 */
function getMktData(
	ib: IB,
	args: {
		readonly exchange: Exchange;
		readonly primaryExch: PrimaryExchange;
		readonly secType: SecType;
		readonly symbol: string;
		readonly currency?: string;
	}
): Promise<MktData> {
	const requestId = globalRequestIdNext++;
	return new Promise((res) => {
		// see https://interactivebrokers.github.io/tws-api/tick_types.html
		const tickPriceData: { [tickType: string]: number } = {};
		const expecting = new Set<number>(
			args.secType === "STK"
				? [66, 67, 68, 72, 73, 74, 75, 88]
				: args.secType === "CASH"
				? [9]
				: [-1]
		);

		let isDone = false;
		const done = async () => {
			if (!isDone) {
				isDone = true;
				unsetResultHandlerCallback("tickPrice", requestId);
				unsetResultHandlerCallback("tickSize", requestId);
				unsetResultHandlerCallback("tickString", requestId);
				ib.cancelMktData(requestId);
				res({
					closePrice: tickPriceData["9"],
					delayedBid: tickPriceData["66"],
					delayedAsk: tickPriceData["67"],
					delayedLast: tickPriceData["68"],
					delayedHighPrice: tickPriceData["72"],
					delayedLowPrice: tickPriceData["73"],
					delayedVolume: tickPriceData["74"],
					delayedClose: tickPriceData["75"],
				});
			}
		};

		const addNumber = async (tickType: number, value: number) => {
			tickPriceData[tickType.toString()] = value;
			expecting.delete(tickType);
			if (expecting.size === 0) {
				await done();
			}
		};

		const addString = async (tickType: number, value: string) => {
			// tickPriceData[tickType.toString()] = value;
			expecting.delete(tickType);
			if (expecting.size === 0) {
				await done();
			}
		};

		async function callback_tickPrice(
			requestId: number,
			[tickType, price, attrib]: [number, number, boolean]
		): Promise<void> {
			console.log("tickPrice:", { requestId, tickType, price, attrib });
			await addNumber(tickType, price);
		}
		async function callback_tickSize(
			requestId: number,
			[tickType, size]: [number, number]
		): Promise<void> {
			console.log("tickSize:", { requestId, tickType, size });
			await addNumber(tickType, size);
		}
		async function callback_tickString(
			requestId: number,
			[tickType, value]: [number, string]
		): Promise<void> {
			console.log("tickString:", { requestId, tickType, value });
			await addString(tickType, value);
			// console.log("DONE: ", requestId, tickPriceData);
		}

		setResultHandlerCallback("tickPrice", requestId, callback_tickPrice);
		setResultHandlerCallback("tickSize", requestId, callback_tickSize);
		setResultHandlerCallback("tickString", requestId, callback_tickString);

		ib.reqMktData(
			requestId,
			{
				currency: args.currency,
				exchange: args.exchange,
				primaryExch: args.primaryExch,
				secType: args.secType,
				symbol: args.symbol,
			},
			"",
			false,
			false
		); // requestId, contract, genericTickList, snapshot, snapshot2

		// Force the end in 10 seconds
		setTimeout(done, 10 * 1000);
	});
}

function setResultHandlerCallback(
	event: string,
	requestId: number,
	callback: ResultHandlerCallbackFunction
): void {
	if (!(event in resultHandlerCallbacks)) {
		resultHandlerCallbacks[event] = {};
	}
	resultHandlerCallbacks[event][requestId] = callback;
}

function unsetResultHandlerCallback(event: string, requestId: number): void {
	if (resultHandlerCallbacks[event]?.[requestId]) {
		delete resultHandlerCallbacks[event][requestId];
	}
}

run();
ellis commented

Looking some more through your code, I'm wondering if the type of tickType could be changed to TickPrice | TickPrice[] in this interface:

interface SymbolWithTicker {
    tickerId: number;
    symbol: string;
    tickType?: TickPrice;
}

And then in prices.updates.ts instead of:

                // Matches as requested
                if (currentTickerType === tickTypeWords) {

Maybe something like:

                // Matches as requested
                if ((typeof currentTickerType === "string" && currentTickerType === tickTypeWords) || currenTickerType.includes(tickTypeWords)) {

Since I don't know your software architecture, I don't know whether that would actually fit though?

Also, I'm using delayed price data. Can you point me to where I should look for adding support for that to ibkr, if you're open to PRs?

@ellis Thank you for pointing that out, you're welcome to push any PRs, let's start with this one checkout #57 please feel free to push and improve it 🙏

ellis commented

@ceddybi Cool, will aim to test it in the next couple days.

@ellis no problem take your time, please accept the invitation I sent you to the team org so that you can contribute and add PRs