Markets.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. import Logs from "../libs/logs.js";
  2. import { platformGet, platformPost } from "../libs/platformRequest.js";
  3. import pinnacleSdk from "../libs/pinnacleSdk.js";
  4. import polymarketSdk from "../libs/polymarketSdk.js";
  5. import eventSolutions from '../triangle/eventSolutions.js';
  6. const MAKER_FEE_RATE = 0.03;
  7. const MAKER_REBATE_RATE = 0.25;
  8. const MIN_PROFIT_RATE = -1;
  9. const PINNACLE_MIN_RISK_STAKE = 1;
  10. /**
  11. * 精确浮点数字
  12. * @param {number} number
  13. * @param {number} x
  14. * @returns {number}
  15. */
  16. const fixFloat = (number, x=3) => {
  17. return parseFloat(number.toFixed(x));
  18. }
  19. /**
  20. * 计算符合比例的最大和最小数组
  21. */
  22. const findMaxMinGroup = (ratios, minVals=10, maxVals=1000) => {
  23. const n = ratios.length;
  24. // 1. 计算比例总和 & 归一化比例
  25. const totalRatio = ratios.reduce((a, b) => a + b, 0);
  26. const proportions = ratios.map(r => r / totalRatio);
  27. // 2. 计算每个位置允许的最大/最小倍数
  28. let maxPossibleScale = Infinity;
  29. let minPossibleScale = 0; // 如果允许0,通常从0开始
  30. for (let i = 0; i < n; i++) {
  31. // 上限约束
  32. if (proportions[i] > 0) {
  33. maxPossibleScale = Math.min(maxPossibleScale, maxVals[i] / proportions[i]);
  34. }
  35. // 下限约束(如果 minVals[i] > 0 才有意义)
  36. if (proportions[i] > 0 && minVals[i] > 0) {
  37. minPossibleScale = Math.max(minPossibleScale, minVals[i] / proportions[i]);
  38. }
  39. }
  40. // 3. 最终取值
  41. const maxGroup = proportions.map(p => p * maxPossibleScale);
  42. const minGroup = proportions.map(p => p * Math.max(minPossibleScale, 0));
  43. return {
  44. maxGroup: maxGroup.map(v => fixFloat(v)), // 可控制精度
  45. minGroup: minGroup.map(v => fixFloat(v)),
  46. proportions: proportions.map(v => fixFloat(v)),
  47. scaleForMax: fixFloat(maxPossibleScale),
  48. scaleForMin: fixFloat(minPossibleScale)
  49. };
  50. }
  51. /**
  52. * 解析吃单手续费比
  53. * 固定金额吃卖单时,手续费比约等于 费率*(1-价格)
  54. * @param {*} price
  55. * @returns {number}
  56. */
  57. const parseAskFee = (price) => {
  58. return fixFloat(100 * MAKER_FEE_RATE * (1 - price), 4);
  59. }
  60. /**
  61. * 解析挂单返佣比
  62. * 固定金额挂买单时,返佣比约等于 费率*(1-价格)*返佣比例
  63. * @param {*} price
  64. * @returns {number}
  65. */
  66. const parseBidRebate = (price) => {
  67. return fixFloat(100 * MAKER_FEE_RATE * (1 - price) * MAKER_REBATE_RATE, 4);
  68. }
  69. /**
  70. * 获取polymarket盘口信息
  71. * @param {*} ior
  72. * @param {*} id
  73. * @returns
  74. */
  75. const getPolymarketIorInfo = async (ior, id) => {
  76. return platformGet(`http://127.0.0.1:9021/api/trading/get_ior_info/${id}/${ior}`)
  77. .then(res => res.data)
  78. .catch(err => {
  79. Logs.out('get polymarket ior info error', err.message);
  80. Logs.errDev(err);
  81. return Promise.reject(err);
  82. });
  83. }
  84. /**
  85. * 获取pinnacle盘口信息
  86. * @param {*} ior
  87. * @param {*} id
  88. * @returns
  89. */
  90. const getPinnacleIorInfo = async (ior, id) => {
  91. return platformGet(`https://cb.long.bid/api/trading/get_ior_info/${id}/${ior}`)
  92. .then(res => res.data)
  93. .catch(err => {
  94. Logs.out('get pinnacle ior info error', err.message);
  95. Logs.errDev(err);
  96. return Promise.reject(err);
  97. });
  98. }
  99. /**
  100. * 获取平台盘口id信息
  101. */
  102. export const getPlatformIorInfo = async (ior, platform, id) => {
  103. const getInfo = {
  104. polymarket() {
  105. return getPolymarketIorInfo(ior, id);
  106. },
  107. pinnacle() {
  108. return getPinnacleIorInfo(ior, id);
  109. },
  110. obsports() {
  111. return {}
  112. },
  113. huangguan() {
  114. return {}
  115. }
  116. }
  117. const result = await getInfo[platform]?.();
  118. Logs.outDev('getPlatformIorInfo', { ior, platform, id }, result);
  119. return result;
  120. }
  121. /**
  122. * 获取polymarket盘口详细信息
  123. */
  124. const getPolymarketIorDetailInfo = async (info) => {
  125. // Logs.outDev('get polymarket ior detail info', info);
  126. const { id } = info;
  127. return polymarketSdk.getOrderBook(id)
  128. .catch(err => {
  129. Logs.out('get polymarket ior detail info error', err.message);
  130. Logs.errDev(err);
  131. return Promise.reject(err);
  132. });
  133. }
  134. /**
  135. * 获取pinnacle盘口详细信息
  136. */
  137. const getPinnacleIorDetailInfo = async (info, channel) => {
  138. // Logs.outDev('get pinnacle ior detail info', { info, channel });
  139. return pinnacleSdk.getLineInfo({ info, channel })
  140. .catch(err => {
  141. Logs.out('get pinnacle ior detail info error', err.message);
  142. Logs.errDev(err);
  143. return Promise.reject(err);
  144. });
  145. }
  146. /**
  147. * 获取平台盘口详细信息
  148. */
  149. export const getPlatformIorsDetailInfo = async (ior, platform, id) => {
  150. const info = await getPlatformIorInfo(ior, platform, id);
  151. if (!info) {
  152. return Promise.reject(new Error('platform ior info not found', { cause: 400 }));
  153. }
  154. const getInfo = {
  155. polymarket() {
  156. return getPolymarketIorDetailInfo(info);
  157. },
  158. pinnacle() {
  159. return getPinnacleIorDetailInfo(info);
  160. },
  161. obsports() {
  162. return {}
  163. },
  164. huangguan() {
  165. return {}
  166. }
  167. }
  168. return getInfo[platform]?.();
  169. }
  170. /**
  171. * 根据最新赔率获取策略
  172. */
  173. export const getSolutionByLatestIors = (iorsInfo, cross_type, retry=false) => {
  174. const askIndex = +retry;
  175. const iorsValues = iorsInfo.map(item => {
  176. if (item.asks) {
  177. const bestAsk = [...item.asks].sort((a, b) => a.price - b.price)[askIndex];
  178. const bestPrice = bestAsk.price - item.tick_size;
  179. const value = fixFloat(1 / bestPrice, 3);
  180. const maxStake = 99999;
  181. const minStake = fixFloat(item.min_order_size * bestAsk.price);
  182. const tickSize = +item.tick_size;
  183. const negRisk = item.neg_risk;
  184. const rebate = parseBidRebate(bestPrice);
  185. const rebateType = 1;
  186. return { value, maxStake, minStake, bestPrice, tickSize, negRisk, rebate, rebateType };
  187. }
  188. else if (item.info) {
  189. const value = item.info.price;
  190. const maxStake = Math.floor(item.info.maxRiskStake);
  191. const minStake = Math.ceil(PINNACLE_MIN_RISK_STAKE > 0 ? PINNACLE_MIN_RISK_STAKE : item.info.minRiskStake);
  192. const rebateType = 1;
  193. return { value, maxStake, minStake, rebateType };
  194. }
  195. else {
  196. return {};
  197. }
  198. });
  199. const nullIndex = iorsValues.findIndex(item => item?.value == null);
  200. if (nullIndex >= 0) {
  201. return { error: `IORS_NULL_VALUE_AT_INDEX_${nullIndex}_RETRY_${askIndex}`, data: iorsInfo };
  202. }
  203. const baseIndex = iorsValues.reduce((minIdx, cur, idx) => cur.value < iorsValues[minIdx].value ? idx : minIdx, 0);
  204. if (iorsValues.length === 2) {
  205. iorsValues.push({ value: 1, maxStake: 0, minStake: 0 });
  206. }
  207. const betInfo = {
  208. cross_type,
  209. base_index: baseIndex,
  210. base_stake: 10000,
  211. odds_side_a: fixFloat(iorsValues[0].value - 1),
  212. odds_side_b: fixFloat(iorsValues[1].value - 1),
  213. odds_side_c: fixFloat(iorsValues[2].value - 1),
  214. rebate_side_a: fixFloat(((iorsValues[0].rebate ?? 0) / 100), 6),
  215. rebate_side_b: fixFloat(((iorsValues[1].rebate ?? 0) / 100), 6),
  216. rebate_side_c: fixFloat(((iorsValues[2].rebate ?? 0) / 100), 6),
  217. rebate_type_side_a: iorsValues[0].rebateType ?? 0,
  218. rebate_type_side_b: iorsValues[1].rebateType ?? 0,
  219. rebate_type_side_c: iorsValues[2].rebateType ?? 0,
  220. };
  221. const sol = eventSolutions(betInfo, true);
  222. const { win_average, win_profit_rate, gold_side_a, gold_side_b, gold_side_c } = sol;
  223. if (win_profit_rate < MIN_PROFIT_RATE) {
  224. Logs.outDev('win_profit_rate is less than profit rate limit', sol, iorsValues, iorsInfo, cross_type);
  225. return { error: `WIN_PROFIT_RATE_LESS_THAN_MIN_PROFIT_RATE_RETRY_${askIndex}`, data: { sol, iorsValues, iorsInfo } };
  226. }
  227. const goldRatios = [gold_side_a, gold_side_b];
  228. if (gold_side_c) {
  229. goldRatios.push(gold_side_c);
  230. }
  231. const minVals = iorsValues.map(item => item.minStake);
  232. const maxVals = iorsValues.map(item => item.maxStake);
  233. const stakeLimit = findMaxMinGroup(goldRatios, minVals, maxVals);
  234. const { scaleForMax, scaleForMin } = stakeLimit;
  235. if (scaleForMax < scaleForMin) {
  236. Logs.outDev('scaleForMax is less than scaleForMin');
  237. if (!retry) {
  238. return getSolutionByLatestIors(iorsInfo, cross_type, true);
  239. }
  240. else {
  241. return { error: `NO_ENOUGH_STAKE_SIZE_TO_BET_RETRY_${askIndex}`, data: { sol, iorsValues, iorsInfo } };
  242. }
  243. }
  244. const winLimit = {
  245. max: fixFloat(win_average * scaleForMax / (gold_side_a + gold_side_b + gold_side_c)),
  246. min: fixFloat(win_average * scaleForMin / (gold_side_a + gold_side_b + gold_side_c)),
  247. }
  248. return { sol, iorsValues, stakeLimit, winLimit };
  249. }
  250. /**
  251. * 创建Polymarket限价挂单 买单
  252. */
  253. export const createPolymarketLimitBuyOrder = async ({ tokenID, price, size, tickSize = "0.01", negRisk = false } = {}) => {
  254. return polymarketSdk.createLimitOrder({ tokenID, price, size, tickSize, negRisk })
  255. .catch(err => {
  256. Logs.out('create polymarket limit buy order error', err.message);
  257. Logs.errDev(err);
  258. return Promise.reject(err);
  259. });
  260. }
  261. /**
  262. * 查询Polymarket单个订单
  263. * @param {Object} options
  264. * @param {string} options.orderID 订单ID
  265. * @returns {Promise<Object>}
  266. */
  267. export const getPolymarketOrder = async ({ orderID } = {}) => {
  268. if (!orderID) {
  269. throw new Error('orderID is required', { cause: 400 });
  270. }
  271. return polymarketSdk.getOrder(orderID)
  272. .catch(err => {
  273. Logs.out('get polymarket order error', err.message);
  274. Logs.errDev(err);
  275. return Promise.reject(err);
  276. });
  277. }
  278. /**
  279. * 查询Polymarket开放订单
  280. * @param {Object} options
  281. * @param {string} options.id 订单ID
  282. * @param {string} options.market 市场ID
  283. * @param {string} options.asset_id token/asset ID
  284. * @param {boolean} options.only_first_page 是否只查询第一页
  285. * @param {string} options.next_cursor 分页cursor
  286. * @returns {Promise<Array>}
  287. */
  288. export const getPolymarketOpenOrders = async ({
  289. id,
  290. market,
  291. asset_id,
  292. only_first_page = false,
  293. next_cursor,
  294. } = {}) => {
  295. return polymarketSdk.getOpenOrders({
  296. id,
  297. market,
  298. asset_id,
  299. only_first_page,
  300. next_cursor,
  301. })
  302. .catch(err => {
  303. Logs.out('get polymarket open orders error', err.message);
  304. Logs.errDev(err);
  305. return Promise.reject(err);
  306. });
  307. }
  308. /**
  309. * 获取Polymarket钱包余额信息
  310. * @param {*} param0
  311. * @returns
  312. */
  313. export const getPolymarketBalanceAllowance = async ({ wallet = "both" } = {}) => {
  314. if (!wallet || !["both", "proxy", "deposit"].includes(wallet)) {
  315. throw new Error('invalid wallet', { cause: 400 });
  316. }
  317. return polymarketSdk.getBalanceAllowance({ wallet })
  318. .catch(err => {
  319. Logs.out('get polymarket balance allowance error', err.message);
  320. Logs.errDev(err);
  321. return Promise.reject(err);
  322. });
  323. }
  324. /**
  325. * 获取Pinnacle账户余额信息
  326. */
  327. export const getPinnacleBalance = async ({ channel } = {}) => {
  328. return pinnacleSdk.getAccountBalance(channel)
  329. .then(balance => ({
  330. ...balance,
  331. account: process.env.PINNACLE_USERNAME,
  332. }))
  333. .catch(err => {
  334. Logs.out('get pinnacle balance error', err.message);
  335. Logs.errDev(err);
  336. return Promise.reject(err);
  337. });
  338. }
  339. /**
  340. * Polymarket钱包之间转账
  341. * 通过HTTP调用polymarket项目接口,server项目不直接依赖polymarket项目代码
  342. * @param {Object} options
  343. * @param {string|number} options.amount 转账数量
  344. * @param {"proxy"|"deposit"} options.from 来源钱包类型
  345. * @param {"proxy"|"deposit"} options.to 目标钱包类型
  346. * @returns {Promise<Object>}
  347. */
  348. export const transferPolymarketWallet = async ({ amount, from, to } = {}) => {
  349. if (!amount) {
  350. throw new Error('amount is required', { cause: 400 });
  351. }
  352. if (!from || !["proxy", "deposit"].includes(from)) {
  353. throw new Error('from is required', { cause: 400 });
  354. }
  355. if (!to || !["proxy", "deposit"].includes(to)) {
  356. throw new Error('to is required', { cause: 400 });
  357. }
  358. if (from === to) {
  359. throw new Error('from and to cannot be the same', { cause: 400 });
  360. }
  361. return polymarketSdk.transferWallet({ amount, from, to })
  362. .catch(err => {
  363. Logs.out('transfer polymarket wallet error', err.message);
  364. Logs.errDev(err);
  365. return Promise.reject(err);
  366. });
  367. }