import path from "path"; import { fileURLToPath } from "url"; import { getData } from "../libs/cache.js"; import Logs from "../libs/logs.js"; import eventSolutions from '../triangle/eventSolutions.js'; import { getOrderBook, placeOrder as polymarketPlaceOrder } from "../../polymarket/libs/polymarketClient.js"; import { getLineInfo, placeOrder as pinnaclePlaceOrder } from "../../pinnacle/libs/pinnacleClient.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const polymarketMarketsCacheFile = path.join(__dirname, "../../polymarket/cache/polymarketMarketsCache.json"); const pinnacleGamesCacheFile = path.join(__dirname, "../../pinnacle/cache/pinnacleGamesCache.json"); /** * USDC汇率 */ const USDC_RATE = 6.9; /** * 最小盈利率 */ const MIN_PROFIT_RATE = -1; /** * 精确浮点数字 * @param {number} number * @param {number} x * @returns {number} */ const fixFloat = (number, x=3) => { return parseFloat(number.toFixed(x)); } /** * 依次执行任务 */ const runSequentially = async (tasks) => { return new Promise(async (resolve, reject) => { const results = []; let isError = false; for (const task of tasks) { // task 必须是一个「返回 Promise 的函数」 const res = await task().catch(err => { err.results = results; isError = true; reject(err); }); if (isError) { break; } else if (res) { results.push(res); } } if (isError) { return; } resolve(results); }) } /** * 计算符合比例的最大和最小数组 */ const findMaxMinGroup = (ratios, minVals, maxVals) => { const n = ratios.length; // 1. 计算比例总和 & 归一化比例 const totalRatio = ratios.reduce((a, b) => a + b, 0); const proportions = ratios.map(r => r / totalRatio); // 2. 计算每个位置允许的最大/最小倍数 let maxPossibleScale = Infinity; let minPossibleScale = 0; // 如果允许0,通常从0开始 for (let i = 0; i < n; i++) { // 上限约束 if (proportions[i] > 0) { maxPossibleScale = Math.min(maxPossibleScale, maxVals[i] / proportions[i]); } // 下限约束(如果 minVals[i] > 0 才有意义) if (proportions[i] > 0 && minVals[i] > 0) { minPossibleScale = Math.max(minPossibleScale, minVals[i] / proportions[i]); } } // 3. 最终取值 const maxGroup = proportions.map(p => p * maxPossibleScale); const minGroup = proportions.map(p => p * Math.max(minPossibleScale, 0)); return { maxGroup: maxGroup.map(v => fixFloat(v)), // 可控制精度 minGroup: minGroup.map(v => fixFloat(v)), proportions: proportions.map(v => fixFloat(v)), scaleForMax: fixFloat(maxPossibleScale), scaleForMin: fixFloat(minPossibleScale) }; } /** * 解析盘口信息 */ const parseRatio = (ratioString) => { if (!ratioString) { return null; } return parseFloat(`${ratioString[0]}.${ratioString.slice(1)}`); } 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 }; } const getPolymarketIorInfo = async (ior, id) => { const cacheData = await getData(polymarketMarketsCacheFile); const marketsData = cacheData[id]?.marketsData; if (!marketsData) { Logs.outDev('polymarket markets data not found', id); return null; } const iorOptions = parseIor(ior); if (!iorOptions) { Logs.outDev('polymarket ior options not found', ior); return null; } const { type, action, side, ratio } = iorOptions; let marketTypeData, outcomesSide; if (type === 'm') { 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 null; } return result; } const getPinnacleIorInfo = async (ior, id) => { const cacheData = await getData(pinnacleGamesCacheFile); const gamesData = cacheData[id]; if (!gamesData) { Logs.outDev('pinnacle games data not found', id); return null; } const iorOptions = parseIor(ior); if (!iorOptions) { Logs.outDev('pinnacle ior options not found', ior); return null; } const { type, action, side, ratio } = iorOptions; const { leagueId, id: eventId, home: homeTeamName, away: awayTeamName, periods={}, specials={}} = gamesData; const straightData = periods.straight ?? {}; const { lineId: straightLineId, moneyline, spreads, totals } = straightData; const { winningMargin, exactTotalGoals } = specials; if (type === 'm' && moneyline) { const sideKey = side === 'h' ? 'home' : side === 'c' ? 'away' : 'draw'; const team = side === 'h' ? 'TEAM1' : side === 'c' ? 'TEAM2' : 'DRAW'; const odds = moneyline[sideKey]; return { leagueId, eventId, betType: 'MONEYLINE', team, lineId: straightLineId, odds }; } else if (type === 'r' && spreads) { let ratioDirection = 1; if (side === 'c' && action === 'a' || side === 'h' && !action) { ratioDirection = -1; } const ratioKey = parseRatio(ratio) * ratioDirection; const itemSpread = spreads.find(spread => spread.hdp == ratioKey); if (!itemSpread) { Logs.outDev('pinnacle item spread not found', id, type, action, side, ratio); return null; } const { altLineId=null, home, away } = itemSpread; const odds = side === 'h' ? home : away; const team = side === 'h' ? 'TEAM1' : 'TEAM2'; const handicap = ratioKey * (side === 'h' ? 1 : -1); return { leagueId, eventId, handicap, betType: 'SPREAD', team, lineId: straightLineId, altLineId, odds }; } else if (type === 'ou' && totals) { const ratioKey = parseRatio(ratio); const itemTotal = totals.find(total => total.points == ratioKey); if (!itemTotal) { Logs.outDev('pinnacle item total not found', id, type, action, side, ratio); return null; } const { altLineId=null, over, under } = itemTotal; const odds = side === 'c' ? over : under; const sideKey = side === 'c' ? 'OVER' : 'UNDER'; return { leagueId, eventId, handicap: ratioKey, betType: 'TOTAL_POINTS', side: sideKey, lineId: straightLineId, altLineId, odds }; } else if (type === 'wm' && winningMargin) { const ratioKey = parseRatio(ratio); const { id: specialId } = winningMargin; const wmName = side === 'h' ? `${homeTeamName} By ${ratioKey}` : side === 'c' ? `${awayTeamName} By ${ratioKey}` : ''; const wmItem = winningMargin.contestants.find(contestant => contestant.name == wmName); if (!wmItem) { Logs.outDev('pinnacle item winning margin not found', id, type, action, side, ratio); return null; } const { id: contestantId, lineId, price } = wmItem; return { leagueId, eventId, specialId, contestantId, lineId, odds: price }; } else if (type === 'ot' && exactTotalGoals) { const ratioKey = parseRatio(ratio); const { id: specialId } = exactTotalGoals; const otItem = exactTotalGoals.contestants.find(contestant => contestant.name == ratioKey); if (!otItem) { Logs.outDev('pinnacle item exact total goals not found', id, type, action, side, ratio); return null; } const { id: contestantId, lineId, price } = otItem; return { leagueId, eventId, specialId, contestantId, lineId, odds: price }; } else { Logs.outDev('pinnacle ior type not found', ior, id); return null; } } /** * 获取平台盘口id信息 */ export const getPlatformIorInfo = async (ior, platform, id) => { const getInfo = { polymarket() { return getPolymarketIorInfo(ior, id); }, pinnacle() { return getPinnacleIorInfo(ior, id); } } Logs.outDev('getPlatformIorInfo', { ior, platform, id }); return getInfo[platform]?.(); } /** * 获取polymarket盘口详细信息 */ const getPolymarketIorDetailInfo = async (info) => { const { id } = info; return getOrderBook(id); } /** * 获取pinnacle盘口详细信息 */ const getPinnacleIorDetailInfo = async (info) => { return getLineInfo(info); } /** * 获取平台盘口详细信息 */ export const getPlatformIorsDetailInfo = async (ior, platform, id) => { const info = await getPlatformIorInfo(ior, platform, id); if (!info) { return Promise.reject(new Error('platform ior info not found', { cause: 400 })); } const getInfo = { polymarket() { return getPolymarketIorDetailInfo(info); }, pinnacle() { return getPinnacleIorDetailInfo(info); } } return getInfo[platform]?.(); } /** * 根据最新赔率获取策略 */ export const getSolutionByLatestIors = (iorsInfo, cross_type, retry=false) => { const askIndex = +retry; const iorsValues = iorsInfo.map(item => { if (item.asks) { const bestAsk = [...item.asks].sort((a, b) => a.price - b.price)[askIndex]; const value = fixFloat(1 / bestAsk.price, 3); const maxStake = fixFloat(bestAsk.size * bestAsk.price * USDC_RATE); const minStake = fixFloat(item.min_order_size * bestAsk.price * USDC_RATE); return { value, maxStake, minStake, bestPrice: bestAsk.price }; } else if (item.info) { const value = item.info.price; const maxStake = Math.floor(item.info.maxRiskStake); const minStake = Math.ceil(item.info.minRiskStake*10); return { value, maxStake, minStake }; } }); const nullIndex = iorsValues.findIndex(item => item.value == null); if (nullIndex >= 0) { return { error: `IORS_NULL_VALUE_AT_INDEX_${nullIndex}_RETRY_${askIndex}`, data: iorsInfo }; } if (iorsValues.length === 2) { iorsValues.push({ value: 1, maxStake: 0, minStake: 0 }); } const baseIndex = iorsValues.reduce((minIdx, cur, idx) => cur.value < iorsValues[minIdx].value ? idx : minIdx, 0); const betInfo = { cross_type, base_index: baseIndex, base_stake: 10000, odds_side_a: fixFloat(iorsValues[0].value - 1), odds_side_b: fixFloat(iorsValues[1].value - 1), odds_side_c: fixFloat(iorsValues[2].value - 1), }; const sol = eventSolutions(betInfo, true); const { win_average, win_profit_rate, gold_side_a, gold_side_b, gold_side_c } = sol; if (win_profit_rate < MIN_PROFIT_RATE) { Logs.outDev('win_profit_rate is less than profit rate limit', sol, iorsValues, iorsInfo, cross_type); return { error: `WIN_PROFIT_RATE_LESS_THAN_MIN_PROFIT_RATE_RETRY_${askIndex}`, data: { sol, iorsValues, iorsInfo } }; } const goldRatios = [gold_side_a, gold_side_b]; if (gold_side_c) { goldRatios.push(gold_side_c); } const minVals = iorsValues.map(item => item.minStake); const maxVals = iorsValues.map(item => item.maxStake); const stakeLimit = findMaxMinGroup(goldRatios, minVals, maxVals); const { scaleForMax, scaleForMin } = stakeLimit; if (scaleForMax < scaleForMin) { Logs.outDev('scaleForMax is less than scaleForMin'); if (!retry) { return getSolutionByLatestIors(iorsInfo, cross_type, true); } else { return { error: `NO_ENOUGH_STAKE_SIZE_TO_BET_RETRY_${askIndex}`, data: { sol, iorsValues, iorsInfo } }; } } const winLimit = { max: fixFloat(win_average * scaleForMax / (gold_side_a + gold_side_b + gold_side_c)), min: fixFloat(win_average * scaleForMin / (gold_side_a + gold_side_b + gold_side_c)), } return { sol, iors: iorsValues, stakeLimit, winLimit }; } export const getSoulutionBetResult = async ({ iors, iorsInfo, stakeLimit, stake=0 }) => { const maxStake = stakeLimit.maxGroup.reduce((acc, curr) => acc + curr, 0); const minStake = stakeLimit.minGroup.reduce((acc, curr) => acc + curr, 0); let betStakeGroup = []; if (stake > maxStake || stake < 0) { stake = maxStake; betStakeGroup = stakeLimit.maxGroup; } else if (stake < minStake) { stake = minStake; betStakeGroup = stakeLimit.minGroup; } else { betStakeGroup = stakeLimit.proportions.map(p => p * stake); } const betInfo = iorsInfo.map((item, index) => { if (item.asks) { const bestPrice = +iors[index].bestPrice; const stakeSize = fixFloat(betStakeGroup[index] / USDC_RATE / bestPrice, 0); // 必须保证买单金额小数不超过2位 return { ...item, stakeSize, bestPrice, betIndex: index, platform: 'polymarket' } } else if (item.info) { const stakeSize = fixFloat(betStakeGroup[index], 0); return { ...item, stakeSize, betIndex: index, platform: 'pinnacle' } } }).sort((a, b) => { if (a.platform === 'polymarket' && b.platform === 'pinnacle') { return -1; } else if (a.platform === 'pinnacle' && b.platform === 'polymarket') { return 1; } else { return 0; } }); // return { betInfo }; return runSequentially(betInfo.map(item => async() => { if (item.asks) { const result = await polymarketPlaceOrder(item); return [result, item.betIndex] } else if (item.info) { const result = await pinnaclePlaceOrder(item); return [result, item.betIndex] } })).then(results => { return results.sort((a, b) => a[1] - b[1]).map(item => item[0]); }).catch(error => { Logs.errDev(error); return Promise.reject(error); }); }