Răsfoiți Sursa

增加订单功能

flyzto 3 săptămâni în urmă
părinte
comite
8b8edaa739

+ 41 - 0
polymarket/libs/polymarketClient.js

@@ -687,6 +687,47 @@ export const createLimitOrder = async ({
   return client.createAndPostOrder(orderData, orderOptions, orderType, postOnly, deferExec);
 }
 
+/**
+ * 查询单个 Polymarket 订单
+ * @param {string} orderID 订单 ID
+ * @returns {Promise<Object>}
+ */
+export const getOrder = async (orderID) => {
+  if (!orderID) {
+    throw new Error("orderID is required", { cause: 400 });
+  }
+
+  const client = createClobClient();
+  return client.getOrder(orderID);
+}
+
+/**
+ * 查询 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 getOpenOrders = async ({
+  id,
+  market,
+  asset_id,
+  only_first_page = false,
+  next_cursor,
+} = {}) => {
+  const client = createClobClient();
+  const params = {
+    ...(id ? { id } : {}),
+    ...(market ? { market } : {}),
+    ...(asset_id ? { asset_id } : {}),
+  };
+
+  return client.getOpenOrders(params, Boolean(only_first_page), next_cursor);
+}
+
 /**
  * 请求平台数据
  * @param {*} options

+ 35 - 0
polymarket/routes/trading.js

@@ -8,6 +8,8 @@ import {
   createLimitOrder,
   getBalanceAllowance,
   getMultipleOrderBooks,
+  getOpenOrders,
+  getOrder,
   getOrderBook,
   transferWallet,
 } from "../libs/polymarketClient.js";
@@ -122,6 +124,39 @@ router.post('/orders/limit', (req, res) => {
   });
 });
 
+router.get('/orders/open', (req, res) => {
+  const {
+    id,
+    market,
+    asset_id,
+    assetId,
+    only_first_page,
+    next_cursor,
+  } = req.query;
+
+  getOpenOrders({
+    id,
+    market,
+    asset_id: asset_id || assetId,
+    only_first_page: only_first_page === true || only_first_page === 'true',
+    next_cursor,
+  })
+  .then(data => res.sendSuccess(data))
+  .catch(error => {
+    Logs.errDev('get open orders error', error);
+    return res.sendError(error);
+  });
+});
+
+router.get('/orders/:orderID', (req, res) => {
+  getOrder(req.params.orderID)
+  .then(data => res.sendSuccess(data))
+  .catch(error => {
+    Logs.errDev('get order error', error);
+    return res.sendError(error);
+  });
+});
+
 startSyncMarketsData();
 
 export default router;

+ 8 - 1
server/main.js

@@ -1,5 +1,6 @@
 import 'dotenv/config';
 
+import mongoose from 'mongoose';
 import express from 'express';
 import expressWs from 'express-ws';
 import cookieParser from 'cookie-parser';
@@ -93,4 +94,10 @@ app.use('/api/platforms', requireInternalToken, platformsRoutes);
 
 // 启动服务
 const PORT = process.env.PORT || 9020;
-app.listen(PORT, () => Logs.out(`Server running on port ${PORT}`));
+
+mongoose.connect(process.env.MONGO_URI)
+.then(() => {
+  Logs.out('MongoDB connected');
+  app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
+})
+.catch(Logs.err);

+ 86 - 0
server/models/BetOrder.js

@@ -0,0 +1,86 @@
+import mongoose from 'mongoose';
+
+const BetOrderSchema = new mongoose.Schema({
+  sid: { type: String, index: true },
+  stake: { type: Number, default: 0 },
+  platform: { type: String, default: 'polymarket', index: true },
+  status: { type: String, default: 'created', index: true },
+  side: { type: String, default: 'BUY', index: true },
+  size: { type: Number },
+  polymarketOrderID: { type: String, index: true },
+  polymarketOrder: { type: mongoose.Schema.Types.Mixed },
+  iorsValues: { type: mongoose.Schema.Types.Mixed },
+  sol: { type: mongoose.Schema.Types.Mixed },
+  stakeLimit: { type: mongoose.Schema.Types.Mixed },
+  winLimit: { type: mongoose.Schema.Types.Mixed },
+  iorsInfo: { type: mongoose.Schema.Types.Mixed },
+  cpr: { type: mongoose.Schema.Types.Mixed },
+  gameRelation: { type: mongoose.Schema.Types.Mixed },
+}, {
+  timestamps: true,
+});
+
+BetOrderSchema.index({ createdAt: -1 });
+
+const BetOrder = mongoose.models.BetOrder || mongoose.model('BetOrder', BetOrderSchema);
+
+export const createBetOrder = async ({
+  sid,
+  stake,
+  side = 'BUY',
+  size,
+  polymarketOrder,
+  iorsValues,
+  sol,
+  stakeLimit,
+  winLimit,
+  iorsInfo,
+  cpr,
+  gameRelation,
+} = {}) => {
+  const orderID = polymarketOrder?.orderID || polymarketOrder?.id;
+  const status = polymarketOrder?.status || 'created';
+
+  return BetOrder.create({
+    sid: sid != null ? String(sid) : undefined,
+    stake: Number(stake) || 0,
+    status,
+    side,
+    size: Number.isFinite(Number(size)) ? Number(size) : undefined,
+    polymarketOrderID: orderID,
+    polymarketOrder,
+    iorsValues,
+    sol,
+    stakeLimit,
+    winLimit,
+    iorsInfo,
+    cpr,
+    gameRelation,
+  });
+}
+
+export const getBetOrders = async ({ limit = 100 } = {}) => {
+  const queryLimit = Math.min(Math.max(Number(limit) || 100, 1), 500);
+  return BetOrder.find({})
+  .sort({ createdAt: -1 })
+  .limit(queryLimit)
+  .lean();
+}
+
+export const removeBetOrder = async (id) => {
+  if (!id) {
+    throw new Error('id is required', { cause: 400 });
+  }
+
+  const result = await BetOrder.findByIdAndDelete(id).lean();
+  if (!result) {
+    throw new Error('order not found', { cause: 404 });
+  }
+  return result;
+}
+
+export default {
+  createBetOrder,
+  getBetOrders,
+  removeBetOrder,
+};

+ 65 - 5
server/models/Games.js

@@ -1,12 +1,15 @@
 import GetTranslation from "../libs/getTranslation.js";
 import { getSolutionsWithRelations, getGamesRelationsMap } from "../libs/getGamesRelations.js";
 import Store from "../state/store.js";
+import { createBetOrder, getBetOrders, removeBetOrder } from "./BetOrder.js";
 
 import {
   getPlatformIorsDetailInfo,
   getSolutionByLatestIors,
   createPolymarketLimitBuyOrder,
   getPolymarketBalanceAllowance,
+  getPolymarketOpenOrders,
+  getPolymarketOrder,
   transferPolymarketWallet,
 } from "./Markets.js";
 
@@ -178,9 +181,9 @@ export const getSolutionIorsInfo = async (sid) => {
  * 根据策略下注
  */
 export const betSolution = async (sid, stake=0) => {
-  const { cpr, iorsInfo, cross_type } = await getSolutionIorsInfo(sid);
-  const solutionInfo = getSolutionByLatestIors(iorsInfo, cross_type);
-  const { stakeLimit: { minGroup } } = solutionInfo ?? { stakeLimit: {} };
+  const { cpr, iorsInfo, cross_type, gameRelation } = await getSolutionIorsInfo(sid);
+  const { iorsValues, stakeLimit, winLimit, sol } = getSolutionByLatestIors(iorsInfo, cross_type);
+  const { minGroup } = stakeLimit ?? {};
   if (!minGroup?.length) {
     return Promise.reject(new Error('no stake limit', { cause: 400 }));
   }
@@ -208,7 +211,63 @@ export const betSolution = async (sid, stake=0) => {
     negRisk: polymarketInfo.neg_risk,
   });
 
-  return { polymarketOrder, ...solutionInfo, iorsInfo, cpr };
+  const result = {
+    polymarketOrder,
+    iorsValues,
+    sol,
+    stakeLimit,
+    winLimit,
+    iorsInfo,
+    cpr,
+    gameRelation,
+  };
+  return createBetOrder({
+    sid,
+    stake: polymarketStake,
+    side: 'BUY',
+    size: polymarketStakeCount,
+    ...result,
+  });
+}
+
+/**
+ * 获取下注订单记录
+ */
+export const getOrders = async ({ limit = 100 } = {}) => {
+  return getBetOrders({ limit });
+}
+
+/**
+ * 删除下注订单记录
+ */
+export const removeOrder = async (id) => {
+  return removeBetOrder(id);
+}
+
+/**
+ * 查询Polymarket单个订单
+ */
+export const getOrder = async ({ orderID } = {}) => {
+  return getPolymarketOrder({ orderID });
+}
+
+/**
+ * 查询Polymarket开放订单
+ */
+export const getOpenOrders = async ({
+  id,
+  market,
+  asset_id,
+  only_first_page = false,
+  next_cursor,
+} = {}) => {
+  return getPolymarketOpenOrders({
+    id,
+    market,
+    asset_id,
+    only_first_page,
+    next_cursor,
+  });
 }
 
 /**
@@ -232,5 +291,6 @@ setInterval(cleanGamesRelations, 1000 * 60);
 export default {
   getLeagues, setLeaguesRelation, removeLeaguesRelation, getLeaguesRelations,
   getGames, setGamesRelation, removeGamesRelation, getGamesRelations,
-  getSolutions, getSolutionIorsInfo, betSolution, getPolymarketBalanceAllowance, transferPolymarketWallet,
+  getSolutions, getSolutionIorsInfo, betSolution, getOrders, removeOrder, getOrder, getOpenOrders,
+  getPolymarketBalanceAllowance, transferPolymarketWallet,
 };

+ 49 - 0
server/models/Markets.js

@@ -271,6 +271,55 @@ export const createPolymarketLimitBuyOrder = async ({ tokenID, price, size, tick
   });
 }
 
+/**
+ * 查询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 platformGet(`http://127.0.0.1:9021/api/trading/orders/${orderID}`)
+  .then(res => res.data)
+  .catch(err => {
+    Logs.errDev('get polymarket order error', 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 platformGet(`http://127.0.0.1:9021/api/trading/orders/open`, {
+    ...(id ? { id } : {}),
+    ...(market ? { market } : {}),
+    ...(asset_id ? { asset_id } : {}),
+    ...(only_first_page ? { only_first_page } : {}),
+    ...(next_cursor ? { next_cursor } : {}),
+  })
+  .then(res => res.data)
+  .catch(err => {
+    Logs.errDev('get polymarket open orders error', err);
+    return Promise.reject(err);
+  });
+}
+
 /**
  * 获取Polymarket钱包余额信息
  * @param {*} param0

+ 206 - 1
server/package-lock.json

@@ -15,7 +15,8 @@
         "dayjs": "^1.11.19",
         "dotenv": "^17.2.3",
         "express": "^5.2.1",
-        "express-ws": "^5.0.2"
+        "express-ws": "^5.0.2",
+        "mongoose": "^9.6.2"
       }
     },
     "node_modules/@google-cloud/common": {
@@ -140,6 +141,15 @@
         "url": "https://opencollective.com/js-sdsl"
       }
     },
+    "node_modules/@mongodb-js/saslprep": {
+      "version": "1.4.11",
+      "resolved": "https://registry.npmmirror.com/@mongodb-js/saslprep/-/saslprep-1.4.11.tgz",
+      "integrity": "sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==",
+      "license": "MIT",
+      "dependencies": {
+        "sparse-bitfield": "^3.0.3"
+      }
+    },
     "node_modules/@pkgjs/parseargs": {
       "version": "0.11.0",
       "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -232,6 +242,21 @@
         "undici-types": "~7.16.0"
       }
     },
+    "node_modules/@types/webidl-conversions": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmmirror.com/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
+      "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/whatwg-url": {
+      "version": "13.0.0",
+      "resolved": "https://registry.npmmirror.com/@types/whatwg-url/-/whatwg-url-13.0.0.tgz",
+      "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/webidl-conversions": "*"
+      }
+    },
     "node_modules/accepts": {
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz",
@@ -372,6 +397,15 @@
         "balanced-match": "^1.0.0"
       }
     },
+    "node_modules/bson": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmmirror.com/bson/-/bson-7.2.0.tgz",
+      "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=20.19.0"
+      }
+    },
     "node_modules/buffer-equal-constant-time": {
       "version": "1.0.1",
       "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -1432,6 +1466,15 @@
         "safe-buffer": "^5.0.1"
       }
     },
+    "node_modules/kareem": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmmirror.com/kareem/-/kareem-3.3.0.tgz",
+      "integrity": "sha512-kpSuLD3/7RenBnjnJdOHXCKC8dTd1JzeOiJhN0necWWci6cC+qX+VuwPnMVgb+a4+KNJSfgqahpnfWaeDXCimw==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
     "node_modules/lodash.camelcase": {
       "version": "4.3.0",
       "resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@@ -1468,6 +1511,12 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/memory-pager": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/memory-pager/-/memory-pager-1.5.0.tgz",
+      "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
+      "license": "MIT"
+    },
     "node_modules/merge-descriptors": {
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
@@ -1529,6 +1578,104 @@
         "node": ">=16 || 14 >=14.17"
       }
     },
+    "node_modules/mongodb-connection-string-url": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmmirror.com/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz",
+      "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@types/whatwg-url": "^13.0.0",
+        "whatwg-url": "^14.1.0"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      }
+    },
+    "node_modules/mongoose": {
+      "version": "9.6.2",
+      "resolved": "https://registry.npmmirror.com/mongoose/-/mongoose-9.6.2.tgz",
+      "integrity": "sha512-7m8HntjkoRnwEmuPC0kdlwcZXJOQf4twumFj+PNzg/anqqZE2Er7hQslqyzy07mP3JcFjoTSgH5765PyqOXsxw==",
+      "license": "MIT",
+      "dependencies": {
+        "kareem": "3.3.0",
+        "mongodb": "~7.2",
+        "mpath": "0.9.0",
+        "mquery": "6.0.0",
+        "ms": "2.1.3",
+        "sift": "17.1.3"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mongoose"
+      }
+    },
+    "node_modules/mongoose/node_modules/mongodb": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmmirror.com/mongodb/-/mongodb-7.2.0.tgz",
+      "integrity": "sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@mongodb-js/saslprep": "^1.3.0",
+        "bson": "^7.2.0",
+        "mongodb-connection-string-url": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "peerDependencies": {
+        "@aws-sdk/credential-providers": "^3.806.0",
+        "@mongodb-js/zstd": "^7.0.0",
+        "gcp-metadata": "^7.0.1",
+        "kerberos": "^7.0.0",
+        "mongodb-client-encryption": ">=7.0.0 <7.1.0",
+        "snappy": "^7.3.2",
+        "socks": "^2.8.6"
+      },
+      "peerDependenciesMeta": {
+        "@aws-sdk/credential-providers": {
+          "optional": true
+        },
+        "@mongodb-js/zstd": {
+          "optional": true
+        },
+        "gcp-metadata": {
+          "optional": true
+        },
+        "kerberos": {
+          "optional": true
+        },
+        "mongodb-client-encryption": {
+          "optional": true
+        },
+        "snappy": {
+          "optional": true
+        },
+        "socks": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/mpath": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmmirror.com/mpath/-/mpath-0.9.0.tgz",
+      "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/mquery": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/mquery/-/mquery-6.0.0.tgz",
+      "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.19.0"
+      }
+    },
     "node_modules/ms": {
       "version": "2.1.3",
       "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
@@ -1729,6 +1876,15 @@
       "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
       "license": "MIT"
     },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/qs": {
       "version": "6.14.1",
       "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.1.tgz",
@@ -2005,6 +2161,12 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/sift": {
+      "version": "17.1.3",
+      "resolved": "https://registry.npmmirror.com/sift/-/sift-17.1.3.tgz",
+      "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
+      "license": "MIT"
+    },
     "node_modules/signal-exit": {
       "version": "4.1.0",
       "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz",
@@ -2017,6 +2179,15 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/sparse-bitfield": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmmirror.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
+      "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
+      "license": "MIT",
+      "dependencies": {
+        "memory-pager": "^1.0.2"
+      }
+    },
     "node_modules/statuses": {
       "version": "2.0.2",
       "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz",
@@ -2201,6 +2372,18 @@
         "node": ">=0.6"
       }
     },
+    "node_modules/tr46": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmmirror.com/tr46/-/tr46-5.1.1.tgz",
+      "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+      "license": "MIT",
+      "dependencies": {
+        "punycode": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/type-is": {
       "version": "2.0.1",
       "resolved": "https://registry.npmmirror.com/type-is/-/type-is-2.0.1.tgz",
@@ -2254,6 +2437,28 @@
         "node": ">= 8"
       }
     },
+    "node_modules/webidl-conversions": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+      "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/whatwg-url": {
+      "version": "14.2.0",
+      "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-14.2.0.tgz",
+      "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+      "license": "MIT",
+      "dependencies": {
+        "tr46": "^5.1.0",
+        "webidl-conversions": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/which": {
       "version": "2.0.2",
       "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",

+ 2 - 1
server/package.json

@@ -17,6 +17,7 @@
     "dayjs": "^1.11.19",
     "dotenv": "^17.2.3",
     "express": "^5.2.1",
-    "express-ws": "^5.0.2"
+    "express-ws": "^5.0.2",
+    "mongoose": "^9.6.2"
   }
 }

+ 58 - 0
server/routes/games.js

@@ -117,6 +117,64 @@ router.get('/bet_solution', (req, res) => {
   });
 });
 
+router.get('/orders', (req, res) => {
+  const { limit } = req.query;
+  Games.getOrders({ limit })
+  .then(orders => {
+    res.sendSuccess(orders);
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+router.delete('/orders/:id', (req, res) => {
+  const { id } = req.params;
+  Games.removeOrder(id)
+  .then(order => {
+    res.sendSuccess(order);
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+router.get('/get_polymarket_orders/open', (req, res) => {
+  const {
+    id,
+    market,
+    asset_id,
+    assetId,
+    only_first_page,
+    next_cursor,
+  } = req.query;
+
+  Games.getOpenOrders({
+    id,
+    market,
+    asset_id: asset_id || assetId,
+    only_first_page: only_first_page === true || only_first_page === 'true',
+    next_cursor,
+  })
+  .then(orders => {
+    res.sendSuccess(orders);
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
+router.get('/get_polymarket_order/:orderID', (req, res) => {
+  const { orderID } = req.params;
+  Games.getOrder({ orderID })
+  .then(order => {
+    res.sendSuccess(order);
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
 router.get('/get_polymarket_balance_allowance/:wallet', (req, res) => {
   const { wallet } = req.params;
   Games.getPolymarketBalanceAllowance({ wallet })

+ 11 - 5
web/src/main.vue

@@ -1,7 +1,7 @@
 <script setup>
 import { ref, watch } from 'vue';
 import { RouterView, useRoute, useRouter } from 'vue-router';
-import { AccountBookOutlined, LogoutOutlined, TrophyOutlined, WalletOutlined } from '@ant-design/icons-vue';
+import { AccountBookOutlined, FileSearchOutlined, LogoutOutlined, TrophyOutlined, WalletOutlined } from '@ant-design/icons-vue';
 import { message } from 'ant-design-vue';
 import { authState, logout } from '@/stores/auth';
 
@@ -42,11 +42,11 @@ const handleLogout = async () => {
         </template>
         策略
       </a-menu-item>
-      <a-menu-item key="games">
+      <a-menu-item key="orders">
         <template #icon>
-          <trophy-outlined />
+          <file-search-outlined />
         </template>
-        比赛
+        订单
       </a-menu-item>
       <a-menu-item key="wallet">
         <template #icon>
@@ -54,6 +54,12 @@ const handleLogout = async () => {
         </template>
         钱包
       </a-menu-item>
+      <a-menu-item key="games">
+        <template #icon>
+          <trophy-outlined />
+        </template>
+        比赛
+      </a-menu-item>
     </a-menu>
 
     <div class="header-actions">
@@ -68,7 +74,7 @@ const handleLogout = async () => {
   </header>
 
   <router-view v-slot="{ Component }">
-    <keep-alive :include="['games', 'leagues', 'locales', 'wallet']">
+    <keep-alive :include="['wallet', 'games', 'leagues', 'locales']">
       <component :is="Component" />
     </keep-alive>
   </router-view>

+ 6 - 0
web/src/router/index.js

@@ -3,6 +3,7 @@ import HomeView from '@/views/home.vue';
 import GamesView from '@/views/games.vue';
 import LeaguesView from '@/views/leagues.vue';
 import LocalesView from '@/views/locales.vue';
+import OrdersView from '@/views/orders.vue';
 import WalletView from '@/views/wallet.vue';
 import LoginView from '@/views/login.vue';
 import { authState, checkAuth } from '@/stores/auth';
@@ -45,6 +46,11 @@ const router = createRouter({
       name: 'wallet',
       component: WalletView
     },
+    {
+      path: '/orders',
+      name: 'orders',
+      component: OrdersView
+    },
   ],
 });
 

+ 2 - 0
web/src/views/home.vue

@@ -72,6 +72,8 @@ const betSolution = (sid, stake=0) => {
   .then(res => {
     if (res.data.statusCode === 200) {
       console.log('betSolution result', res.data.data);
+      const orderID = res.data.data?.betOrder?.polymarketOrderID;
+      message.success(orderID ? `订单创建成功:${orderID}` : '订单创建成功');
     }
     else {
       throw new Error(res.data.message);

+ 244 - 0
web/src/views/orders.vue

@@ -0,0 +1,244 @@
+<script setup>
+import dayjs from 'dayjs';
+import { computed, onMounted, ref } from 'vue';
+import { message } from 'ant-design-vue';
+import { DeleteOutlined, ReloadOutlined } from '@ant-design/icons-vue';
+import api from '@/libs/api';
+
+const loading = ref(false);
+const deletingId = ref('');
+const orders = ref([]);
+
+const columns = [
+  { title: '状态', dataIndex: 'status', key: 'status', width: 110 },
+  { title: '方向', dataIndex: 'side', key: 'side', width: 90 },
+  { title: '价格(pUSD)', dataIndex: 'price', key: 'price', width: 110 },
+  { title: '数量', dataIndex: 'size', key: 'size', width: 100 },
+  { title: '投入(pUSD)', dataIndex: 'stake', key: 'stake', width: 120 },
+  { title: '收益率', dataIndex: 'profitRate', key: 'profitRate', width: 110 },
+  { title: '订单ID', dataIndex: 'polymarketOrderID', key: 'polymarketOrderID', width: 150 },
+  { title: '市场', dataIndex: 'market', key: 'market', width: 150 },
+  { title: 'Asset ID', dataIndex: 'assetId', key: 'assetId', width: 150 },
+  { title: 'SID', dataIndex: 'sid', key: 'sid', width: 90 },
+  { title: '时间', dataIndex: 'createdAt', key: 'createdAt', width: 170 },
+  { title: '操作', dataIndex: 'actions', key: 'actions', width: 90, fixed: 'right' },
+];
+
+const tableRows = computed(() => {
+  return orders.value.map(item => {
+    const polymarketOrder = item.polymarketOrder ?? {};
+    const cpr = item.cpr ?? [];
+    const polymarketCpr = cpr.find(row => row?.p === 'polymarket') ?? {};
+    const polymarketInfo = Array.isArray(item.iorsInfo)
+      ? item.iorsInfo[cpr.findIndex(row => row?.p === 'polymarket')] ?? {}
+      : {};
+
+    return {
+      ...item,
+      key: item._id,
+      side: item.side || polymarketOrder.side || polymarketCpr.side || '-',
+      price: polymarketOrder.price ?? polymarketCpr.bid_ex ?? '-',
+      size: item.size ?? polymarketOrder.size ?? polymarketOrder.original_size ?? '-',
+      profitRate: item.sol?.win_profit_rate,
+      market: polymarketInfo.market || polymarketOrder.market || '-',
+      assetId: polymarketInfo.asset_id || polymarketOrder.asset_id || '-',
+    };
+  });
+});
+
+const formatDateTime = (value) => {
+  return value ? dayjs(value).format('YYYY-MM-DD HH:mm:ss') : '-';
+};
+
+const formatNumber = (value, precision = 3) => {
+  const number = Number(value);
+  if (!Number.isFinite(number)) {
+    return '-';
+  }
+  return number.toFixed(precision);
+};
+
+const formatProfitRate = (value) => {
+  const number = Number(value);
+  return Number.isFinite(number) ? `${number.toFixed(3)}%` : '-';
+};
+
+const formatJson = (value) => {
+  return JSON.stringify(value ?? {}, null, 2);
+};
+
+const getStatusColor = (status) => {
+  const value = String(status || '').toLowerCase();
+  if (value === 'created' || value === 'live' || value === 'open') {
+    return 'blue';
+  }
+  if (value === 'matched' || value === 'filled') {
+    return 'green';
+  }
+  if (value === 'cancelled' || value === 'canceled' || value === 'failed') {
+    return 'red';
+  }
+  return 'default';
+};
+
+const getProfitColor = (value) => {
+  const number = Number(value);
+  if (number > 0) {
+    return 'green';
+  }
+  if (number < 0) {
+    return 'red';
+  }
+  return 'default';
+};
+
+const refresh = () => {
+  loading.value = true;
+  api.get('/api/games/orders')
+  .then(res => {
+    if (res.data.statusCode === 200) {
+      orders.value = Array.isArray(res.data.data) ? res.data.data : [];
+    }
+    else {
+      throw new Error(res.data.message);
+    }
+  })
+  .catch(err => {
+    message.error(err.response?.data?.message ?? err.message);
+    console.error(err);
+  })
+  .finally(() => {
+    loading.value = false;
+  });
+};
+
+const removeOrder = (record) => {
+  if (!record?._id) {
+    return;
+  }
+
+  deletingId.value = record._id;
+  api.delete(`/api/games/orders/${record._id}`)
+  .then(res => {
+    if (res.data.statusCode === 200) {
+      orders.value = orders.value.filter(item => item._id !== record._id);
+      message.success('订单已删除');
+    }
+    else {
+      throw new Error(res.data.message);
+    }
+  })
+  .catch(err => {
+    message.error(err.response?.data?.message ?? err.message);
+    console.error(err);
+  })
+  .finally(() => {
+    deletingId.value = '';
+  });
+};
+
+onMounted(() => {
+  refresh();
+});
+</script>
+
+<template>
+  <a-page-header title="订单">
+    <template #extra>
+      <a-button :loading="loading" @click="refresh">
+        <template #icon>
+          <reload-outlined />
+        </template>
+        刷新
+      </a-button>
+    </template>
+  </a-page-header>
+
+  <div class="orders-container">
+    <a-table
+      :columns="columns"
+      :data-source="tableRows"
+      :loading="loading"
+      :scroll="{ x: 1800, y: 'calc(100vh - 215px)' }"
+      size="small"
+      bordered
+    >
+      <template #bodyCell="{ column, record, text }">
+        <template v-if="column.key === 'createdAt'">
+          {{ formatDateTime(text) }}
+        </template>
+        <template v-else-if="column.key === 'status'">
+          <a-tag :color="getStatusColor(text)">
+            {{ text || '-' }}
+          </a-tag>
+        </template>
+        <template v-else-if="column.key === 'sid' || column.key === 'polymarketOrderID' || column.key === 'market' || column.key === 'assetId'">
+          <a-tooltip :title="text">
+            <span class="mono ellipsis">{{ text || '-' }}</span>
+          </a-tooltip>
+        </template>
+        <template v-else-if="column.key === 'price'">
+          {{ formatNumber(text) }}
+        </template>
+        <template v-else-if="column.key === 'stake'">
+          {{ formatNumber(text, 2) }}
+        </template>
+        <template v-else-if="column.key === 'profitRate'">
+          <a-tag :color="getProfitColor(text)">
+            {{ formatProfitRate(text) }}
+          </a-tag>
+        </template>
+        <template v-else-if="column.key === 'actions'">
+          <a-popconfirm
+            title="确认删除这条订单记录?"
+            ok-text="删除"
+            cancel-text="取消"
+            @confirm="removeOrder(record)"
+          >
+            <a-button
+              danger
+              size="small"
+              :loading="deletingId === record._id"
+            >
+              <template #icon>
+                <delete-outlined />
+              </template>
+            </a-button>
+          </a-popconfirm>
+        </template>
+      </template>
+      <template #expandedRowRender="{ record }">
+        <pre>{{ formatJson(record) }}</pre>
+      </template>
+    </a-table>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.orders-container {
+  height: calc(100vh - 126px);
+  padding: 15px;
+  border-top: 1px solid rgba(5, 5, 5, 0.06);
+}
+.mono {
+  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+  font-size: 12px;
+  word-break: break-all;
+}
+.ellipsis {
+  display: inline-block;
+  max-width: 100%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  vertical-align: bottom;
+}
+pre {
+  margin: 0;
+  padding: 12px;
+  overflow: auto;
+  background: #f7f8fa;
+  border: 1px solid rgba(5, 5, 5, 0.06);
+  border-radius: 6px;
+}
+</style>