UNPKG

@stoqey/ibkr

Version:

NodeJS Interactive Brokers wrapper & utilities using @stoqey/ib

291 lines 14.7 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MkdManager = void 0; var instrument_utils_1 = require("../utils/instrument.utils"); var chart_1 = require("../utils/chart"); var log_1 = require("../utils/log"); var time_utils_1 = require("../utils/time.utils"); var data_utils_1 = require("../utils/data.utils"); var mkd_utils_1 = require("../utils/mkd.utils"); var omit_1 = __importDefault(require("lodash/omit")); var isEmpty_1 = __importDefault(require("lodash/isEmpty")); var MKD_CLEAN_UP_INTERVAL = process.env.MKD_CLEAN_UP_INTERVAL ? parseInt(process.env.MKD_CLEAN_UP_INTERVAL) : 1000 * 60 * 30; // 30 minutes var MkdManager = /** @class */ (function () { function MkdManager() { var _this = this; this.logsNames = MkdManager.name; this.marketData = {}; this._cleanUp = {}; // symbol, last cleaned up date this.doCleanUp = function (symbol, data, buffer) { if ((0, isEmpty_1.default)(symbol) || (0, isEmpty_1.default)(data) || (0, isEmpty_1.default)(buffer)) { return; } var now = data.date; var lastCleanUp = _this._cleanUp[symbol]; if (lastCleanUp && now.getTime() - lastCleanUp.getTime() > MKD_CLEAN_UP_INTERVAL) { _this._cleanUp[symbol] = now; // MarketData = [dateOld, ..., dateNew] (chronological order) // slice items before CLEAN_UP_INTERVAL, // aggregate the sliced items by minute, // add them back to the marketData var lastCleanUpTimestamp = now.getTime() - MKD_CLEAN_UP_INTERVAL; var oldItemsIndex = _this.findIndex(symbol, lastCleanUpTimestamp, false); var oldItems = buffer.slice(0, oldItemsIndex); var lastNewItem = buffer.slice(oldItemsIndex); var aggregatedData = (0, mkd_utils_1.createAggregator)(oldItems, "1m", { vwap: 'sum', volume: 'sum', close: 'last', open: 'first', high: 'max', low: 'min' }); _this.marketData[symbol] = __spreadArray(__spreadArray([], aggregatedData, true), lastNewItem, true); } }; } MkdManager.prototype.getSymbolKey = function (instrument) { return (0, instrument_utils_1.getSymbolKey)(instrument); }; ; MkdManager.prototype.cacheBar = function (data) { var _a, _b, _c, _d, _e; var symbol = (0, instrument_utils_1.getSymbolKey)(data === null || data === void 0 ? void 0 : data.instrument); if ((0, isEmpty_1.default)(symbol) || (0, isEmpty_1.default)(data)) { return; } if (!this.marketData[symbol]) { this.marketData[symbol] = []; this._cleanUp[symbol] = data.date; } var buffer = this.marketData[symbol]; var ts = new Date(data.date).getTime(); var bar = __assign(__assign({}, (0, omit_1.default)(data, ['instrument'])), { timestamp: ts, date: data.date, open: (_a = data.open) !== null && _a !== void 0 ? _a : data.close, high: (_b = data.high) !== null && _b !== void 0 ? _b : data.close, low: (_c = data.low) !== null && _c !== void 0 ? _c : data.close, close: data.close, volume: (_d = data.volume) !== null && _d !== void 0 ? _d : 0, vwap: (_e = data.vwap) !== null && _e !== void 0 ? _e : data.close }); // Fast append if in order if (buffer.length === 0 || buffer[buffer.length - 1].timestamp <= ts) { buffer.push(bar); this.doCleanUp(symbol, data, buffer); return; } // Otherwise insert at correct index using binary search var idx = this.findIndex(symbol, ts); if (idx === -1) { buffer.push(bar); } else { buffer.splice(idx, 0, bar); } this.doCleanUp(symbol, data, buffer); }; MkdManager.prototype.findIndex = function (symbol, targetTimestamp, last) { if (last === void 0) { last = false; } var data = this.marketData[symbol]; if (!data || data.length === 0) return -1; var left = 0; var right = data.length - 1; var result = -1; while (left <= right) { var mid = Math.floor((left + right) / 2); if (last) { if (data[mid].timestamp <= targetTimestamp) { result = mid; left = mid + 1; } else { right = mid - 1; } } else { if (data[mid].timestamp >= targetTimestamp) { result = mid; right = mid - 1; } else { left = mid + 1; } } } // If no result found and we're looking for insertion point, return the length of the array // This means the target timestamp is older than all existing timestamps if (result === -1 && !last) { return data.length; } return result; }; /** * Get historical data for a given time range using binary search * * @param contract - The instrument to query * @param startDate - Start of the range * @param endDate - End of the range * @param interval - Optional interval for aggregation (not yet implemented) * @returns Array of market data within the range * * @example * const data = await manager.historicalData( * instrument, * new Date('2024-01-15T10:30:00Z'), * new Date('2024-01-15T10:35:00Z') * ); */ MkdManager.prototype.historicalData = function (contract, startDate, endDate, interval) { return __awaiter(this, void 0, void 0, function () { var symbol, data, startTimestamp, endTimestamp, startIdx, endIdx, slicedData, first, last; return __generator(this, function (_a) { symbol = this.getSymbolKey(contract); data = this.marketData[symbol]; if (!data || data.length === 0) return [2 /*return*/, []]; startTimestamp = startDate.getTime(); endTimestamp = endDate.getTime(); // Validate timestamps if (isNaN(startTimestamp) || isNaN(endTimestamp)) { (0, log_1.log)("".concat(this.logsNames, ".historicalData"), "Invalid date provided for ".concat(symbol)); return [2 /*return*/, []]; } // Validate date range if (startTimestamp > endTimestamp) { (0, log_1.log)("".concat(this.logsNames, ".historicalData"), "Start date is after end date for ".concat(symbol)); return [2 /*return*/, []]; } startIdx = this.findIndex(symbol, startTimestamp); endIdx = this.findIndex(symbol, endTimestamp, true); // If startIdx is -1, it means startDate is before all data points // Use first item (index 0) if (startIdx === -1) { startIdx = 0; } // If endIdx is -1, it means endDate is after all data points // Use last item if (endIdx === -1) { endIdx = data.length - 1; } // Final validation if (endIdx < startIdx) { return [2 /*return*/, []]; } slicedData = data.slice(startIdx, endIdx + 1); first = slicedData && slicedData[0]; last = (slicedData || [])[(slicedData === null || slicedData === void 0 ? void 0 : slicedData.length) - 1]; (0, chart_1.plotMkdCli)(slicedData); (0, log_1.log)("".concat(this.logsNames, ".historicalData"), "".concat(slicedData.length, " data points for ").concat(symbol, " from ").concat((0, time_utils_1.formatDateStr)(first.date), " to ").concat((0, time_utils_1.formatDateStr)(last.date), " @").concat((0, data_utils_1.formatDec)(first.close), " -> @").concat((0, data_utils_1.formatDec)(last.close))); return [2 /*return*/, slicedData]; }); }); }; ; /** * Get quote at or before specified timestamp (point-in-time lookup) * Returns the most recent data point at or before the requested time. * If no data exists before the requested time, returns the first available data point. * * @param contract - The instrument to query * @param date - The timestamp to query (defaults to current time) * @returns The market data at or before the specified time, or null if no data exists * * @example * // Get current quote * const quote = await manager.getQuote(instrument); * * @example * // Get historical quote at specific time * const quote = await manager.getQuote(instrument, new Date('2024-01-15T10:32:00Z')); */ MkdManager.prototype.getQuote = function (contract, date) { return __awaiter(this, void 0, void 0, function () { var symbol, timestamp, data, closest, firstItem, lastItem; var _a, _b; return __generator(this, function (_c) { symbol = this.getSymbolKey(contract); timestamp = (_a = date === null || date === void 0 ? void 0 : date.getTime()) !== null && _a !== void 0 ? _a : Date.now(); // Validate timestamp (check for NaN from invalid Date) if (isNaN(timestamp)) { (0, log_1.log)("".concat(this.logsNames, ".getQuote"), "Invalid date provided for ".concat(symbol)); return [2 /*return*/, null]; } data = this.marketData[symbol]; if (!data || data.length === 0) return [2 /*return*/, null]; closest = this.findIndex(symbol, timestamp, true); if (closest === -1) { firstItem = data[0]; lastItem = data[data.length - 1]; // If query timestamp is before first item, return first item if (firstItem && timestamp < firstItem.timestamp) { return [2 /*return*/, firstItem]; } // If query timestamp is at or after last item, return last item if (lastItem && timestamp >= lastItem.timestamp) { return [2 /*return*/, lastItem]; } // Fallback (should not reach here) return [2 /*return*/, lastItem !== null && lastItem !== void 0 ? lastItem : null]; } return [2 /*return*/, (_b = data[closest]) !== null && _b !== void 0 ? _b : null]; }); }); }; return MkdManager; }()); exports.MkdManager = MkdManager; //# sourceMappingURL=Mkd.js.map