| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400 |
- import Logs from "../libs/logs.js";
- import { platformGet, platformPost } from "../libs/platformRequest.js";
- import pinnacleSdk from "../libs/pinnacleSdk.js";
- import polymarketSdk from "../libs/polymarketSdk.js";
- import eventSolutions from '../triangle/eventSolutions.js';
- const MAKER_FEE_RATE = 0.03;
- const MAKER_REBATE_RATE = 0.25;
- const MIN_PROFIT_RATE = -1;
- const PINNACLE_MIN_RISK_STAKE = 1;
- /**
- * 精确浮点数字
- * @param {number} number
- * @param {number} x
- * @returns {number}
- */
- const fixFloat = (number, x=3) => {
- return parseFloat(number.toFixed(x));
- }
- /**
- * 计算符合比例的最大和最小数组
- */
- const findMaxMinGroup = (ratios, minVals=10, maxVals=1000) => {
- 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)
- };
- }
- /**
- * 解析吃单手续费比
- * 固定金额吃卖单时,手续费比约等于 费率*(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);
- }
- /**
- * 获取polymarket盘口信息
- * @param {*} ior
- * @param {*} id
- * @returns
- */
- const getPolymarketIorInfo = async (ior, id) => {
- return platformGet(`http://127.0.0.1:9021/api/trading/get_ior_info/${id}/${ior}`)
- .then(res => res.data)
- .catch(err => {
- Logs.out('get polymarket ior info error', err.message);
- Logs.errDev(err);
- return Promise.reject(err);
- });
- }
- /**
- * 获取pinnacle盘口信息
- * @param {*} ior
- * @param {*} id
- * @returns
- */
- const getPinnacleIorInfo = async (ior, id) => {
- return platformGet(`https://cb.long.bid/api/trading/get_ior_info/${id}/${ior}`)
- .then(res => res.data)
- .catch(err => {
- Logs.out('get pinnacle ior info error', err.message);
- Logs.errDev(err);
- return Promise.reject(err);
- });
- }
- /**
- * 获取平台盘口id信息
- */
- export const getPlatformIorInfo = async (ior, platform, id) => {
- const getInfo = {
- polymarket() {
- return getPolymarketIorInfo(ior, id);
- },
- pinnacle() {
- return getPinnacleIorInfo(ior, id);
- },
- obsports() {
- return {}
- },
- huangguan() {
- return {}
- }
- }
- const result = await getInfo[platform]?.();
- Logs.outDev('getPlatformIorInfo', { ior, platform, id },result);
- return result;
- }
- /**
- * 获取polymarket盘口详细信息
- */
- const getPolymarketIorDetailInfo = async (info) => {
- Logs.outDev('get polymarket ior detail info', info);
- const { id } = info;
- return polymarketSdk.getOrderBook(id)
- .catch(err => {
- Logs.out('get polymarket ior detail info error', err.message);
- Logs.errDev(err);
- return Promise.reject(err);
- });
- }
- /**
- * 获取pinnacle盘口详细信息
- */
- const getPinnacleIorDetailInfo = async (info, channel) => {
- Logs.outDev('get pinnacle ior detail info', { info, channel });
- return pinnacleSdk.getLineInfo({ info, channel })
- .catch(err => {
- Logs.out('get pinnacle ior detail info error', err.message);
- Logs.errDev(err);
- return Promise.reject(err);
- });
- }
- /**
- * 获取平台盘口详细信息
- */
- 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);
- },
- obsports() {
- return {}
- },
- huangguan() {
- return {}
- }
- }
- 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 bestPrice = bestAsk.price - item.tick_size;
- const value = fixFloat(1 / bestPrice, 3);
- const maxStake = 99999;
- const minStake = fixFloat(item.min_order_size * bestAsk.price);
- const tickSize = +item.tick_size;
- const negRisk = item.neg_risk;
- const rebate = parseBidRebate(bestPrice);
- const rebateType = 1;
- return { value, maxStake, minStake, bestPrice, tickSize, negRisk, rebate, rebateType };
- }
- else if (item.info) {
- const value = item.info.price;
- const maxStake = Math.floor(item.info.maxRiskStake);
- const minStake = Math.ceil(PINNACLE_MIN_RISK_STAKE > 0 ? PINNACLE_MIN_RISK_STAKE : item.info.minRiskStake);
- const rebateType = 1;
- return { value, maxStake, minStake, rebateType };
- }
- });
- 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),
- rebate_side_a: fixFloat(((iorsValues[0].rebate ?? 0) / 100), 6),
- rebate_side_b: fixFloat(((iorsValues[1].rebate ?? 0) / 100), 6),
- rebate_side_c: fixFloat(((iorsValues[2].rebate ?? 0) / 100), 6),
- rebate_type_side_a: iorsValues[0].rebateType ?? 0,
- rebate_type_side_b: iorsValues[1].rebateType ?? 0,
- rebate_type_side_c: iorsValues[2].rebateType ?? 0,
- };
- 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, iorsValues, stakeLimit, winLimit };
- }
- /**
- * 创建Polymarket限价挂单 买单
- */
- export const createPolymarketLimitBuyOrder = async ({ tokenID, price, size, tickSize = "0.01", negRisk = false } = {}) => {
- return polymarketSdk.createLimitOrder({ tokenID, price, size, tickSize, negRisk })
- .catch(err => {
- Logs.out('create polymarket limit buy order error', err.message);
- Logs.errDev(err);
- return Promise.reject(err);
- });
- }
- /**
- * 查询Polymarket单个订单
- * @param {Object} options
- * @param {string} options.orderID 订单ID
- * @returns {Promise<Object>}
- */
- export const getPolymarketOrder = async ({ orderID } = {}) => {
- if (!orderID) {
- throw new Error('orderID is required', { cause: 400 });
- }
- return polymarketSdk.getOrder(orderID)
- .catch(err => {
- Logs.out('get polymarket order error', err.message);
- Logs.errDev(err);
- return Promise.reject(err);
- });
- }
- /**
- * 查询Polymarket开放订单
- * @param {Object} options
- * @param {string} options.id 订单ID
- * @param {string} options.market 市场ID
- * @param {string} options.asset_id token/asset ID
- * @param {boolean} options.only_first_page 是否只查询第一页
- * @param {string} options.next_cursor 分页cursor
- * @returns {Promise<Array>}
- */
- export const getPolymarketOpenOrders = async ({
- id,
- market,
- asset_id,
- only_first_page = false,
- next_cursor,
- } = {}) => {
- return polymarketSdk.getOpenOrders({
- id,
- market,
- asset_id,
- only_first_page,
- next_cursor,
- })
- .catch(err => {
- Logs.out('get polymarket open orders error', err.message);
- Logs.errDev(err);
- return Promise.reject(err);
- });
- }
- /**
- * 获取Polymarket钱包余额信息
- * @param {*} param0
- * @returns
- */
- export const getPolymarketBalanceAllowance = async ({ wallet = "both" } = {}) => {
- if (!wallet || !["both", "proxy", "deposit"].includes(wallet)) {
- throw new Error('invalid wallet', { cause: 400 });
- }
- return polymarketSdk.getBalanceAllowance({ wallet })
- .catch(err => {
- Logs.out('get polymarket balance allowance error', err.message);
- Logs.errDev(err);
- return Promise.reject(err);
- });
- }
- /**
- * 获取Pinnacle账户余额信息
- */
- export const getPinnacleBalance = async ({ channel } = {}) => {
- return pinnacleSdk.getAccountBalance(channel)
- .then(balance => ({
- ...balance,
- account: process.env.PINNACLE_USERNAME,
- }))
- .catch(err => {
- Logs.out('get pinnacle balance error', err.message);
- Logs.errDev(err);
- return Promise.reject(err);
- });
- }
- /**
- * Polymarket钱包之间转账
- * 通过HTTP调用polymarket项目接口,server项目不直接依赖polymarket项目代码
- * @param {Object} options
- * @param {string|number} options.amount 转账数量
- * @param {"proxy"|"deposit"} options.from 来源钱包类型
- * @param {"proxy"|"deposit"} options.to 目标钱包类型
- * @returns {Promise<Object>}
- */
- export const transferPolymarketWallet = async ({ amount, from, to } = {}) => {
- if (!amount) {
- throw new Error('amount is required', { cause: 400 });
- }
- if (!from || !["proxy", "deposit"].includes(from)) {
- throw new Error('from is required', { cause: 400 });
- }
- if (!to || !["proxy", "deposit"].includes(to)) {
- throw new Error('to is required', { cause: 400 });
- }
- if (from === to) {
- throw new Error('from and to cannot be the same', { cause: 400 });
- }
- return polymarketSdk.transferWallet({ amount, from, to })
- .catch(err => {
- Logs.out('transfer polymarket wallet error', err.message);
- Logs.errDev(err);
- return Promise.reject(err);
- });
- }
|