| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566 |
- 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_bid };
- 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 tokenYes = marketData[side].outcomes['Yes']['id'];
- const askNo = +marketData[side].outcomes['No']['best_ask'];
- 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, token: tokenYes, slug };
- odds[iorKeyNo] = { v: iorNo, b: -feeNo, t: 1, ask: askNo, token: tokenNo, slug };
- });
- }
- else if (key === 'spreads') {
- Object.keys(marketData).forEach(handicap => {
- const ratio = +handicap;
- const askHome = +marketData[handicap].outcomes['Home']['best_ask'];
- const tokenHome = marketData[handicap].outcomes['Home']['id'];
- const askAway = +marketData[handicap].outcomes['Away']['best_ask'];
- 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, token: tokenHome, slug };
- odds[`ior_r${ratioAccept(-ratio)}c_${ratioString(ratio)}`] = { v: iorAway, b: -feeAway, t: 1, ask: askAway, token: tokenAway, slug };
- });
- }
- else if (key === 'totals') {
- Object.keys(marketData).forEach(handicap => {
- const ratio = +handicap;
- const askOver = +marketData[handicap].outcomes['Over']['best_ask'];
- const tokenOver = marketData[handicap].outcomes['Over']['id'];
- const askUnder = +marketData[handicap].outcomes['Under']['best_ask'];
- 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, token: tokenOver, slug };
- odds[`ior_ouh_${ratioString(ratio)}`] = { v: iorUnder, b: -feeUnder, t: 1, ask: askUnder, 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 tokenYes = marketData[side].outcomes['Yes']['id'];
- const askNo = +marketData[side].outcomes['No']['best_ask'];
- const bidNo = +marketData[side].outcomes['No']['best_bid'];
- // 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_ex: bidPriceYes, tick_size, /*token: tokenYes, slug */ };
- odds[iorKeyNo] = { v: iorNo, b: rebateNo, t: 1, ask: askNo, bid: bidNo, 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 tokenHome = marketData[handicap].outcomes['Home']['id'];
- const askAway = +marketData[handicap].outcomes['Away']['best_ask'];
- const bidAway = +marketData[handicap].outcomes['Away']['best_bid'];
- // 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_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_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 tokenOver = marketData[handicap].outcomes['Over']['id'];
- const askUnder = +marketData[handicap].outcomes['Under']['best_ask'];
- const bidUnder = +marketData[handicap].outcomes['Under']['best_bid'];
- // 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_ex: bidPriceOver, tick_size, /*token: tokenOver, slug */ };
- odds[`ior_ouh_${ratioString(ratio)}`] = { v: iorUnder, b: rebateUnder, t: 1, ask: askUnder, bid: bidUnder, 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, title, series, gameId, parentEventId, startTime } = 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,
- 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;
- }
|