Markets.js 9.9 KB

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