Markets.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  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. });
  196. const nullIndex = iorsValues.findIndex(item => item.value == null);
  197. if (nullIndex >= 0) {
  198. return { error: `IORS_NULL_VALUE_AT_INDEX_${nullIndex}_RETRY_${askIndex}`, data: iorsInfo };
  199. }
  200. const baseIndex = iorsValues.reduce((minIdx, cur, idx) => cur.value < iorsValues[minIdx].value ? idx : minIdx, 0);
  201. if (iorsValues.length === 2) {
  202. iorsValues.push({ value: 1, maxStake: 0, minStake: 0 });
  203. }
  204. const betInfo = {
  205. cross_type,
  206. base_index: baseIndex,
  207. base_stake: 10000,
  208. odds_side_a: fixFloat(iorsValues[0].value - 1),
  209. odds_side_b: fixFloat(iorsValues[1].value - 1),
  210. odds_side_c: fixFloat(iorsValues[2].value - 1),
  211. rebate_side_a: fixFloat(((iorsValues[0].rebate ?? 0) / 100), 6),
  212. rebate_side_b: fixFloat(((iorsValues[1].rebate ?? 0) / 100), 6),
  213. rebate_side_c: fixFloat(((iorsValues[2].rebate ?? 0) / 100), 6),
  214. rebate_type_side_a: iorsValues[0].rebateType ?? 0,
  215. rebate_type_side_b: iorsValues[1].rebateType ?? 0,
  216. rebate_type_side_c: iorsValues[2].rebateType ?? 0,
  217. };
  218. const sol = eventSolutions(betInfo, true);
  219. const { win_average, win_profit_rate, gold_side_a, gold_side_b, gold_side_c } = sol;
  220. if (win_profit_rate < MIN_PROFIT_RATE) {
  221. Logs.outDev('win_profit_rate is less than profit rate limit', sol, iorsValues, iorsInfo, cross_type);
  222. return { error: `WIN_PROFIT_RATE_LESS_THAN_MIN_PROFIT_RATE_RETRY_${askIndex}`, data: { sol, iorsValues, iorsInfo } };
  223. }
  224. const goldRatios = [gold_side_a, gold_side_b];
  225. if (gold_side_c) {
  226. goldRatios.push(gold_side_c);
  227. }
  228. const minVals = iorsValues.map(item => item.minStake);
  229. const maxVals = iorsValues.map(item => item.maxStake);
  230. const stakeLimit = findMaxMinGroup(goldRatios, minVals, maxVals);
  231. const { scaleForMax, scaleForMin } = stakeLimit;
  232. if (scaleForMax < scaleForMin) {
  233. Logs.outDev('scaleForMax is less than scaleForMin');
  234. if (!retry) {
  235. return getSolutionByLatestIors(iorsInfo, cross_type, true);
  236. }
  237. else {
  238. return { error: `NO_ENOUGH_STAKE_SIZE_TO_BET_RETRY_${askIndex}`, data: { sol, iorsValues, iorsInfo } };
  239. }
  240. }
  241. const winLimit = {
  242. max: fixFloat(win_average * scaleForMax / (gold_side_a + gold_side_b + gold_side_c)),
  243. min: fixFloat(win_average * scaleForMin / (gold_side_a + gold_side_b + gold_side_c)),
  244. }
  245. return { sol, iorsValues, stakeLimit, winLimit };
  246. }
  247. /**
  248. * 创建Polymarket限价挂单 买单
  249. */
  250. export const createPolymarketLimitBuyOrder = async ({ tokenID, price, size, tickSize = "0.01", negRisk = false } = {}) => {
  251. return polymarketSdk.createLimitOrder({ tokenID, price, size, tickSize, negRisk })
  252. .catch(err => {
  253. Logs.out('create polymarket limit buy order error', err.message);
  254. Logs.errDev(err);
  255. return Promise.reject(err);
  256. });
  257. }
  258. /**
  259. * 查询Polymarket单个订单
  260. * @param {Object} options
  261. * @param {string} options.orderID 订单ID
  262. * @returns {Promise<Object>}
  263. */
  264. export const getPolymarketOrder = async ({ orderID } = {}) => {
  265. if (!orderID) {
  266. throw new Error('orderID is required', { cause: 400 });
  267. }
  268. return polymarketSdk.getOrder(orderID)
  269. .catch(err => {
  270. Logs.out('get polymarket order error', err.message);
  271. Logs.errDev(err);
  272. return Promise.reject(err);
  273. });
  274. }
  275. /**
  276. * 查询Polymarket开放订单
  277. * @param {Object} options
  278. * @param {string} options.id 订单ID
  279. * @param {string} options.market 市场ID
  280. * @param {string} options.asset_id token/asset ID
  281. * @param {boolean} options.only_first_page 是否只查询第一页
  282. * @param {string} options.next_cursor 分页cursor
  283. * @returns {Promise<Array>}
  284. */
  285. export const getPolymarketOpenOrders = async ({
  286. id,
  287. market,
  288. asset_id,
  289. only_first_page = false,
  290. next_cursor,
  291. } = {}) => {
  292. return polymarketSdk.getOpenOrders({
  293. id,
  294. market,
  295. asset_id,
  296. only_first_page,
  297. next_cursor,
  298. })
  299. .catch(err => {
  300. Logs.out('get polymarket open orders error', err.message);
  301. Logs.errDev(err);
  302. return Promise.reject(err);
  303. });
  304. }
  305. /**
  306. * 获取Polymarket钱包余额信息
  307. * @param {*} param0
  308. * @returns
  309. */
  310. export const getPolymarketBalanceAllowance = async ({ wallet = "both" } = {}) => {
  311. if (!wallet || !["both", "proxy", "deposit"].includes(wallet)) {
  312. throw new Error('invalid wallet', { cause: 400 });
  313. }
  314. return polymarketSdk.getBalanceAllowance({ wallet })
  315. .catch(err => {
  316. Logs.out('get polymarket balance allowance error', err.message);
  317. Logs.errDev(err);
  318. return Promise.reject(err);
  319. });
  320. }
  321. /**
  322. * 获取Pinnacle账户余额信息
  323. */
  324. export const getPinnacleBalance = async ({ channel } = {}) => {
  325. return pinnacleSdk.getAccountBalance(channel)
  326. .then(balance => ({
  327. ...balance,
  328. account: process.env.PINNACLE_USERNAME,
  329. }))
  330. .catch(err => {
  331. Logs.out('get pinnacle balance error', err.message);
  332. Logs.errDev(err);
  333. return Promise.reject(err);
  334. });
  335. }
  336. /**
  337. * Polymarket钱包之间转账
  338. * 通过HTTP调用polymarket项目接口,server项目不直接依赖polymarket项目代码
  339. * @param {Object} options
  340. * @param {string|number} options.amount 转账数量
  341. * @param {"proxy"|"deposit"} options.from 来源钱包类型
  342. * @param {"proxy"|"deposit"} options.to 目标钱包类型
  343. * @returns {Promise<Object>}
  344. */
  345. export const transferPolymarketWallet = async ({ amount, from, to } = {}) => {
  346. if (!amount) {
  347. throw new Error('amount is required', { cause: 400 });
  348. }
  349. if (!from || !["proxy", "deposit"].includes(from)) {
  350. throw new Error('from is required', { cause: 400 });
  351. }
  352. if (!to || !["proxy", "deposit"].includes(to)) {
  353. throw new Error('to is required', { cause: 400 });
  354. }
  355. if (from === to) {
  356. throw new Error('from and to cannot be the same', { cause: 400 });
  357. }
  358. return polymarketSdk.transferWallet({ amount, from, to })
  359. .catch(err => {
  360. Logs.out('transfer polymarket wallet error', err.message);
  361. Logs.errDev(err);
  362. return Promise.reject(err);
  363. });
  364. }