import getDateInTimezone from "./getDateInTimezone.js"; const MAKER_FEE_RATE = 0.03; const MAKER_REBATE_RATE = 0.25; /** * 精确浮点数字 * @param {number} number * @param {number} x * @returns {number} */ const fixFloat = (number, x=3) => { return parseFloat(number.toFixed(x)); } /** * 清理对象中的undefined值 * @param {*} obj * @returns {Object} */ const cleanUndefined = (obj) => { return Object.fromEntries( Object.entries(obj).filter(([, v]) => v !== undefined) ); } /** * 解析赛事标题 * @param {*} eventTitle * @returns {Object} */ const parseTeamData = (eventTitle) => { let titleSpliter; if (eventTitle.includes(' vs. ')) { titleSpliter = ' vs. '; } else if (eventTitle.includes(' vs ')) { titleSpliter = ' vs '; } if (!titleSpliter) { return { teamHomeName: '', teamAwayName: '', }; } const teamData = eventTitle.replace(' - More Markets', '').split(titleSpliter); return { teamHomeName: teamData[0], teamAwayName: teamData[1], }; } /** * 解析盘口键值对 * 示例: * { * "Yes": 0.525, * "No": 0.475 * } * @param {*} outcomes * @param {*} outcomePrices * @returns {Object} */ const parseOutcomes = (outcomes, clobTokenIds, bestBid=0, bestAsk=0) => { if (!outcomes || !clobTokenIds) { return {}; } const keys = JSON.parse(outcomes); const ids = JSON.parse(clobTokenIds); return keys.reduce((obj, key, index) => { let best_ask; let best_bid; if (index === 0) { best_ask = (bestAsk).toFixed(3); best_bid = (bestBid).toFixed(3); } else if (index === 1) { best_ask = (1 - bestBid).toFixed(3); best_bid = (1 - bestAsk).toFixed(3); } obj[key] = { id: ids[index], best_ask, best_ask_size: 0, best_bid, best_bid_size: 0 }; return obj; }, {}); } /** * 解析让球方向 * 让球方向为正数时,返回'a',否则返回'' * @param {*} ratio * @returns {string} */ const ratioAccept = (ratio) => { if (ratio > 0) { return 'a'; } return ''; } /** * 解析盘口比率 * 比率取绝对值,并去掉小数点后的数字 * @param {*} ratio * @returns {string} */ const ratioString = (ratio) => { ratio = Math.abs(ratio); ratio = ratio.toString(); ratio = ratio.replace(/\./, ''); return ratio; } /** * 解析买单价格 * 当卖单最低价格与买单最高价格差值超过最小变动价位时,返回买单最高价格+最小变动价位 * 否则,返回买单最高价格 * @param {*} ask * @param {*} bid * @param {*} tickSize * @returns {number} */ const parseBidPrice = (ask, bid, tickSize) => { const askPrice = +ask; const bidPrice = +bid; const minTickSize = +tickSize; const bestBidPrice = fixFloat(askPrice - minTickSize); if (bestBidPrice > bidPrice) { return bestBidPrice; } return bidPrice; } /** * 解析吃单手续费比 * 固定金额吃卖单时,手续费比约等于 费率*(1-价格) * @param {*} price * @returns {number} */ const parseAskFee = (price) => { return fixFloat(100 * MAKER_FEE_RATE * (1 - price), 4); } /** * 解析挂单返佣比 * 固定金额挂买单时,返佣比约等于 费率*(1-价格)*返佣比例 * @param {*} price * @returns {number} */ const parseBidRebate = (price) => { return fixFloat(100 * MAKER_FEE_RATE * (1 - price) * MAKER_REBATE_RATE, 4); } /** * 解析盘口数据 * 使用卖单最优价格 * @param {*} markets * @returns {Object} */ export const parseOddsAsk = (markets) => { const odds = {}; Object.keys(markets).forEach(key => { const marketData = markets[key]; if (key === 'moneyline') { Object.keys(marketData).forEach(side => { const askYes = +marketData[side].outcomes['Yes']['best_ask']; const askSizeYes = +marketData[side].outcomes['Yes']['best_ask_size']; const maxYes = fixFloat(askYes * askSizeYes); // const tokenYes = marketData[side].outcomes['Yes']['id']; const askNo = +marketData[side].outcomes['No']['best_ask']; const askSizeNo = +marketData[side].outcomes['No']['best_ask_size']; const maxNo = fixFloat(askNo * askSizeNo); // const tokenNo = marketData[side].outcomes['No']['id']; // const slug = marketData[side].market.slug; if (askYes <= 0.1 || askNo <= 0.1) { return; } const iorYes = fixFloat(1 / askYes); const iorNo = fixFloat(1 / askNo); const feeYes = parseAskFee(askYes); const feeNo = parseAskFee(askNo); let iorKeyYes = ''; let iorKeyNo = ''; switch (side) { case 'Home': iorKeyYes = 'ior_mh'; iorKeyNo = 'ior_moh'; break; case 'Draw': iorKeyYes = 'ior_mn'; iorKeyNo = 'ior_mon'; break; case 'Away': iorKeyYes = 'ior_mc'; iorKeyNo = 'ior_moc'; break; } odds[iorKeyYes] = { v: iorYes, b: -feeYes, t: 1, ask: askYes, ask_size: askSizeYes, max: maxYes, /*token: tokenYes, slug */ }; odds[iorKeyNo] = { v: iorNo, b: -feeNo, t: 1, ask: askNo, ask_size: askSizeNo, max: maxNo, /*token: tokenNo, slug */ }; }); } else if (key === 'spreads') { Object.keys(marketData).forEach(handicap => { const ratio = +handicap; const askHome = +marketData[handicap].outcomes['Home']['best_ask']; const askSizeHome = +marketData[handicap].outcomes['Home']['best_ask_size']; const maxHome = fixFloat(askHome * askSizeHome); // const tokenHome = marketData[handicap].outcomes['Home']['id']; const askAway = +marketData[handicap].outcomes['Away']['best_ask']; const askSizeAway = +marketData[handicap].outcomes['Away']['best_ask_size']; const maxAway = fixFloat(askAway * askSizeAway); // const tokenAway = marketData[handicap].outcomes['Away']['id']; // const slug = marketData[handicap].market.slug; if (askHome <= 0.1 || askAway <= 0.1) { return; } const iorHome = fixFloat(1 / askHome); const iorAway = fixFloat(1 / askAway); const feeHome = parseAskFee(askHome); const feeAway = parseAskFee(askAway); odds[`ior_r${ratioAccept(ratio)}h_${ratioString(ratio)}`] = { v: iorHome, b: -feeHome, t: 1, ask: askHome, ask_size: askSizeHome, max: maxHome, /*token: tokenHome, slug */ }; odds[`ior_r${ratioAccept(-ratio)}c_${ratioString(ratio)}`] = { v: iorAway, b: -feeAway, t: 1, ask: askAway, ask_size: askSizeAway, max: maxAway, /*token: tokenAway, slug */ }; }); } else if (key === 'totals') { Object.keys(marketData).forEach(handicap => { const ratio = +handicap; const askOver = +marketData[handicap].outcomes['Over']['best_ask']; const askSizeOver = +marketData[handicap].outcomes['Over']['best_ask_size']; const maxOver = fixFloat(askOver * askSizeOver); // const tokenOver = marketData[handicap].outcomes['Over']['id']; const askUnder = +marketData[handicap].outcomes['Under']['best_ask']; const askSizeUnder = +marketData[handicap].outcomes['Under']['best_ask_size']; const maxUnder = fixFloat(askUnder * askSizeUnder); // const tokenUnder = marketData[handicap].outcomes['Under']['id']; const slug = marketData[handicap].market.slug; if (askOver <= 0.1 || askUnder <= 0.1) { return; } const iorOver = fixFloat(1 / askOver); const iorUnder = fixFloat(1 / askUnder); const feeOver = parseAskFee(askOver); const feeUnder = parseAskFee(askUnder); odds[`ior_ouc_${ratioString(ratio)}`] = { v: iorOver, b: -feeOver, t: 1, ask: askOver, ask_size: askSizeOver, max: maxOver, /*token: tokenOver, slug */ }; odds[`ior_ouh_${ratioString(ratio)}`] = { v: iorUnder, b: -feeUnder, t: 1, ask: askUnder, ask_size: askSizeUnder, max: maxUnder, /*token: tokenUnder, slug */ }; }); } }); return odds; } /** * 解析盘口数据 * 当卖单最低价格与买单最高价格差值超过最小变动价位时 * 创建新的买单价格,即买单最高价格+最小变动价位 * 否则,使用买单最高价格 * @param {*} markets * @returns {Object} */ export const parseOddsBid = (markets) => { const odds = {}; Object.keys(markets).forEach(key => { const marketData = markets[key]; if (key === 'moneyline') { Object.keys(marketData).forEach(side => { const askYes = +marketData[side].outcomes['Yes']['best_ask']; const bidYes = +marketData[side].outcomes['Yes']['best_bid']; const bidSizeYes = +marketData[side].outcomes['Yes']['best_bid_size']; // const tokenYes = marketData[side].outcomes['Yes']['id']; const askNo = +marketData[side].outcomes['No']['best_ask']; const bidNo = +marketData[side].outcomes['No']['best_bid']; const bidSizeNo = +marketData[side].outcomes['No']['best_bid_size']; // const tokenNo = marketData[side].outcomes['No']['id']; // const slug = marketData[side].market.slug; const tick_size = marketData[side].market.orderPriceMinTickSize; if (askYes <= 0.1 || askNo <= 0.1) { return; } const bidPriceYes = parseBidPrice(askYes, bidYes, tick_size); const bidPriceNo = parseBidPrice(askNo, bidNo, tick_size); const iorYes = fixFloat(1 / bidPriceYes); const iorNo = fixFloat(1 / bidPriceNo); const rebateYes = parseBidRebate(bidPriceYes); const rebateNo = parseBidRebate(bidPriceNo); let iorKeyYes = ''; let iorKeyNo = ''; switch (side) { case 'Home': iorKeyYes = 'ior_mh'; iorKeyNo = 'ior_moh'; break; case 'Draw': iorKeyYes = 'ior_mn'; iorKeyNo = 'ior_mon'; break; case 'Away': iorKeyYes = 'ior_mc'; iorKeyNo = 'ior_moc'; break; } odds[iorKeyYes] = { v: iorYes, b: rebateYes, t: 1, ask: askYes, bid: bidYes, bid_size: bidSizeYes, bid_ex: bidPriceYes, tick_size, /*token: tokenYes, slug */ }; odds[iorKeyNo] = { v: iorNo, b: rebateNo, t: 1, ask: askNo, bid: bidNo, bid_size: bidSizeNo, bid_ex: bidPriceNo, tick_size, /*token: tokenNo, slug */ }; }); } else if (key === 'spreads') { Object.keys(marketData).forEach(handicap => { const ratio = +handicap; const askHome = +marketData[handicap].outcomes['Home']['best_ask']; const bidHome = +marketData[handicap].outcomes['Home']['best_bid']; const bidSizeHome = +marketData[handicap].outcomes['Home']['best_bid_size']; // const tokenHome = marketData[handicap].outcomes['Home']['id']; const askAway = +marketData[handicap].outcomes['Away']['best_ask']; const bidAway = +marketData[handicap].outcomes['Away']['best_bid']; const bidSizeAway = +marketData[handicap].outcomes['Away']['best_bid_size']; // const tokenAway = marketData[handicap].outcomes['Away']['id']; // const slug = marketData[handicap].market.slug; const tick_size = marketData[handicap].market.orderPriceMinTickSize; if (askHome <= 0.1 || askAway <= 0.1) { return; } const bidPriceHome = parseBidPrice(askHome, bidHome, tick_size); const bidPriceAway = parseBidPrice(askAway, bidAway, tick_size); const iorHome = fixFloat(1 / bidPriceHome); const iorAway = fixFloat(1 / bidPriceAway); const rebateHome = parseBidRebate(bidPriceHome); const rebateAway = parseBidRebate(bidPriceAway); odds[`ior_r${ratioAccept(ratio)}h_${ratioString(ratio)}`] = { v: iorHome, b: rebateHome, t: 1, ask: askHome, bid: bidHome, bid_size: bidSizeHome, bid_ex: bidPriceHome, tick_size, /*token: tokenHome, slug */ }; odds[`ior_r${ratioAccept(-ratio)}c_${ratioString(ratio)}`] = { v: iorAway, b: rebateAway, t: 1, ask: askAway, bid: bidAway, bid_size: bidSizeAway, bid_ex: bidPriceAway, tick_size, /*token: tokenAway, slug */ }; }); } else if (key === 'totals') { Object.keys(marketData).forEach(handicap => { const ratio = +handicap; const askOver = +marketData[handicap].outcomes['Over']['best_ask']; const bidOver = +marketData[handicap].outcomes['Over']['best_bid']; const bidSizeOver = +marketData[handicap].outcomes['Over']['best_bid_size']; // const tokenOver = marketData[handicap].outcomes['Over']['id']; const askUnder = +marketData[handicap].outcomes['Under']['best_ask']; const bidUnder = +marketData[handicap].outcomes['Under']['best_bid']; const bidSizeUnder = +marketData[handicap].outcomes['Under']['best_bid_size']; // const tokenUnder = marketData[handicap].outcomes['Under']['id']; // const slug = marketData[handicap].market.slug; const tick_size = marketData[handicap].market.orderPriceMinTickSize; if (askOver <= 0.1 || askUnder <= 0.1) { return; } const bidPriceOver = parseBidPrice(askOver, bidOver, tick_size); const bidPriceUnder = parseBidPrice(askUnder, bidUnder, tick_size); const iorOver = fixFloat(1 / bidPriceOver); const iorUnder = fixFloat(1 / bidPriceUnder); const rebateOver = parseBidRebate(bidPriceOver); const rebateUnder = parseBidRebate(bidPriceUnder); odds[`ior_ouc_${ratioString(ratio)}`] = { v: iorOver, b: rebateOver, t: 1, ask: askOver, bid: bidOver, bid_size: bidSizeOver, bid_ex: bidPriceOver, tick_size, /*token: tokenOver, slug */ }; odds[`ior_ouh_${ratioString(ratio)}`] = { v: iorUnder, b: rebateUnder, t: 1, ask: askUnder, bid: bidUnder, bid_size: bidSizeUnder, bid_ex: bidPriceUnder, tick_size, /*token: tokenUnder, slug */ }; }); } }); return odds; } /** * 解析胜平负盘口 * @param {*} groupItemThreshold * @param {*} outcomesMap * @param {*} market * @returns {Object} */ const parseMoneyline = (groupItemTitle, outcomesMap, teams, market) => { const { teamHomeName, teamAwayName } = teams; let key = ''; if (groupItemTitle == teamHomeName) { key = 'Home'; } else if (groupItemTitle == teamAwayName) { key = 'Away'; } else if (groupItemTitle.startsWith('Draw')) { key = 'Draw'; } if (!key) { return {}; } return { [key]: { outcomes: outcomesMap, market } }; }; /** * 解析让球盘口 * @param {*} groupItemTitle * @param {*} spreadsLine * @param {*} outcomesMap * @param {*} teams * @param {*} market * @returns {Object} */ const parseSpreads = (groupItemTitle, spreadsLine, outcomesMap, teams, market) => { const { teamHomeName, teamAwayName } = teams; let spreadDirection = 0; if (groupItemTitle.includes(teamHomeName)) { spreadDirection = 1; } else if (groupItemTitle.includes(teamAwayName)) { spreadDirection = -1; } const spreads = spreadsLine * spreadDirection; const key = spreads > 0 ? `+${spreads}` : `${spreads}`; const spreadsMap = {}; Object.keys(outcomesMap).forEach(key => { if (key == teamHomeName) { spreadsMap['Home'] = outcomesMap[key]; } else if (key == teamAwayName) { spreadsMap['Away'] = outcomesMap[key]; } }); return { [key]: { outcomes: spreadsMap, market } }; }; /** * 解析大小盘口 * @param {*} line * @param {*} outcomesMap * @param {*} market * @returns {Object} */ const parseTotals = (line, outcomesMap, market) => { return { [line]: { outcomes: outcomesMap, market } }; }; /** * 解析市场数据 * @param {*} markets * @param {*} teams * @returns {Object} */ const parseMarketsData = (markets, teams) => { const marketsData = {}; markets.forEach(market => { const { sportsMarketType, groupItemTitle, line, outcomes, clobTokenIds, bestBid=0, bestAsk=0 } = market; const outcomesMap = parseOutcomes(outcomes, clobTokenIds, bestBid, bestAsk); if (sportsMarketType === "moneyline") { if (!marketsData.moneyline) { marketsData.moneyline = {}; } Object.assign(marketsData.moneyline, parseMoneyline(groupItemTitle, outcomesMap, teams, market)) } else if (sportsMarketType === "spreads") { if (!marketsData.spreads) { marketsData.spreads = {}; } Object.assign(marketsData.spreads, parseSpreads(groupItemTitle, line, outcomesMap, teams, market)) } else if (sportsMarketType === "totals") { if (!marketsData.totals) { marketsData.totals = {}; } Object.assign(marketsData.totals, parseTotals(line, outcomesMap, market)); } }); return marketsData; } /** * 解析赛事数据 * @param {*} event * @returns {Object} */ const parseEvent = (event) => { const { id, slug, title, series, gameId, parentEventId, startTime, sport: { sport } = {}} = event; const { teamHomeName, teamAwayName } = parseTeamData(title); const leagueId = series[0].id; const leagueName = series[0].title; const timestamp = new Date(startTime).getTime(); return { id: +id, sport, slug, gameId: gameId ? +gameId : undefined, parentEventId: parentEventId ? +parentEventId : undefined, leagueId: +leagueId, teamHomeName, teamAwayName, leagueName, timestamp, startTime: getDateInTimezone('+8', timestamp, true), }; } /** * 解析市场列表数据 * @param {*} events * @returns {Object} */ export const parseMarkets = (eventsData) => { const mergedMarketsData = {}; Object.values(eventsData).map(event => { const item = parseEvent(event); const { markets } = event; const { teamHomeName, teamAwayName } = item; const marketsData = parseMarketsData(markets, { teamHomeName, teamAwayName }); return { ...item, marketsData }; }).sort((a, b) => a.id - b.id).forEach(item => { if (item.id && !item.parentEventId) { mergedMarketsData[item.id] = cleanUndefined(item); } else if (item.parentEventId && mergedMarketsData[item.parentEventId] && item.marketsData) { const { marketsData: parentEvents } = mergedMarketsData[item.parentEventId]; Object.assign(parentEvents, item.marketsData); } }); return mergedMarketsData; } /* * 解析比率信息 */ const parseRatio = (ratioString) => { if (!ratioString) { return null; } return parseFloat(`${ratioString[0]}.${ratioString.slice(1)}`); } /** * 解析盘口信息 * @param {*} ior * @returns */ const parseIor = (ior) => { const iorMatch = ior.match(/ior_(m|r|ou|wm|ot)([ao])?([hcn])?_?(\d+)?/); if (!iorMatch) { return null; } const [, type, action, side, ratio] = iorMatch; return { type, action, side, ratio }; } /** * 解析盘口详情 * @param {*} ior * @param {*} id * @param {*} marketsMap * @returns */ export const parseIorDetail = (ior, id, marketsMap) => { const marketsData = marketsMap[id]?.marketsData; if (!marketsData) { return { ior, id, message: 'markets data not found', cause: 400 }; } const iorOptions = parseIor(ior); if (!iorOptions) { return { ior, id, message: 'ior options not found', cause: 400 }; } const { type, action, side, ratio } = iorOptions; let marketTypeData, outcomesSide; if (type === 'm' && !ratio) { const sideKey = side === 'h' ? 'Home' : side === 'c' ? 'Away' : 'Draw'; const sideAction = action === 'o' ? 'No' : 'Yes'; marketTypeData = marketsData.moneyline[sideKey]; outcomesSide = sideAction; } else if (type === 'r') { const sideKey = side === 'h' ? 'Home' : side === 'c' ? 'Away' : ''; let ratioDirection = 1; if (side === 'c' && action === 'a' || side === 'h' && !action) { ratioDirection = -1; } const ratioValue = parseRatio(ratio) * ratioDirection; const ratioKey = ratioValue > 0 ? `+${ratioValue}` : `${ratioValue}`; marketTypeData = marketsData.spreads?.[ratioKey]; outcomesSide = sideKey; } else if (type === 'ou') { const sideKey = side === 'c' ? 'Over' : side === 'h' ? 'Under' : ''; const ratioKey = parseRatio(ratio); marketTypeData = marketsData.totals[ratioKey]; outcomesSide = sideKey; } const result = marketTypeData?.outcomes?.[outcomesSide]; if (!result) { Logs.outDev('polymarket market type data not found', { ior, id, type, action, side, ratio, marketTypeData, outcomesSide }); return { ior, id, message: 'market type data not found', cause: 400 }; } return result; }