UNPKG

betfair-node

Version:

A comprehensive Node.js TypeScript library for the Betfair Exchange API, providing both JSON-RPC API integration and real-time Exchange Stream API support for automated betting and trading applications.

715 lines 27.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.clearOrderCache = exports.getOrderCache = exports.resetStreamDecoder = exports.getMarketCache = exports.updateSubscribedMarkets = exports.updateCurrencyRate = exports.clearMarketCache = exports.processDataPacket = exports.createStreamDecoderState = void 0; const betfair_exchange_stream_api_types_1 = require("./betfair-exchange-stream-api-types"); const utils_1 = require("./utils"); /** * Creates initial stream decoder state */ const createStreamDecoderState = (currencyRate, subscribedMarkets = []) => ({ marketCache: {}, orderCache: {}, currencyRate, deltas: [], orderDeltas: [], subscribedMarkets, segmentBuffer: { marketSegments: {}, orderSegments: {}, }, }); exports.createStreamDecoderState = createStreamDecoderState; /** * Processes received data packet */ const processDataPacket = (state, callbacks, packet) => { const message = (0, utils_1.safeJsonParse)(packet); if (!message) { console.error('Failed to parse packet:', packet); return state; } return decodeMessage(state, callbacks, message); }; exports.processDataPacket = processDataPacket; /** * Decodes a message based on its operation type */ const decodeMessage = (state, callbacks, message) => { switch (message.op) { case 'connection': callbacks.onConnection(message); return state; case 'status': callbacks.onStatus(message); return state; case 'mcm': return handleSegmentedMarketMessage(state, callbacks, message); case 'ocm': return handleSegmentedOrderMessage(state, callbacks, message); default: console.warn('Unknown message type:', message.op); return state; } }; /** * Handles segmented market messages - only processes complete messages */ const handleSegmentedMarketMessage = (state, callbacks, message) => { const requestId = message.id?.toString() || 'default'; const segmentType = message.segmentationType; console.log(`🧩 Market message segment: requestId=${requestId}, segmentType=${segmentType || 'NONE'}, changeType=${message.ct}`); // Handle non-segmented messages immediately (only if no segments are buffered for this request) if (!segmentType && !state.segmentBuffer.marketSegments[requestId]) { console.log('📦 Processing complete (non-segmented) market message'); return decodeMarketChangeMessage(state, callbacks, message); } let updatedState = { ...state }; let updatedSegmentBuffer = { ...state.segmentBuffer }; // Handle segment start - clear any existing segments for this request if (segmentType === betfair_exchange_stream_api_types_1.SegmentType.SEG_START) { console.log('🟢 Starting new market message segment collection'); updatedSegmentBuffer.marketSegments[requestId] = [message]; updatedState.segmentBuffer = updatedSegmentBuffer; return updatedState; } // Handle segment end - reassemble and process complete message if (segmentType === betfair_exchange_stream_api_types_1.SegmentType.SEG_END) { console.log('🔴 Ending market message segment collection - reassembling'); const segments = updatedSegmentBuffer.marketSegments[requestId] || []; segments.push(message); // Reassemble complete message const completeMessage = reassembleMarketMessage(segments); // Clean up segment buffer delete updatedSegmentBuffer.marketSegments[requestId]; updatedState.segmentBuffer = updatedSegmentBuffer; console.log('✅ Processing reassembled complete market message'); return decodeMarketChangeMessage(updatedState, callbacks, completeMessage); } // Handle middle segments (including undefined segmentType when already collecting) - just collect them console.log('🟡 Collecting middle market message segment'); const existingSegments = updatedSegmentBuffer.marketSegments[requestId] || []; existingSegments.push(message); updatedSegmentBuffer.marketSegments[requestId] = existingSegments; updatedState.segmentBuffer = updatedSegmentBuffer; return updatedState; }; /** * Handles segmented order messages - only processes complete messages */ const handleSegmentedOrderMessage = (state, callbacks, message) => { const requestId = message.id?.toString() || 'default'; const segmentType = message.segmentationType; console.log(`🧩 Order message segment: requestId=${requestId}, segmentType=${segmentType || 'NONE'}, changeType=${message.ct}`); // Handle non-segmented messages immediately (only if no segments are buffered for this request) if (!segmentType && !state.segmentBuffer.orderSegments[requestId]) { console.log('📦 Processing complete (non-segmented) order message'); return decodeOrderChangeMessage(state, callbacks, message); } let updatedState = { ...state }; let updatedSegmentBuffer = { ...state.segmentBuffer }; // Handle segment start - clear any existing segments for this request if (segmentType === betfair_exchange_stream_api_types_1.SegmentType.SEG_START) { console.log('🟢 Starting new order message segment collection'); updatedSegmentBuffer.orderSegments[requestId] = [message]; updatedState.segmentBuffer = updatedSegmentBuffer; return updatedState; } // Handle segment end - reassemble and process complete message if (segmentType === betfair_exchange_stream_api_types_1.SegmentType.SEG_END) { console.log('🔴 Ending order message segment collection - reassembling'); const segments = updatedSegmentBuffer.orderSegments[requestId] || []; segments.push(message); // Reassemble complete message const completeMessage = reassembleOrderMessage(segments); // Clean up segment buffer delete updatedSegmentBuffer.orderSegments[requestId]; updatedState.segmentBuffer = updatedSegmentBuffer; console.log('✅ Processing reassembled complete order message'); return decodeOrderChangeMessage(updatedState, callbacks, completeMessage); } // Handle middle segments (including undefined segmentType when already collecting) - just collect them console.log('🟡 Collecting middle order message segment'); const existingSegments = updatedSegmentBuffer.orderSegments[requestId] || []; existingSegments.push(message); updatedSegmentBuffer.orderSegments[requestId] = existingSegments; updatedState.segmentBuffer = updatedSegmentBuffer; return updatedState; }; /** * Reassembles segmented market messages into a complete message */ const reassembleMarketMessage = (segments) => { if (segments.length === 0) { throw new Error('Cannot reassemble empty market message segments'); } const firstSegment = segments[0]; const lastSegment = segments[segments.length - 1]; // Merge all market changes from all segments const allMarketChanges = []; for (const segment of segments) { if (segment.mc) { allMarketChanges.push(...segment.mc); } } console.log(`🔧 Reassembled ${segments.length} market segments into ${allMarketChanges.length} market changes`); // Create complete message with merged data const completeMessage = { op: firstSegment.op, id: firstSegment.id, ct: firstSegment.ct, pt: lastSegment.pt || firstSegment.pt, status: lastSegment.status || firstSegment.status, con: lastSegment.con !== undefined ? lastSegment.con : firstSegment.con, segmentationType: lastSegment.segmentationType || firstSegment.segmentationType, segmentationEnabled: firstSegment.segmentationEnabled, conflateMs: firstSegment.conflateMs, heartbeatMs: firstSegment.heartbeatMs, initialClk: firstSegment.initialClk, clk: lastSegment.clk || firstSegment.clk, mc: allMarketChanges, }; return completeMessage; }; /** * Reassembles segmented order messages into a complete message */ const reassembleOrderMessage = (segments) => { if (segments.length === 0) { throw new Error('Cannot reassemble empty order message segments'); } const firstSegment = segments[0]; const lastSegment = segments[segments.length - 1]; // Merge all order changes from all segments const allOrderChanges = []; for (const segment of segments) { if (segment.oc) { allOrderChanges.push(...segment.oc); } } console.log(`🔧 Reassembled ${segments.length} order segments into ${allOrderChanges.length} order changes`); // Create complete message with merged data const completeMessage = { op: firstSegment.op, id: firstSegment.id, ct: firstSegment.ct, pt: lastSegment.pt || firstSegment.pt, status: lastSegment.status || firstSegment.status, con: lastSegment.con !== undefined ? lastSegment.con : firstSegment.con, segmentationType: lastSegment.segmentationType || firstSegment.segmentationType, segmentationEnabled: firstSegment.segmentationEnabled, conflateMs: firstSegment.conflateMs, heartbeatMs: firstSegment.heartbeatMs, initialClk: firstSegment.initialClk, clk: lastSegment.clk || firstSegment.clk, oc: allOrderChanges, }; return completeMessage; }; /** * Decodes market change messages */ const decodeMarketChangeMessage = (state, callbacks, message) => { if (message.ct === betfair_exchange_stream_api_types_1.ChangeType.HEARTBEAT) { callbacks.onHeartbeat(); return state; } const updatedState = processSubImage(state, message); callbacks.onMarketChange(updatedState.marketCache, updatedState.deltas); // Clear deltas after callback return { ...updatedState, deltas: [], }; }; /** * Decodes order change messages */ const decodeOrderChangeMessage = (state, callbacks, message) => { if (message.ct === betfair_exchange_stream_api_types_1.ChangeType.HEARTBEAT) { callbacks.onHeartbeat(); return state; } console.log('🔍 Raw order change message received:', { changeType: message.ct, segmentationType: message.segmentationType, publishTime: message.pt, connectionFlag: message.con, initialClk: message.initialClk, clk: message.clk, status: message.status, orderChangesCount: message.oc?.length || 0, orderChanges: message.oc?.map(oc => ({ marketId: oc.id, closed: oc.closed, fullImage: oc.fullImage, runnerChangesCount: oc.orc?.length || 0, runnerChanges: oc.orc?.map(orc => ({ runnerId: orc.id, fullImage: orc.fullImage, unmatchedOrdersCount: orc.uo?.length || 0, matchedBacksCount: orc.mb?.length || 0, matchedLaysCount: orc.ml?.length || 0 })) })) }); const updatedState = processOrderSubImage(state, message); if (callbacks.onOrderChange) { callbacks.onOrderChange(updatedState.orderCache, updatedState.orderDeltas); } // Clear order deltas after callback return { ...updatedState, orderDeltas: [], }; }; /** * Processes order subscription image data */ const processOrderSubImage = (state, message) => { if (!message.oc || message.oc.length === 0) { return state; } let updatedOrderCache = { ...state.orderCache }; const newOrderDeltas = [...state.orderDeltas]; for (const orderAccountChange of message.oc) { const result = processOrderAccountChange(updatedOrderCache, orderAccountChange); updatedOrderCache = result.cache; newOrderDeltas.push(...result.deltas); } return { ...state, orderCache: updatedOrderCache, orderDeltas: newOrderDeltas, }; }; /** * Processes subscription image data */ const processSubImage = (state, message) => { if (!message.mc || message.mc.length === 0) { return state; } let updatedCache = { ...state.marketCache }; const newDeltas = [...state.deltas]; for (const marketChange of message.mc) { const result = processMarketChange(updatedCache, marketChange, state.currencyRate); updatedCache = result.cache; newDeltas.push(...result.deltas); } return { ...state, marketCache: updatedCache, deltas: newDeltas, }; }; /** * Processes individual market changes */ const processMarketChange = (marketCache, marketChange, currencyRate) => { const marketId = marketChange.id; const deltas = []; // Initialize market cache if it doesn't exist if (!marketCache[marketId]) { marketCache[marketId] = createEmptyMarketCache(marketId); } let market = marketCache[marketId]; // Update market definition if present if (marketChange.marketDefinition) { market = { ...market, marketDefinition: marketChange.marketDefinition, }; deltas.push(`Market definition updated for ${marketId}`); // Extract runner status updates from market definition if (marketChange.marketDefinition.runners) { const statusUpdates = []; const updatedRunners = { ...market.runners }; for (const runnerDef of marketChange.marketDefinition.runners) { const selectionId = runnerDef.id.toString(); if (updatedRunners[selectionId] && runnerDef.status) { const oldStatus = updatedRunners[selectionId].status; updatedRunners[selectionId] = { ...updatedRunners[selectionId], status: runnerDef.status, }; if (oldStatus !== runnerDef.status) { statusUpdates.push(`Runner ${selectionId} status: ${oldStatus}${runnerDef.status}`); } } } if (statusUpdates.length > 0) { market = { ...market, runners: updatedRunners }; deltas.push(...statusUpdates); } } } // Process runner changes if (marketChange.rc) { const result = processRunnerChanges(market, marketChange.rc, currencyRate); market = result.market; deltas.push(...result.deltas); } // Update total matched and last value traded if (marketChange.tv !== undefined) { market = { ...market, totalMatched: marketChange.tv, }; } return { cache: { ...marketCache, [marketId]: market, }, deltas, }; }; /** * Processes runner changes within a market */ const processRunnerChanges = (market, runnerChanges, currencyRate) => { const deltas = []; const updatedRunners = { ...market.runners }; for (const runnerChange of runnerChanges) { const selectionId = runnerChange.id.toString(); if (!updatedRunners[selectionId]) { updatedRunners[selectionId] = createEmptyRunnerCache(runnerChange.id); } let runner = updatedRunners[selectionId]; // Update runner properties if (runnerChange.ltp !== undefined) { runner = { ...runner, ltp: runnerChange.ltp }; // Keep LTP in original currency (GBP) to match back/lay prices deltas.push(`LTP updated for runner ${selectionId}: ${runner.ltp}`); } if (runnerChange.tv !== undefined) { // Don't overwrite existing volume with zero during settlement if (runnerChange.tv > 0 || runner.tv === 0) { runner = { ...runner, tv: runnerChange.tv }; // console.log(`📊 Runner ${selectionId} TV updated to: ${runnerChange.tv}`); } else { // console.log(`🚫 Ignoring zero TV update for runner ${selectionId} (preserving ${runner.tv})`); } } if (runnerChange.batb) { runner = { ...runner, batb: runnerChange.batb }; deltas.push(`Back prices updated for runner ${selectionId}`); } if (runnerChange.batl) { runner = { ...runner, batl: runnerChange.batl }; deltas.push(`Lay prices updated for runner ${selectionId}`); } if (runnerChange.atb) { runner = { ...runner, atb: runnerChange.atb }; } if (runnerChange.atl) { runner = { ...runner, atl: runnerChange.atl }; } // BSP (Betfair Starting Price) fields if (runnerChange.spn !== undefined) { runner = { ...runner, spn: runnerChange.spn }; } if (runnerChange.spf !== undefined) { runner = { ...runner, spf: runnerChange.spf }; } if (runnerChange.spb) { runner = { ...runner, spb: runnerChange.spb }; deltas.push(`BSP back prices updated for runner ${selectionId}`); } if (runnerChange.spl) { runner = { ...runner, spl: runnerChange.spl }; deltas.push(`BSP lay prices updated for runner ${selectionId}`); } // Trading data if (runnerChange.trd) { const totalTraded = runnerChange.trd.reduce((sum, [price, volume]) => sum + volume, 0); // Don't overwrite existing trading data with empty/zero data during settlement if (totalTraded > 0 || !runner.trd || runner.trd.length === 0) { runner = { ...runner, trd: runnerChange.trd }; deltas.push(`Trading data updated for runner ${selectionId}: ${runnerChange.trd.length} trades, total volume: ${totalTraded}`); // console.log(`🔄 Trading data for runner ${selectionId}:`, runnerChange.trd, `total: ${totalTraded}`); } else { // console.log(`🚫 Ignoring zero trading data update for runner ${selectionId} (preserving existing data)`); } } updatedRunners[selectionId] = runner; } return { market: { ...market, runners: updatedRunners, }, deltas, }; }; /** * Processes individual order account changes */ const processOrderAccountChange = (orderCache, orderAccountChange) => { const marketId = orderAccountChange.id; const deltas = []; // Check for full image replacement if (orderAccountChange.fullImage) { if (!orderAccountChange.orc || orderAccountChange.orc.length === 0) { // Empty full image means remove this market from cache const updatedCache = { ...orderCache }; delete updatedCache[marketId]; deltas.push(`Order market ${marketId} removed from cache`); return { cache: updatedCache, deltas }; } } // Initialize order cache if it doesn't exist if (!orderCache[marketId]) { orderCache[marketId] = createEmptyOrderAccountCache(marketId); deltas.push(`Order market ${marketId} initialized`); } let market = orderCache[marketId]; // Update closed status if (orderAccountChange.closed !== undefined) { market = { ...market, closed: orderAccountChange.closed, }; deltas.push(`Market ${marketId} closed status: ${orderAccountChange.closed}`); } // Process runner changes if (orderAccountChange.orc) { const result = processOrderRunnerChanges(market, orderAccountChange.orc, orderAccountChange.fullImage || false); market = result.market; deltas.push(...result.deltas); } return { cache: { ...orderCache, [marketId]: market, }, deltas, }; }; /** * Processes order runner changes within a market */ const processOrderRunnerChanges = (market, runnerChanges, isFullImage) => { const deltas = []; let updatedRunners = { ...market.runners }; for (const runnerChange of runnerChanges) { const selectionId = runnerChange.id.toString(); // Handle runner full image replacement if (runnerChange.fullImage || isFullImage) { updatedRunners[selectionId] = createEmptyOrderRunnerCache(runnerChange.id, runnerChange.hc); deltas.push(`Order runner ${selectionId} full image reset`); } else if (!updatedRunners[selectionId]) { updatedRunners[selectionId] = createEmptyOrderRunnerCache(runnerChange.id, runnerChange.hc); deltas.push(`Order runner ${selectionId} initialized`); } let runner = updatedRunners[selectionId]; // Process unmatched orders (orders are sent in full on change) if (runnerChange.uo) { const orderUpdates = []; const updatedUnmatchedOrders = {}; for (const order of runnerChange.uo) { updatedUnmatchedOrders[order.id] = order; if (runner.unmatchedOrders[order.id]) { orderUpdates.push(`Order ${order.id} updated: ${order.status}, matched: ${order.sm}/${order.s}`); } else { orderUpdates.push(`Order ${order.id} added: ${order.side} ${order.s}@${order.p}, status: ${order.status}`); } } runner = { ...runner, unmatchedOrders: updatedUnmatchedOrders }; deltas.push(...orderUpdates); } // Process matched backs ladder (delta merge) if (runnerChange.mb) { const updatedMatchedBacks = mergeMatchedLadder(runner.matchedBacks, runnerChange.mb); runner = { ...runner, matchedBacks: updatedMatchedBacks }; deltas.push(`Matched backs updated for runner ${selectionId}: ${runnerChange.mb.length} levels`); } // Process matched lays ladder (delta merge) if (runnerChange.ml) { const updatedMatchedLays = mergeMatchedLadder(runner.matchedLays, runnerChange.ml); runner = { ...runner, matchedLays: updatedMatchedLays }; deltas.push(`Matched lays updated for runner ${selectionId}: ${runnerChange.ml.length} levels`); } // Process strategy match changes if (runnerChange.smc) { const updatedStrategyMatches = { ...runner.strategyMatches }; for (const [strategyRef, strategyChange] of Object.entries(runnerChange.smc)) { if (!updatedStrategyMatches[strategyRef]) { updatedStrategyMatches[strategyRef] = { mb: [], ml: [] }; } let strategyMatch = updatedStrategyMatches[strategyRef]; if (strategyChange.mb) { strategyMatch = { ...strategyMatch, mb: mergeMatchedLadder(strategyMatch.mb || [], strategyChange.mb) }; } if (strategyChange.ml) { strategyMatch = { ...strategyMatch, ml: mergeMatchedLadder(strategyMatch.ml || [], strategyChange.ml) }; } updatedStrategyMatches[strategyRef] = strategyMatch; deltas.push(`Strategy ${strategyRef} matches updated for runner ${selectionId}`); } runner = { ...runner, strategyMatches: updatedStrategyMatches }; } updatedRunners[selectionId] = runner; } return { market: { ...market, runners: updatedRunners, }, deltas, }; }; /** * Merges matched ladder data using delta format */ const mergeMatchedLadder = (currentLadder, updates) => { const ladderMap = new Map(); // Add current ladder to map for (const [price, size] of currentLadder) { ladderMap.set(price, size); } // Apply updates for (const [price, size] of updates) { if (size === 0) { // Remove price level ladderMap.delete(price); } else { // Update price level ladderMap.set(price, size); } } // Convert back to array and sort by price return Array.from(ladderMap.entries()).sort((a, b) => a[0] - b[0]); }; /** * Creates an empty order account cache entry */ const createEmptyOrderAccountCache = (marketId) => ({ marketId, closed: false, runners: {}, published: Date.now(), }); /** * Creates an empty order runner cache entry */ const createEmptyOrderRunnerCache = (id, hc) => ({ id, hc, unmatchedOrders: {}, matchedBacks: [], matchedLays: [], strategyMatches: {}, }); /** * Creates an empty market cache entry */ const createEmptyMarketCache = (marketId) => ({ marketId, marketDefinition: {}, runners: {}, totalMatched: 0, lastValueTraded: 0, published: Date.now(), }); /** * Creates an empty runner cache entry */ const createEmptyRunnerCache = (id) => ({ id, status: 'ACTIVE', adjustmentFactor: 0, lastPriceTraded: 0, totalMatched: 0, batb: [], batl: [], atb: [], atl: [], ltp: 0, tv: 0, // BSP fields spn: 0, spf: 0, spb: [], spl: [], // Trading data trd: [], fullImage: {}, }); /** * Converts price using currency rate */ const convertPrice = (price, currencyRate) => { return price * currencyRate.rate; }; /** * Clears the market cache */ const clearMarketCache = (state) => ({ ...state, marketCache: {}, }); exports.clearMarketCache = clearMarketCache; /** * Updates the currency rate */ const updateCurrencyRate = (state, currencyRate) => ({ ...state, currencyRate, }); exports.updateCurrencyRate = updateCurrencyRate; /** * Updates subscribed markets */ const updateSubscribedMarkets = (state, markets) => ({ ...state, subscribedMarkets: markets, }); exports.updateSubscribedMarkets = updateSubscribedMarkets; /** * Gets the current market cache */ const getMarketCache = (state) => { return state.marketCache; }; exports.getMarketCache = getMarketCache; /** * Resets the stream decoder state */ const resetStreamDecoder = (state) => ({ ...state, marketCache: {}, orderCache: {}, deltas: [], orderDeltas: [], segmentBuffer: { marketSegments: {}, orderSegments: {}, }, }); exports.resetStreamDecoder = resetStreamDecoder; /** * Gets the current order cache */ const getOrderCache = (state) => { return state.orderCache; }; exports.getOrderCache = getOrderCache; /** * Clears the order cache */ const clearOrderCache = (state) => ({ ...state, orderCache: {}, orderDeltas: [], segmentBuffer: { ...state.segmentBuffer, orderSegments: {}, // Clear order segments but keep market segments }, }); exports.clearOrderCache = clearOrderCache; //# sourceMappingURL=betfair-stream-decoder.js.map