| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458 |
- 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");
- /**
- * 最小盈利率
- */
- 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' && !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 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 && !action && !ratio) {
- 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, channel) => {
- return getLineInfo(info, channel);
- }
- /**
- * 获取平台盘口详细信息
- */
- export const getPlatformIorsDetailInfo = async (ior, platform, id, channel) => {
- 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, channel);
- }
- }
- return getInfo[platform]?.();
- }
- /**
- * 平台盘口下注
- */
- export const placePlatformOrder = async (ior, platform, id, stake=0, channel) => {
- const iorInfo = await getPlatformIorsDetailInfo(ior, platform, id, channel);
- const betInfo = { ...iorInfo, stakeSize: stake };
- const placeOrder = {
- polymarket() {
- return polymarketPlaceOrder(betInfo);
- },
- pinnacle() {
- return pinnaclePlaceOrder(betInfo, channel);
- }
- }
- return placeOrder[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);
- const minStake = fixFloat(item.min_order_size * bestAsk.price);
- 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 };
- }
- const baseIndex = iorsValues.reduce((minIdx, cur, idx) => cur.value < iorsValues[minIdx].value ? idx : minIdx, 0);
- if (iorsValues.length === 2) {
- iorsValues.push({ value: 1, maxStake: 0, minStake: 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] / 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);
- });
- }
|