polycopy 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +60 -127
- package/dist/index.js +406 -204
- package/dist/{init-Bs-N_AZi.js → process-lock-y_yjnMCr.js} +184 -220
- package/package.json +4 -5
- package/dist/config.js +0 -13
- package/dist/paths-CEjGES8j.js +0 -20
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
const
|
|
2
|
+
const processLock = require("./process-lock-y_yjnMCr.js");
|
|
3
3
|
const grammy = require("grammy");
|
|
4
|
+
const log4js = require("log4js");
|
|
5
|
+
const path = require("path");
|
|
4
6
|
const wallet = require("@ethersproject/wallet");
|
|
5
7
|
const clobClient = require("@polymarket/clob-client");
|
|
6
8
|
const builderSigningSdk = require("@polymarket/builder-signing-sdk");
|
|
@@ -11,7 +13,6 @@ const builderRelayerClient = require("@polymarket/builder-relayer-client");
|
|
|
11
13
|
const setPromiseInterval = require("set-promise-interval");
|
|
12
14
|
const child_process = require("child_process");
|
|
13
15
|
const fs = require("fs");
|
|
14
|
-
const paths = require("./paths-CEjGES8j.js");
|
|
15
16
|
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
16
17
|
function _interopNamespace(e) {
|
|
17
18
|
if (e && e.__esModule) return e;
|
|
@@ -30,8 +31,114 @@ function _interopNamespace(e) {
|
|
|
30
31
|
n.default = e;
|
|
31
32
|
return Object.freeze(n);
|
|
32
33
|
}
|
|
34
|
+
const log4js__default = /* @__PURE__ */ _interopDefault(log4js);
|
|
35
|
+
const path__namespace = /* @__PURE__ */ _interopNamespace(path);
|
|
33
36
|
const setPromiseInterval__default = /* @__PURE__ */ _interopDefault(setPromiseInterval);
|
|
34
37
|
const fs__namespace = /* @__PURE__ */ _interopNamespace(fs);
|
|
38
|
+
const LOG_FILE = path__namespace.join(processLock.LOGS_DIR, "polycopy.log");
|
|
39
|
+
path__namespace.join(processLock.LOGS_DIR, "polycopy");
|
|
40
|
+
log4js__default.default.configure({
|
|
41
|
+
appenders: {
|
|
42
|
+
file: {
|
|
43
|
+
type: "dateFile",
|
|
44
|
+
filename: LOG_FILE,
|
|
45
|
+
pattern: "yyyy-MM-dd",
|
|
46
|
+
alwaysIncludePattern: true,
|
|
47
|
+
compress: false,
|
|
48
|
+
keepFileExt: true,
|
|
49
|
+
maxLogSize: 2 * 1024 * 1024,
|
|
50
|
+
backups: 10,
|
|
51
|
+
layout: { type: "pattern", pattern: "[%d{yyyy-MM-dd hh:mm:ss}] %m" }
|
|
52
|
+
},
|
|
53
|
+
stdout: {
|
|
54
|
+
type: "stdout",
|
|
55
|
+
layout: { type: "pattern", pattern: "[%d{yyyy-MM-dd hh:mm:ss}] %m" }
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
categories: {
|
|
59
|
+
default: { appenders: ["stdout", "file"], level: "info" }
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const log4jsLogger = log4js__default.default.getLogger("polycopy");
|
|
63
|
+
const formatMessage = (message, args) => args.length > 0 ? `${message} ${args.join(" ")}` : message;
|
|
64
|
+
class Logger {
|
|
65
|
+
initialized = false;
|
|
66
|
+
/**
|
|
67
|
+
* 初始化日志模块(输出日志路径信息)
|
|
68
|
+
*/
|
|
69
|
+
init() {
|
|
70
|
+
if (this.initialized) return;
|
|
71
|
+
this.initialized = true;
|
|
72
|
+
console.log(`
|
|
73
|
+
📁 日志目录: ${processLock.LOGS_DIR}`);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 输出信息日志(前后自动添加空行)
|
|
77
|
+
*/
|
|
78
|
+
info(message, ...args) {
|
|
79
|
+
log4jsLogger.info(formatMessage(message, args));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 输出成功日志(前后自动添加空行)
|
|
83
|
+
*/
|
|
84
|
+
success(message, ...args) {
|
|
85
|
+
log4jsLogger.info(`✅ ${formatMessage(message, args)}`);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 输出警告日志(前后自动添加空行)
|
|
89
|
+
*/
|
|
90
|
+
warning(message, ...args) {
|
|
91
|
+
log4jsLogger.warn(`⚠️ ${formatMessage(message, args)}`);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 输出错误日志(前后自动添加空行)
|
|
95
|
+
*/
|
|
96
|
+
error(message, ...args) {
|
|
97
|
+
log4jsLogger.error(`❌ ${formatMessage(message, args)}`);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 输出章节标题(前后自动添加空行)
|
|
101
|
+
*/
|
|
102
|
+
section(title) {
|
|
103
|
+
log4jsLogger.info(`=== ${title} ===`);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 输出列表项(不添加空行,用于连续输出)
|
|
107
|
+
*/
|
|
108
|
+
item(message, indent = 0) {
|
|
109
|
+
const indentStr = " ".repeat(indent);
|
|
110
|
+
const formatted = `${indentStr}${message}`;
|
|
111
|
+
log4jsLogger.info(formatted);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 输出普通文本(不添加空行,用于连续输出)
|
|
115
|
+
*/
|
|
116
|
+
text(message, ...args) {
|
|
117
|
+
log4jsLogger.info(formatMessage(message, args));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 输出空行
|
|
121
|
+
*/
|
|
122
|
+
blank() {
|
|
123
|
+
console.log("");
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 输出带前缀的信息(不添加空行)
|
|
127
|
+
* 用于在同一组日志中输出多行信息
|
|
128
|
+
*/
|
|
129
|
+
line(prefix, message, ...args) {
|
|
130
|
+
const formatted = args.length > 0 ? ` ${prefix} ${message} ${args.join(" ")}` : ` ${prefix} ${message}`;
|
|
131
|
+
log4jsLogger.info(formatted);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 刷新日志输出(用于进程退出前)
|
|
135
|
+
*/
|
|
136
|
+
async flush(timeoutMs = 1e3) {
|
|
137
|
+
await log4js__default.default.shutdown();
|
|
138
|
+
await new Promise((resolve) => setTimeout(resolve, timeoutMs));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const logger = new Logger();
|
|
35
142
|
function parseSignalMetadata(messageText) {
|
|
36
143
|
const match = messageText.match(/🤖\s*({.*})/s);
|
|
37
144
|
if (!match) {
|
|
@@ -41,7 +148,7 @@ function parseSignalMetadata(messageText) {
|
|
|
41
148
|
const jsonStr = match[1];
|
|
42
149
|
const metadata = JSON.parse(jsonStr);
|
|
43
150
|
if (!metadata.eventId || !metadata.assetId || !metadata.outcome || !metadata.endTime) {
|
|
44
|
-
|
|
151
|
+
logger.error("解析失败: 缺少必需字段", {
|
|
45
152
|
hasEventId: !!metadata.eventId,
|
|
46
153
|
hasAssetId: !!metadata.assetId,
|
|
47
154
|
hasOutcome: !!metadata.outcome,
|
|
@@ -50,14 +157,14 @@ function parseSignalMetadata(messageText) {
|
|
|
50
157
|
return null;
|
|
51
158
|
}
|
|
52
159
|
if (metadata.outcome !== "Up" && metadata.outcome !== "Down") {
|
|
53
|
-
|
|
160
|
+
logger.error("解析失败: outcome 值无效", {
|
|
54
161
|
outcome: metadata.outcome,
|
|
55
162
|
expected: "Up 或 Down"
|
|
56
163
|
});
|
|
57
164
|
return null;
|
|
58
165
|
}
|
|
59
166
|
if (typeof metadata.endTime !== "number" || metadata.endTime <= 0) {
|
|
60
|
-
|
|
167
|
+
logger.error("解析失败: endTime 值无效", {
|
|
61
168
|
endTime: metadata.endTime,
|
|
62
169
|
expected: "毫秒级时间戳"
|
|
63
170
|
});
|
|
@@ -65,9 +172,9 @@ function parseSignalMetadata(messageText) {
|
|
|
65
172
|
}
|
|
66
173
|
return metadata;
|
|
67
174
|
} catch (error) {
|
|
68
|
-
|
|
175
|
+
logger.error("解析信号元数据失败:", error);
|
|
69
176
|
if (error instanceof Error) {
|
|
70
|
-
|
|
177
|
+
logger.line("", `错误详情: ${error.message}`);
|
|
71
178
|
}
|
|
72
179
|
return null;
|
|
73
180
|
}
|
|
@@ -97,12 +204,12 @@ class TelegramListener {
|
|
|
97
204
|
await this.handleMyChatMemberUpdate(ctx);
|
|
98
205
|
});
|
|
99
206
|
this.bot.catch((err) => {
|
|
100
|
-
|
|
207
|
+
logger.error("Telegram Bot 错误:", err);
|
|
101
208
|
if (err.error) {
|
|
102
|
-
|
|
209
|
+
logger.line("", `错误详情: ${err.error}`);
|
|
103
210
|
}
|
|
104
211
|
if (err.ctx) {
|
|
105
|
-
|
|
212
|
+
logger.line("", `上下文信息:`, {
|
|
106
213
|
chatId: err.ctx.chat?.id,
|
|
107
214
|
messageId: err.ctx.message?.message_id
|
|
108
215
|
});
|
|
@@ -169,28 +276,28 @@ class TelegramListener {
|
|
|
169
276
|
reason = "需要管理员权限才能监听";
|
|
170
277
|
}
|
|
171
278
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
279
|
+
logger.info(`📢 频道状态更新: ${chatTitle}`);
|
|
280
|
+
logger.line("", action);
|
|
281
|
+
logger.line(
|
|
175
282
|
"",
|
|
176
283
|
`当前状态: ${canListen ? "✅ 可监听预测消息" : "❌ 无法监听预测消息"}`
|
|
177
284
|
);
|
|
178
285
|
if (reason) {
|
|
179
|
-
|
|
286
|
+
logger.line("", `原因: ${reason}`);
|
|
180
287
|
}
|
|
181
288
|
}
|
|
182
289
|
if (isAdmin && !this.targetChatIds.has(chatId)) {
|
|
183
290
|
this.targetChatIds.add(chatId);
|
|
184
291
|
await this.saveChatIds();
|
|
185
|
-
|
|
292
|
+
logger.line("📡", "已自动开始监听此频道");
|
|
186
293
|
}
|
|
187
294
|
if ((isRemoved || !isAdmin) && this.targetChatIds.has(chatId)) {
|
|
188
295
|
this.targetChatIds.delete(chatId);
|
|
189
296
|
await this.saveChatIds();
|
|
190
297
|
if (isRemoved) {
|
|
191
|
-
|
|
298
|
+
logger.line("⏹️", "已停止监听此频道(Bot 已被移除)");
|
|
192
299
|
} else {
|
|
193
|
-
|
|
300
|
+
logger.line("⏹️", "已停止监听此频道(Bot 不再是管理员)");
|
|
194
301
|
}
|
|
195
302
|
}
|
|
196
303
|
}
|
|
@@ -205,7 +312,7 @@ class TelegramListener {
|
|
|
205
312
|
const chatIds = Array.from(this.targetChatIds);
|
|
206
313
|
await this.onChatIdsChangedCallback(chatIds);
|
|
207
314
|
} catch (error) {
|
|
208
|
-
|
|
315
|
+
logger.error("保存频道 ID 列表时出错:", error);
|
|
209
316
|
}
|
|
210
317
|
}
|
|
211
318
|
/**
|
|
@@ -234,9 +341,9 @@ class TelegramListener {
|
|
|
234
341
|
}
|
|
235
342
|
if (isChannel && !this.targetChatIds.has(chatId)) {
|
|
236
343
|
this.targetChatIds.add(chatId);
|
|
237
|
-
|
|
344
|
+
logger.info(`➕ 自动添加频道到监听列表: ${chatTitle} (ID: ${chatId})`);
|
|
238
345
|
await this.saveChatIds();
|
|
239
|
-
|
|
346
|
+
logger.line("💾", "已保存到配置文件");
|
|
240
347
|
}
|
|
241
348
|
if (!isChannel) {
|
|
242
349
|
return;
|
|
@@ -250,13 +357,13 @@ class TelegramListener {
|
|
|
250
357
|
const emojiIndex = messageText.indexOf("🤖");
|
|
251
358
|
const jsonStart = emojiIndex + 1;
|
|
252
359
|
const jsonEnd = Math.min(jsonStart + 200, messageText.length);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
360
|
+
logger.warning(`解析预测消息失败:`);
|
|
361
|
+
logger.line("", `频道 ID: ${chatId}`);
|
|
362
|
+
logger.line(
|
|
256
363
|
"",
|
|
257
364
|
`消息片段: ${messageText.substring(emojiIndex, jsonEnd)}${messageText.length > jsonEnd ? "..." : ""}`
|
|
258
365
|
);
|
|
259
|
-
|
|
366
|
+
logger.line("", "提示: 请检查消息格式是否正确");
|
|
260
367
|
}
|
|
261
368
|
return;
|
|
262
369
|
}
|
|
@@ -271,7 +378,7 @@ class TelegramListener {
|
|
|
271
378
|
try {
|
|
272
379
|
await this.onSignalCallback(parsedSignal);
|
|
273
380
|
} catch (error) {
|
|
274
|
-
|
|
381
|
+
logger.error("处理信号时出错:", error);
|
|
275
382
|
}
|
|
276
383
|
}
|
|
277
384
|
}
|
|
@@ -299,15 +406,15 @@ class TelegramListener {
|
|
|
299
406
|
*/
|
|
300
407
|
async start(onReady) {
|
|
301
408
|
try {
|
|
302
|
-
|
|
409
|
+
logger.info("正在启动 Bot...");
|
|
303
410
|
const botInfo = await this.bot.api.getMe();
|
|
304
411
|
const botId = botInfo.id;
|
|
305
|
-
|
|
412
|
+
logger.info(`Bot 信息: @${botInfo.username} (ID: ${botId})`);
|
|
306
413
|
await this.checkAndDisplayChannels(botId);
|
|
307
414
|
if (onReady) {
|
|
308
415
|
await onReady();
|
|
309
416
|
}
|
|
310
|
-
|
|
417
|
+
logger.info("开始监听预测消息...");
|
|
311
418
|
await this.bot.start({
|
|
312
419
|
drop_pending_updates: true,
|
|
313
420
|
allowed_updates: [
|
|
@@ -317,7 +424,7 @@ class TelegramListener {
|
|
|
317
424
|
]
|
|
318
425
|
});
|
|
319
426
|
} catch (error) {
|
|
320
|
-
|
|
427
|
+
logger.error("启动 Bot 失败:", error);
|
|
321
428
|
throw error;
|
|
322
429
|
}
|
|
323
430
|
}
|
|
@@ -328,14 +435,14 @@ class TelegramListener {
|
|
|
328
435
|
async checkAndDisplayChannels(botId) {
|
|
329
436
|
const chatIds = Array.from(this.targetChatIds);
|
|
330
437
|
if (chatIds.length === 0) {
|
|
331
|
-
|
|
332
|
-
|
|
438
|
+
logger.info(`📋 当前没有保存的频道配置`);
|
|
439
|
+
logger.line(
|
|
333
440
|
"",
|
|
334
441
|
"提示: 当 Bot 被添加为频道管理员时,会自动添加到监听列表"
|
|
335
442
|
);
|
|
336
443
|
return;
|
|
337
444
|
}
|
|
338
|
-
|
|
445
|
+
logger.info(`📋 检查 Bot 所在的频道:`);
|
|
339
446
|
const validChannels = [];
|
|
340
447
|
const invalidChannels = [];
|
|
341
448
|
for (const chatId of chatIds) {
|
|
@@ -367,43 +474,43 @@ class TelegramListener {
|
|
|
367
474
|
const adminChannels = validChannels.filter((c) => c.isAdmin);
|
|
368
475
|
const nonAdminChannels = validChannels.filter((c) => !c.isAdmin);
|
|
369
476
|
if (adminChannels.length > 0) {
|
|
370
|
-
|
|
477
|
+
logger.info(`📡 可监听频道 (${adminChannels.length} 个):`);
|
|
371
478
|
adminChannels.forEach((channel, index) => {
|
|
372
|
-
|
|
479
|
+
logger.item(
|
|
373
480
|
`${index + 1}. ${channel.title} (ID: ${channel.id}) 👑 管理员`,
|
|
374
481
|
1
|
|
375
482
|
);
|
|
376
483
|
});
|
|
377
484
|
}
|
|
378
485
|
if (nonAdminChannels.length > 0) {
|
|
379
|
-
|
|
486
|
+
logger.warning(`无法监听频道 (${nonAdminChannels.length} 个):`);
|
|
380
487
|
nonAdminChannels.forEach((channel, index) => {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
488
|
+
logger.item(`${index + 1}. ${channel.title}`, 1);
|
|
489
|
+
logger.item(`ID: ${channel.id}`, 2);
|
|
490
|
+
logger.item(`状态: 🚫 非管理员(无法接收预测消息)`, 2);
|
|
491
|
+
logger.item(`原因: 需要管理员权限才能监听`, 2);
|
|
385
492
|
});
|
|
386
493
|
nonAdminChannels.forEach((channel) => {
|
|
387
494
|
this.targetChatIds.delete(channel.id);
|
|
388
495
|
});
|
|
389
496
|
if (nonAdminChannels.length > 0) {
|
|
390
497
|
await this.saveChatIds();
|
|
391
|
-
|
|
498
|
+
logger.line("💾", "已清理无法监听的频道并保存配置");
|
|
392
499
|
}
|
|
393
500
|
}
|
|
394
501
|
}
|
|
395
502
|
if (invalidChannels.length > 0) {
|
|
396
|
-
|
|
503
|
+
logger.error(`无效频道 (${invalidChannels.length} 个):`);
|
|
397
504
|
invalidChannels.forEach((channel, index) => {
|
|
398
|
-
|
|
399
|
-
|
|
505
|
+
logger.item(`${index + 1}. ID: ${channel.id}`, 1);
|
|
506
|
+
logger.item(`原因: ${channel.reason}`, 2);
|
|
400
507
|
});
|
|
401
508
|
invalidChannels.forEach((channel) => {
|
|
402
509
|
this.targetChatIds.delete(channel.id);
|
|
403
510
|
});
|
|
404
511
|
if (invalidChannels.length > 0) {
|
|
405
512
|
await this.saveChatIds();
|
|
406
|
-
|
|
513
|
+
logger.line("💾", "已清理无效频道并保存配置");
|
|
407
514
|
}
|
|
408
515
|
}
|
|
409
516
|
}
|
|
@@ -439,13 +546,13 @@ class TelegramService {
|
|
|
439
546
|
this.bot = new grammy.Bot(botToken);
|
|
440
547
|
this.listener = new TelegramListener(this.bot, targetChatIds);
|
|
441
548
|
if (!adminChatId) {
|
|
442
|
-
|
|
549
|
+
logger.warning("未配置管理员 Chat ID,通知功能已禁用");
|
|
443
550
|
this.notifyEnabled = false;
|
|
444
551
|
return;
|
|
445
552
|
}
|
|
446
553
|
this.adminChatId = adminChatId;
|
|
447
554
|
this.notifyEnabled = true;
|
|
448
|
-
|
|
555
|
+
logger.info("管理员通知已启用");
|
|
449
556
|
}
|
|
450
557
|
/**
|
|
451
558
|
* 设置频道 ID 列表变更回调
|
|
@@ -511,7 +618,7 @@ ${details}` : message;
|
|
|
511
618
|
});
|
|
512
619
|
} catch (error) {
|
|
513
620
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
514
|
-
|
|
621
|
+
logger.error(`发送通知失败: ${errorMsg}`);
|
|
515
622
|
}
|
|
516
623
|
}
|
|
517
624
|
}
|
|
@@ -528,11 +635,11 @@ class ClobClientWrapper {
|
|
|
528
635
|
return;
|
|
529
636
|
}
|
|
530
637
|
const wallet$1 = new wallet.Wallet(config.privateKey);
|
|
531
|
-
|
|
638
|
+
logger.info(`钱包地址: ${wallet$1.address}`);
|
|
532
639
|
const tempClient = new clobClient.ClobClient(CLOB_HOST, clobClient.Chain.POLYGON, wallet$1);
|
|
533
640
|
this.patchClient(tempClient);
|
|
534
641
|
const creds = await tempClient.deriveApiKey();
|
|
535
|
-
|
|
642
|
+
logger.success("API 凭证已获取");
|
|
536
643
|
let builderConfig;
|
|
537
644
|
if (config.builderCreds) {
|
|
538
645
|
builderConfig = new builderSigningSdk.BuilderConfig({
|
|
@@ -556,7 +663,7 @@ class ClobClientWrapper {
|
|
|
556
663
|
);
|
|
557
664
|
this.patchClient(this.client);
|
|
558
665
|
this.initialized = true;
|
|
559
|
-
|
|
666
|
+
logger.success("ClobClient 初始化完成");
|
|
560
667
|
}
|
|
561
668
|
/**
|
|
562
669
|
* 魔法操作:clob-client 请求出错不抛异常,只返回 { error: ... }
|
|
@@ -632,13 +739,13 @@ class ClobClientWrapper {
|
|
|
632
739
|
clobClient.OrderType.GTC
|
|
633
740
|
);
|
|
634
741
|
if (response.success) {
|
|
635
|
-
|
|
742
|
+
logger.success(`买单已提交: ${response.orderID}`);
|
|
636
743
|
return {
|
|
637
744
|
success: true,
|
|
638
745
|
orderId: response.orderID
|
|
639
746
|
};
|
|
640
747
|
} else {
|
|
641
|
-
|
|
748
|
+
logger.error(`买单提交失败: ${response.errorMsg}`);
|
|
642
749
|
return {
|
|
643
750
|
success: false,
|
|
644
751
|
orderId: "",
|
|
@@ -647,7 +754,7 @@ class ClobClientWrapper {
|
|
|
647
754
|
}
|
|
648
755
|
} catch (error) {
|
|
649
756
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
650
|
-
|
|
757
|
+
logger.error(`买单提交失败: ${errorMsg}`);
|
|
651
758
|
return {
|
|
652
759
|
success: false,
|
|
653
760
|
orderId: "",
|
|
@@ -709,11 +816,11 @@ class ClobClientWrapper {
|
|
|
709
816
|
const client = this.ensureInitialized();
|
|
710
817
|
try {
|
|
711
818
|
await client.cancelOrder({ orderID: orderId });
|
|
712
|
-
|
|
819
|
+
logger.info(`订单已取消: ${orderId}`);
|
|
713
820
|
return true;
|
|
714
821
|
} catch (error) {
|
|
715
822
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
716
|
-
|
|
823
|
+
logger.error(`取消订单失败: ${errorMsg}`);
|
|
717
824
|
return false;
|
|
718
825
|
}
|
|
719
826
|
}
|
|
@@ -759,7 +866,7 @@ class RelayClientWrapper {
|
|
|
759
866
|
builderRelayerClient.RelayerTxType.SAFE
|
|
760
867
|
);
|
|
761
868
|
this.initialized = true;
|
|
762
|
-
|
|
869
|
+
logger.success("RelayClient 初始化完成");
|
|
763
870
|
}
|
|
764
871
|
/**
|
|
765
872
|
* 检查是否已初始化
|
|
@@ -767,15 +874,7 @@ class RelayClientWrapper {
|
|
|
767
874
|
isInitialized() {
|
|
768
875
|
return this.initialized;
|
|
769
876
|
}
|
|
770
|
-
|
|
771
|
-
* 执行 Redeem
|
|
772
|
-
* @param conditionId 条件 ID
|
|
773
|
-
* @returns 交易哈希
|
|
774
|
-
*/
|
|
775
|
-
async redeem(conditionId) {
|
|
776
|
-
if (!this.relayClient || !this.initialized) {
|
|
777
|
-
throw new Error("RelayClient 未初始化");
|
|
778
|
-
}
|
|
877
|
+
buildRedeemTransaction(conditionId) {
|
|
779
878
|
const calldata = viem.encodeFunctionData({
|
|
780
879
|
abi: [
|
|
781
880
|
{
|
|
@@ -794,14 +893,27 @@ class RelayClientWrapper {
|
|
|
794
893
|
functionName: "redeemPositions",
|
|
795
894
|
args: [USDC_ADDRESS, viem.zeroHash, conditionId, [1n, 2n]]
|
|
796
895
|
});
|
|
797
|
-
|
|
896
|
+
return {
|
|
798
897
|
to: CTF_ADDRESS,
|
|
799
898
|
value: "0",
|
|
800
899
|
data: calldata
|
|
801
900
|
};
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* 批量执行 Redeem
|
|
904
|
+
* @param conditionIds 条件 ID 列表
|
|
905
|
+
* @returns 交易哈希
|
|
906
|
+
*/
|
|
907
|
+
async redeemBatch(conditionIds) {
|
|
908
|
+
if (!this.relayClient || !this.initialized) {
|
|
909
|
+
throw new Error("RelayClient 未初始化");
|
|
910
|
+
}
|
|
911
|
+
const transactions = conditionIds.map(
|
|
912
|
+
(conditionId) => this.buildRedeemTransaction(conditionId)
|
|
913
|
+
);
|
|
802
914
|
const response = await this.relayClient.execute(
|
|
803
|
-
|
|
804
|
-
|
|
915
|
+
transactions,
|
|
916
|
+
`Polymarket redeem positions (${transactions.length})`
|
|
805
917
|
);
|
|
806
918
|
const result = await response.wait();
|
|
807
919
|
if (!result) {
|
|
@@ -811,6 +923,14 @@ class RelayClientWrapper {
|
|
|
811
923
|
}
|
|
812
924
|
return result.transactionHash;
|
|
813
925
|
}
|
|
926
|
+
/**
|
|
927
|
+
* 执行 Redeem(兼容单个)
|
|
928
|
+
* @param conditionId 条件 ID
|
|
929
|
+
* @returns 交易哈希
|
|
930
|
+
*/
|
|
931
|
+
async redeem(conditionId) {
|
|
932
|
+
return this.redeemBatch([conditionId]);
|
|
933
|
+
}
|
|
814
934
|
}
|
|
815
935
|
class AssetFilter {
|
|
816
936
|
// endTime -> Set<assetId>
|
|
@@ -838,7 +958,7 @@ class AssetFilter {
|
|
|
838
958
|
this.traded.set(endTime, /* @__PURE__ */ new Set());
|
|
839
959
|
}
|
|
840
960
|
this.traded.get(endTime).add(assetId);
|
|
841
|
-
|
|
961
|
+
logger.info(`已标记 assetId: ${assetId.slice(0, 10)}...`);
|
|
842
962
|
}
|
|
843
963
|
/**
|
|
844
964
|
* 清理过期记录
|
|
@@ -852,7 +972,7 @@ class AssetFilter {
|
|
|
852
972
|
}
|
|
853
973
|
}
|
|
854
974
|
if (cleanedCount > 0) {
|
|
855
|
-
|
|
975
|
+
logger.info(`已清理 ${cleanedCount} 个过期周期的交易记录`);
|
|
856
976
|
}
|
|
857
977
|
}
|
|
858
978
|
}
|
|
@@ -872,12 +992,12 @@ class BalanceCache {
|
|
|
872
992
|
}
|
|
873
993
|
if (this.cache.has(endTime)) {
|
|
874
994
|
const cached = this.cache.get(endTime);
|
|
875
|
-
|
|
995
|
+
logger.info(`使用缓存余额: ${cached} USDC`);
|
|
876
996
|
return cached;
|
|
877
997
|
}
|
|
878
998
|
const balance = await fetchFn();
|
|
879
999
|
this.cache.set(endTime, balance);
|
|
880
|
-
|
|
1000
|
+
logger.info(`获取当前余额: ${balance} USDC`);
|
|
881
1001
|
return balance;
|
|
882
1002
|
}
|
|
883
1003
|
/**
|
|
@@ -886,7 +1006,7 @@ class BalanceCache {
|
|
|
886
1006
|
invalidate() {
|
|
887
1007
|
if (this.cache.size > 0) {
|
|
888
1008
|
this.cache.clear();
|
|
889
|
-
|
|
1009
|
+
logger.info("余额缓存已清除");
|
|
890
1010
|
}
|
|
891
1011
|
}
|
|
892
1012
|
/**
|
|
@@ -901,7 +1021,7 @@ class BalanceCache {
|
|
|
901
1021
|
}
|
|
902
1022
|
}
|
|
903
1023
|
if (cleanedCount > 0) {
|
|
904
|
-
|
|
1024
|
+
logger.info(`已清理 ${cleanedCount} 个过期周期的余额缓存`);
|
|
905
1025
|
}
|
|
906
1026
|
}
|
|
907
1027
|
}
|
|
@@ -918,13 +1038,13 @@ class OrderWatcher {
|
|
|
918
1038
|
async watch(orderId, endTime, getOrderFn) {
|
|
919
1039
|
const timeoutAt = endTime + TIMEOUT_BUFFER;
|
|
920
1040
|
const shortId = orderId.substring(0, 10) + "...";
|
|
921
|
-
|
|
1041
|
+
logger.info(
|
|
922
1042
|
`开始监控订单 ${shortId},超时: ${new Date(timeoutAt).toLocaleString()}`
|
|
923
1043
|
);
|
|
924
1044
|
while (true) {
|
|
925
1045
|
const now = Date.now();
|
|
926
1046
|
if (now >= timeoutAt) {
|
|
927
|
-
|
|
1047
|
+
logger.warning(`订单超时: ${shortId}`);
|
|
928
1048
|
return { status: "TIMEOUT" };
|
|
929
1049
|
}
|
|
930
1050
|
try {
|
|
@@ -932,7 +1052,7 @@ class OrderWatcher {
|
|
|
932
1052
|
const status = order.status.toUpperCase();
|
|
933
1053
|
if (TERMINAL_STATUSES.includes(status)) {
|
|
934
1054
|
const filledSize = parseFloat(order.size_matched || "0");
|
|
935
|
-
|
|
1055
|
+
logger.info(`订单 ${shortId} 终态: ${status},成交: ${filledSize}`);
|
|
936
1056
|
return {
|
|
937
1057
|
status,
|
|
938
1058
|
filledSize,
|
|
@@ -941,7 +1061,7 @@ class OrderWatcher {
|
|
|
941
1061
|
}
|
|
942
1062
|
} catch (error) {
|
|
943
1063
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
944
|
-
|
|
1064
|
+
logger.warning(`查询订单失败: ${errorMsg},继续重试...`);
|
|
945
1065
|
}
|
|
946
1066
|
await this.delay(POLL_INTERVAL);
|
|
947
1067
|
}
|
|
@@ -952,6 +1072,7 @@ class OrderWatcher {
|
|
|
952
1072
|
}
|
|
953
1073
|
const DATA_API_HOST = "https://data-api.polymarket.com";
|
|
954
1074
|
const BASE_INTERVAL = 5 * 60 * 1e3;
|
|
1075
|
+
const DEFAULT_BATCH_SIZE = 5;
|
|
955
1076
|
const MAX_MATCH_TIMES = 3;
|
|
956
1077
|
class Redeemer {
|
|
957
1078
|
// conditionId -> RedeemRecord
|
|
@@ -962,6 +1083,7 @@ class Redeemer {
|
|
|
962
1083
|
state = "stopped";
|
|
963
1084
|
scheduler;
|
|
964
1085
|
redeemFn = null;
|
|
1086
|
+
batchSize = DEFAULT_BATCH_SIZE;
|
|
965
1087
|
funderAddress = "";
|
|
966
1088
|
onRedeemSuccess = null;
|
|
967
1089
|
constructor(scheduler = {
|
|
@@ -983,10 +1105,14 @@ class Redeemer {
|
|
|
983
1105
|
return;
|
|
984
1106
|
}
|
|
985
1107
|
this.redeemFn = redeemFn;
|
|
1108
|
+
const envBatchSize = Number(process.env.POLYCOPY_REDEEM_BATCH_SIZE || "");
|
|
1109
|
+
if (!Number.isNaN(envBatchSize) && envBatchSize > 0) {
|
|
1110
|
+
this.batchSize = Math.floor(envBatchSize);
|
|
1111
|
+
}
|
|
986
1112
|
this.funderAddress = funderAddress;
|
|
987
1113
|
this.onRedeemSuccess = onRedeemSuccess || null;
|
|
988
1114
|
this.state = "running";
|
|
989
|
-
|
|
1115
|
+
logger.success("自动 redeem 已启动");
|
|
990
1116
|
this.startInterval();
|
|
991
1117
|
}
|
|
992
1118
|
/**
|
|
@@ -1034,7 +1160,7 @@ class Redeemer {
|
|
|
1034
1160
|
this.delayTimeoutId = void 0;
|
|
1035
1161
|
}
|
|
1036
1162
|
this.state = "stopped";
|
|
1037
|
-
|
|
1163
|
+
logger.info("自动 redeem 已停止");
|
|
1038
1164
|
}
|
|
1039
1165
|
/**
|
|
1040
1166
|
* 获取可 Redeem 的仓位
|
|
@@ -1085,7 +1211,7 @@ class Redeemer {
|
|
|
1085
1211
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1086
1212
|
if (errorMsg.includes("429") || errorMsg.includes("Too Many Requests")) {
|
|
1087
1213
|
const resetSeconds = this.parseResetSeconds(errorMsg);
|
|
1088
|
-
|
|
1214
|
+
logger.warning(`获取仓位限流,${resetSeconds} 秒后重试`);
|
|
1089
1215
|
telegramService.warning(
|
|
1090
1216
|
"API 限流",
|
|
1091
1217
|
`获取仓位限流,${resetSeconds} 秒后重试`
|
|
@@ -1093,9 +1219,27 @@ class Redeemer {
|
|
|
1093
1219
|
this.pauseAndRestart((resetSeconds + 60) * 1e3);
|
|
1094
1220
|
return;
|
|
1095
1221
|
}
|
|
1096
|
-
|
|
1222
|
+
logger.error(`获取仓位失败: ${errorMsg}`);
|
|
1097
1223
|
return;
|
|
1098
1224
|
}
|
|
1225
|
+
const currentIds = new Set(positions.map((position) => position.conditionId));
|
|
1226
|
+
this.logPendingPositions(positions);
|
|
1227
|
+
const pendingRecords = this.buildRecords(positions);
|
|
1228
|
+
this.updateMatchedRecords(currentIds);
|
|
1229
|
+
const shouldContinue = await this.executeBatches(pendingRecords);
|
|
1230
|
+
if (!shouldContinue) {
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
const successItems = this.collectSuccessRecords(currentIds);
|
|
1234
|
+
if (successItems.length > 0) {
|
|
1235
|
+
const successTitle = `自动 redeem: 成功 ${successItems.length} 个`;
|
|
1236
|
+
const successLines = this.logPositions(successTitle, successItems);
|
|
1237
|
+
telegramService.success(successTitle, successLines.join("\n"));
|
|
1238
|
+
this.onRedeemSuccess?.();
|
|
1239
|
+
}
|
|
1240
|
+
logger.info(`redeem 状态: ${this.records.size} 个待确认`);
|
|
1241
|
+
}
|
|
1242
|
+
logPendingPositions(positions) {
|
|
1099
1243
|
const pendingRedeemablePositions = positions.filter((position) => {
|
|
1100
1244
|
const record = this.records.get(position.conditionId);
|
|
1101
1245
|
return !record?.success;
|
|
@@ -1105,8 +1249,11 @@ class Redeemer {
|
|
|
1105
1249
|
`自动 redeem: 发现 ${pendingRedeemablePositions.length} 个可 Redeem 仓位`,
|
|
1106
1250
|
pendingRedeemablePositions.map((position) => ({ position }))
|
|
1107
1251
|
);
|
|
1252
|
+
logger.info(`Redeem 批量大小: ${this.batchSize}`);
|
|
1108
1253
|
}
|
|
1109
|
-
|
|
1254
|
+
}
|
|
1255
|
+
buildRecords(positions) {
|
|
1256
|
+
return positions.map((position) => {
|
|
1110
1257
|
let record = this.records.get(position.conditionId);
|
|
1111
1258
|
if (!record) {
|
|
1112
1259
|
record = {
|
|
@@ -1118,52 +1265,76 @@ class Redeemer {
|
|
|
1118
1265
|
this.records.set(position.conditionId, record);
|
|
1119
1266
|
}
|
|
1120
1267
|
record.position = position;
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1268
|
+
return record;
|
|
1269
|
+
}).filter((record) => !record.success && record.failedCount < MAX_MATCH_TIMES);
|
|
1270
|
+
}
|
|
1271
|
+
updateMatchedRecords(currentIds) {
|
|
1272
|
+
for (const record of this.records.values()) {
|
|
1273
|
+
if (!record.success) {
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
if (!currentIds.has(record.position.conditionId)) {
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
record.matchedCount++;
|
|
1280
|
+
if (record.matchedCount >= MAX_MATCH_TIMES) {
|
|
1281
|
+
logger.error(
|
|
1282
|
+
`Redeem 失败: 执行成功但仍存在 ${record.matchedCount} 次 (conditionId: ${record.position.conditionId.slice(0, 10)}...)`
|
|
1283
|
+
);
|
|
1284
|
+
telegramService.error(
|
|
1285
|
+
"Redeem 失败",
|
|
1286
|
+
`执行成功但仓位仍存在 ${record.matchedCount} 次
|
|
1287
|
+
conditionId: ${record.position.conditionId}
|
|
1288
|
+
价值: ${record.position.currentValue} USDC`
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
async executeBatches(pendingRecords) {
|
|
1294
|
+
for (let i = 0; i < pendingRecords.length; i += this.batchSize) {
|
|
1295
|
+
const batch = pendingRecords.slice(i, i + this.batchSize);
|
|
1296
|
+
const conditionIds = batch.map((record) => record.position.conditionId);
|
|
1297
|
+
if (conditionIds.length === 0) {
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
try {
|
|
1301
|
+
const txHash = await this.redeemFn?.(conditionIds);
|
|
1302
|
+
batch.forEach((record) => {
|
|
1137
1303
|
record.success = true;
|
|
1138
|
-
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1304
|
+
record.matchedCount = 0;
|
|
1305
|
+
});
|
|
1306
|
+
logger.info(`Redeem 已执行 (批量 ${batch.length}): ${txHash}`);
|
|
1307
|
+
} catch (error) {
|
|
1308
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1309
|
+
if (errorMsg.includes("429") || errorMsg.includes("Too Many Requests")) {
|
|
1310
|
+
const resetSeconds = this.parseResetSeconds(errorMsg);
|
|
1311
|
+
logger.warning(`Redeem 限流,${resetSeconds} 秒后重试`);
|
|
1312
|
+
telegramService.warning("Redeem 限流", `${resetSeconds} 秒后重试`);
|
|
1313
|
+
this.pauseAndRestart((resetSeconds + 60) * 1e3);
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1316
|
+
batch.forEach((record) => {
|
|
1148
1317
|
record.failedCount++;
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1318
|
+
});
|
|
1319
|
+
logger.error(`Redeem 批量异常 (批量 ${batch.length}): ${errorMsg}`);
|
|
1320
|
+
batch.forEach((record) => {
|
|
1152
1321
|
if (record.failedCount >= MAX_MATCH_TIMES) {
|
|
1153
1322
|
telegramService.error(
|
|
1154
1323
|
"Redeem 异常",
|
|
1155
1324
|
`重试 ${record.failedCount} 次仍失败
|
|
1156
|
-
conditionId: ${position.conditionId}
|
|
1325
|
+
conditionId: ${record.position.conditionId}
|
|
1157
1326
|
错误: ${errorMsg}`
|
|
1158
1327
|
);
|
|
1159
1328
|
}
|
|
1160
|
-
}
|
|
1329
|
+
});
|
|
1161
1330
|
}
|
|
1162
1331
|
}
|
|
1332
|
+
return true;
|
|
1333
|
+
}
|
|
1334
|
+
collectSuccessRecords(currentIds) {
|
|
1163
1335
|
const successItems = [];
|
|
1164
1336
|
for (const [conditionId, record] of this.records.entries()) {
|
|
1165
|
-
|
|
1166
|
-
if (!stillExists) {
|
|
1337
|
+
if (!currentIds.has(conditionId)) {
|
|
1167
1338
|
successItems.push({
|
|
1168
1339
|
position: record.position,
|
|
1169
1340
|
matchedCount: record.matchedCount
|
|
@@ -1171,13 +1342,7 @@ conditionId: ${position.conditionId}
|
|
|
1171
1342
|
this.records.delete(conditionId);
|
|
1172
1343
|
}
|
|
1173
1344
|
}
|
|
1174
|
-
|
|
1175
|
-
const successTitle = `自动 redeem: 成功 ${successItems.length} 个`;
|
|
1176
|
-
const successLines = this.logPositions(successTitle, successItems);
|
|
1177
|
-
telegramService.success(successTitle, successLines.join("\n"));
|
|
1178
|
-
this.onRedeemSuccess?.();
|
|
1179
|
-
}
|
|
1180
|
-
init.logger.info(`redeem 状态: ${this.records.size} 个待确认`);
|
|
1345
|
+
return successItems;
|
|
1181
1346
|
}
|
|
1182
1347
|
/**
|
|
1183
1348
|
* 统一格式化仓位信息
|
|
@@ -1192,15 +1357,12 @@ conditionId: ${position.conditionId}
|
|
|
1192
1357
|
* 统一输出 Redeem 仓位列表
|
|
1193
1358
|
*/
|
|
1194
1359
|
logPositions(title, items) {
|
|
1195
|
-
|
|
1360
|
+
logger.info(title);
|
|
1196
1361
|
const lines = [];
|
|
1197
1362
|
for (const item of items) {
|
|
1198
|
-
const line = this.formatPositionLine(
|
|
1199
|
-
item.position,
|
|
1200
|
-
item.matchedCount
|
|
1201
|
-
);
|
|
1363
|
+
const line = this.formatPositionLine(item.position, item.matchedCount);
|
|
1202
1364
|
lines.push(line);
|
|
1203
|
-
|
|
1365
|
+
logger.line("", `- ${line}`);
|
|
1204
1366
|
}
|
|
1205
1367
|
return lines;
|
|
1206
1368
|
}
|
|
@@ -1240,7 +1402,7 @@ class Trader {
|
|
|
1240
1402
|
}
|
|
1241
1403
|
this.config = config;
|
|
1242
1404
|
if (!config.polymarket?.privateKey) {
|
|
1243
|
-
|
|
1405
|
+
logger.warning("未配置 PolyMarket 私钥,交易功能已禁用");
|
|
1244
1406
|
return;
|
|
1245
1407
|
}
|
|
1246
1408
|
await this.client.init({
|
|
@@ -1254,10 +1416,10 @@ class Trader {
|
|
|
1254
1416
|
builderCreds: config.polymarket.builderCreds
|
|
1255
1417
|
});
|
|
1256
1418
|
} else {
|
|
1257
|
-
|
|
1419
|
+
logger.warning("未配置 builderCreds,自动 Redeem 功能已禁用");
|
|
1258
1420
|
}
|
|
1259
1421
|
this.initialized = true;
|
|
1260
|
-
|
|
1422
|
+
logger.success("交易模块初始化完成");
|
|
1261
1423
|
}
|
|
1262
1424
|
/**
|
|
1263
1425
|
* 启动 Redeemer(在 Telegram listener 就绪后调用)
|
|
@@ -1267,7 +1429,7 @@ class Trader {
|
|
|
1267
1429
|
return;
|
|
1268
1430
|
}
|
|
1269
1431
|
this.redeemer.start(
|
|
1270
|
-
this.
|
|
1432
|
+
this.executeRedeemBatch.bind(this),
|
|
1271
1433
|
this.config.polymarket.funderAddress || "",
|
|
1272
1434
|
() => this.balanceCache.invalidate()
|
|
1273
1435
|
);
|
|
@@ -1283,19 +1445,19 @@ class Trader {
|
|
|
1283
1445
|
*/
|
|
1284
1446
|
async executeSignal(signal) {
|
|
1285
1447
|
if (!this.initialized || !this.config) {
|
|
1286
|
-
|
|
1448
|
+
logger.warning("交易模块未初始化,跳过信号");
|
|
1287
1449
|
return;
|
|
1288
1450
|
}
|
|
1289
1451
|
const { metadata } = signal;
|
|
1290
1452
|
const { assetId, endTime, outcome } = metadata;
|
|
1291
|
-
|
|
1453
|
+
logger.section(`处理信号: ${outcome} (${metadata.eventId})`);
|
|
1292
1454
|
if (!this.assetFilter.canTrade(assetId, endTime)) {
|
|
1293
|
-
|
|
1455
|
+
logger.warning(`assetId 已交易,跳过: ${assetId.slice(0, 10)}...`);
|
|
1294
1456
|
return;
|
|
1295
1457
|
}
|
|
1296
1458
|
const tradingConfig = this.config.trading;
|
|
1297
1459
|
const amountMode = tradingConfig?.amountMode || "fixed";
|
|
1298
|
-
const amountValue = tradingConfig?.
|
|
1460
|
+
const amountValue = amountMode === "percentage" ? tradingConfig?.amountPercentageValue ?? 0.1 : tradingConfig?.amountFixedValue ?? 5;
|
|
1299
1461
|
const buyPrice = tradingConfig?.buyPrice || 0.98;
|
|
1300
1462
|
const sellPrice = tradingConfig?.sellPrice || 1;
|
|
1301
1463
|
let balance;
|
|
@@ -1306,12 +1468,12 @@ class Trader {
|
|
|
1306
1468
|
);
|
|
1307
1469
|
} catch (error) {
|
|
1308
1470
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1309
|
-
|
|
1471
|
+
logger.error(`获取余额失败: ${errorMsg}`);
|
|
1310
1472
|
return;
|
|
1311
1473
|
}
|
|
1312
1474
|
if (amountMode === "fixed" && balance < amountValue) {
|
|
1313
1475
|
const msg = `余额不足: ${balance} USDC < ${amountValue} USDC`;
|
|
1314
|
-
|
|
1476
|
+
logger.warning(msg);
|
|
1315
1477
|
telegramService.warning(
|
|
1316
1478
|
"余额不足",
|
|
1317
1479
|
`当前余额: ${balance} USDC
|
|
@@ -1320,18 +1482,18 @@ class Trader {
|
|
|
1320
1482
|
return;
|
|
1321
1483
|
}
|
|
1322
1484
|
if (amountMode === "percentage" && balance <= 0) {
|
|
1323
|
-
|
|
1485
|
+
logger.warning(`余额为零: ${balance} USDC`);
|
|
1324
1486
|
telegramService.warning("余额为零", `当前余额: ${balance} USDC`);
|
|
1325
1487
|
return;
|
|
1326
1488
|
}
|
|
1327
1489
|
const orderAmount = amountMode === "fixed" ? amountValue : balance * amountValue;
|
|
1328
1490
|
const size = orderAmount / buyPrice;
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1491
|
+
logger.info(`下单金额: ${orderAmount.toFixed(2)} USDC`);
|
|
1492
|
+
logger.info(`下单价格: ${buyPrice}`);
|
|
1493
|
+
logger.info(`下单数量: ${size.toFixed(4)}`);
|
|
1332
1494
|
const buyResult = await this.client.createBuyOrder(assetId, buyPrice, size);
|
|
1333
1495
|
if (!buyResult.success) {
|
|
1334
|
-
|
|
1496
|
+
logger.error(`买单失败: ${buyResult.errorMsg}`);
|
|
1335
1497
|
telegramService.error(
|
|
1336
1498
|
"买单失败",
|
|
1337
1499
|
`事件: ${metadata.eventId}
|
|
@@ -1341,7 +1503,7 @@ class Trader {
|
|
|
1341
1503
|
}
|
|
1342
1504
|
this.assetFilter.markTraded(assetId, endTime);
|
|
1343
1505
|
if (sellPrice === 1) {
|
|
1344
|
-
|
|
1506
|
+
logger.info("sellPrice = 1,成交后将自动 redeem");
|
|
1345
1507
|
return;
|
|
1346
1508
|
}
|
|
1347
1509
|
this.watchAndHandle(
|
|
@@ -1371,7 +1533,7 @@ class Trader {
|
|
|
1371
1533
|
);
|
|
1372
1534
|
} catch (error) {
|
|
1373
1535
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1374
|
-
|
|
1536
|
+
logger.error(`订单监控异常: ${errorMsg}`);
|
|
1375
1537
|
telegramService.error(
|
|
1376
1538
|
"订单监控异常",
|
|
1377
1539
|
`事件: ${eventId}
|
|
@@ -1388,22 +1550,22 @@ class Trader {
|
|
|
1388
1550
|
case "MATCHED":
|
|
1389
1551
|
case "CONFIRMED":
|
|
1390
1552
|
const filledSize = result.filledSize || 0;
|
|
1391
|
-
|
|
1553
|
+
logger.success(`买单成交: ${filledSize.toFixed(4)} 份`);
|
|
1392
1554
|
if (sellPrice < 1) {
|
|
1393
1555
|
this.waitAndSell(assetId, filledSize, sellPrice, endTime);
|
|
1394
1556
|
} else {
|
|
1395
|
-
|
|
1557
|
+
logger.info("sellPrice = 1,等待 Redeem");
|
|
1396
1558
|
}
|
|
1397
1559
|
break;
|
|
1398
1560
|
case "CANCELLED":
|
|
1399
|
-
|
|
1561
|
+
logger.info("订单已取消(市场可能已结束)");
|
|
1400
1562
|
break;
|
|
1401
1563
|
case "FAILED":
|
|
1402
|
-
|
|
1564
|
+
logger.error("订单执行失败");
|
|
1403
1565
|
telegramService.error("订单执行失败", `订单 ID: ${orderId}`);
|
|
1404
1566
|
break;
|
|
1405
1567
|
case "TIMEOUT":
|
|
1406
|
-
|
|
1568
|
+
logger.warning("订单超时,尝试取消");
|
|
1407
1569
|
await this.client.cancelOrder(orderId);
|
|
1408
1570
|
break;
|
|
1409
1571
|
}
|
|
@@ -1414,18 +1576,18 @@ class Trader {
|
|
|
1414
1576
|
async waitAndSell(assetId, expectedSize, sellPrice, endTime) {
|
|
1415
1577
|
const shortId = assetId.substring(0, 10) + "...";
|
|
1416
1578
|
const threshold = expectedSize * POSITION_MATCH_THRESHOLD;
|
|
1417
|
-
|
|
1579
|
+
logger.info(`等待仓位同步: ${shortId},预期 >= ${threshold.toFixed(4)}`);
|
|
1418
1580
|
while (true) {
|
|
1419
1581
|
const now = Date.now();
|
|
1420
1582
|
if (now >= endTime) {
|
|
1421
|
-
|
|
1583
|
+
logger.warning(`仓位同步超时: ${shortId},等待 Redeem`);
|
|
1422
1584
|
this.notifyRedeemFallback(assetId, expectedSize);
|
|
1423
1585
|
return;
|
|
1424
1586
|
}
|
|
1425
1587
|
try {
|
|
1426
1588
|
const positionSize = await this.client.getPositionSize(assetId);
|
|
1427
1589
|
if (positionSize >= threshold) {
|
|
1428
|
-
|
|
1590
|
+
logger.info(
|
|
1429
1591
|
`仓位已同步: ${shortId},数量: ${positionSize.toFixed(4)}`
|
|
1430
1592
|
);
|
|
1431
1593
|
await this.executeSellOrder(assetId, sellPrice, positionSize);
|
|
@@ -1433,7 +1595,7 @@ class Trader {
|
|
|
1433
1595
|
}
|
|
1434
1596
|
} catch (error) {
|
|
1435
1597
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1436
|
-
|
|
1598
|
+
logger.warning(`获取仓位失败: ${shortId},${errorMsg}`);
|
|
1437
1599
|
}
|
|
1438
1600
|
await this.delay(POSITION_POLL_INTERVAL);
|
|
1439
1601
|
}
|
|
@@ -1444,7 +1606,7 @@ class Trader {
|
|
|
1444
1606
|
notifyRedeemFallback(assetId, expectedSize) {
|
|
1445
1607
|
if (!this.relayClient.isInitialized()) {
|
|
1446
1608
|
const msg = "仓位同步超时且 Redeem 不可用";
|
|
1447
|
-
|
|
1609
|
+
logger.warning(msg);
|
|
1448
1610
|
telegramService.warning(
|
|
1449
1611
|
msg,
|
|
1450
1612
|
`Asset: ${assetId.substring(0, 20)}...
|
|
@@ -1463,36 +1625,36 @@ class Trader {
|
|
|
1463
1625
|
* 执行卖单
|
|
1464
1626
|
*/
|
|
1465
1627
|
async executeSellOrder(assetId, price, size) {
|
|
1466
|
-
|
|
1628
|
+
logger.info(`创建卖单: ${size.toFixed(4)} 份 @ ${price}`);
|
|
1467
1629
|
const sellResult = await this.client.createSellOrder(assetId, price, size);
|
|
1468
1630
|
if (sellResult.success) {
|
|
1469
|
-
|
|
1631
|
+
logger.success(`卖单已提交: ${sellResult.orderId}`);
|
|
1470
1632
|
} else {
|
|
1471
|
-
|
|
1633
|
+
logger.error(`卖单提交失败: ${sellResult.errorMsg}`);
|
|
1472
1634
|
telegramService.error("卖单提交失败", `错误: ${sellResult.errorMsg}`);
|
|
1473
1635
|
}
|
|
1474
1636
|
}
|
|
1475
1637
|
/**
|
|
1476
|
-
*
|
|
1638
|
+
* 批量执行 Redeem(供 Redeemer 调用)
|
|
1477
1639
|
*/
|
|
1478
|
-
async
|
|
1640
|
+
async executeRedeemBatch(conditionIds) {
|
|
1479
1641
|
if (!this.relayClient.isInitialized()) {
|
|
1480
1642
|
throw new Error("RelayClient 未初始化");
|
|
1481
1643
|
}
|
|
1482
|
-
return this.relayClient.
|
|
1644
|
+
return this.relayClient.redeemBatch(conditionIds);
|
|
1483
1645
|
}
|
|
1484
1646
|
/**
|
|
1485
1647
|
* 停止交易模块
|
|
1486
1648
|
*/
|
|
1487
1649
|
async shutdown() {
|
|
1488
1650
|
this.redeemer.stop();
|
|
1489
|
-
|
|
1651
|
+
logger.info("交易模块已停止");
|
|
1490
1652
|
}
|
|
1491
1653
|
}
|
|
1492
1654
|
const trader = new Trader();
|
|
1493
1655
|
function enterDaemonMode() {
|
|
1494
|
-
if (!fs__namespace.existsSync(
|
|
1495
|
-
fs__namespace.mkdirSync(
|
|
1656
|
+
if (!fs__namespace.existsSync(processLock.PID_DIR)) {
|
|
1657
|
+
fs__namespace.mkdirSync(processLock.PID_DIR, { recursive: true });
|
|
1496
1658
|
}
|
|
1497
1659
|
const child = child_process.spawn(process.execPath, [__filename], {
|
|
1498
1660
|
detached: true,
|
|
@@ -1504,46 +1666,81 @@ function enterDaemonMode() {
|
|
|
1504
1666
|
}
|
|
1505
1667
|
});
|
|
1506
1668
|
if (child.pid) {
|
|
1507
|
-
fs__namespace.writeFileSync(
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1669
|
+
fs__namespace.writeFileSync(processLock.PID_FILE, child.pid.toString());
|
|
1670
|
+
logger.blank();
|
|
1671
|
+
logger.success(`已进入后台模式 (PID: ${child.pid})`);
|
|
1672
|
+
logger.line("", `日志目录: ${processLock.LOGS_DIR}`);
|
|
1673
|
+
logger.line("", `使用 'polycopy stop' 停止`);
|
|
1674
|
+
logger.line("", `使用 'polycopy log' 查看日志`);
|
|
1513
1675
|
child.unref();
|
|
1514
1676
|
process.exit(0);
|
|
1515
1677
|
} else {
|
|
1516
|
-
|
|
1678
|
+
logger.error("进入后台模式失败");
|
|
1517
1679
|
process.exit(1);
|
|
1518
1680
|
}
|
|
1519
1681
|
}
|
|
1520
1682
|
async function main() {
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1683
|
+
logger.init();
|
|
1684
|
+
if (process.env.POLYCOPY_DAEMON_CHILD !== "1") {
|
|
1685
|
+
const runningPid = processLock.resolveRunningPid();
|
|
1686
|
+
if (runningPid) {
|
|
1687
|
+
console.log(`⚠️ 程序正在运行中 (PID: ${runningPid})`);
|
|
1688
|
+
console.log(` 请先停止程序后再启动`);
|
|
1689
|
+
process.exit(1);
|
|
1690
|
+
}
|
|
1691
|
+
processLock.savePid(process.pid);
|
|
1692
|
+
}
|
|
1693
|
+
const exitOnFatal = async () => {
|
|
1694
|
+
await logger.flush();
|
|
1695
|
+
if (process.env.POLYCOPY_DAEMON_CHILD !== "1") {
|
|
1696
|
+
processLock.removePid();
|
|
1697
|
+
}
|
|
1698
|
+
process.exit(1);
|
|
1699
|
+
};
|
|
1700
|
+
process.on("uncaughtException", async (error) => {
|
|
1701
|
+
const message = error instanceof Error ? error.stack || error.message : String(error);
|
|
1702
|
+
logger.error(`捕获未处理异常:
|
|
1703
|
+
${message}`);
|
|
1704
|
+
await exitOnFatal();
|
|
1705
|
+
});
|
|
1706
|
+
process.on("unhandledRejection", async (reason) => {
|
|
1707
|
+
const message = reason instanceof Error ? reason.stack || reason.message : String(reason);
|
|
1708
|
+
logger.error(`捕获未处理 Promise 拒绝:
|
|
1709
|
+
${message}`);
|
|
1710
|
+
await exitOnFatal();
|
|
1711
|
+
});
|
|
1712
|
+
process.on("beforeExit", async () => {
|
|
1713
|
+
await logger.flush();
|
|
1714
|
+
});
|
|
1715
|
+
const config = await processLock.ensureConfig();
|
|
1716
|
+
if (process.env.POLYCOPY_DAEMON === "1") {
|
|
1717
|
+
enterDaemonMode();
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
logger.section("PolyMarket 跟单机器人");
|
|
1524
1721
|
telegramService.init(
|
|
1525
1722
|
config.telegram.botToken,
|
|
1526
1723
|
config.telegram.adminChatId,
|
|
1527
1724
|
config.telegram.targetChatIds || []
|
|
1528
1725
|
);
|
|
1529
|
-
const autoTradeEnabled =
|
|
1726
|
+
const autoTradeEnabled = processLock.canAutoTrade(config);
|
|
1530
1727
|
if (!autoTradeEnabled) {
|
|
1531
|
-
|
|
1728
|
+
logger.warning("自动跟单功能未启用");
|
|
1532
1729
|
if (!config.polymarket?.privateKey) {
|
|
1533
|
-
|
|
1730
|
+
logger.line("", "- PolyMarket 配置不完整(缺少私钥)");
|
|
1534
1731
|
}
|
|
1535
|
-
if (!config.trading?.amountMode || !config.trading?.
|
|
1536
|
-
|
|
1732
|
+
if (!config.trading?.amountMode || !config.trading?.buyPrice || !config.trading?.sellPrice || (config.trading.amountMode === "fixed" ? !config.trading.amountFixedValue : !config.trading.amountPercentageValue)) {
|
|
1733
|
+
logger.line("", "- 交易配置不完整");
|
|
1537
1734
|
}
|
|
1538
|
-
|
|
1735
|
+
logger.line("", "当前不会跟单任何预测消息");
|
|
1539
1736
|
} else {
|
|
1540
1737
|
try {
|
|
1541
1738
|
await trader.init(config);
|
|
1542
1739
|
if (config.trading?.sellPrice === 1 && !config.polymarket?.builderCreds) {
|
|
1543
|
-
|
|
1740
|
+
logger.error(
|
|
1544
1741
|
"sellPrice = 1 但未配置 builderCreds,Redeem 功能无法使用,程序已退出"
|
|
1545
1742
|
);
|
|
1546
|
-
|
|
1743
|
+
logger.line(
|
|
1547
1744
|
"",
|
|
1548
1745
|
"请配置 Builder API 凭证,或将卖价设置为 < 1 使用限价卖出模式"
|
|
1549
1746
|
);
|
|
@@ -1551,29 +1748,29 @@ async function main() {
|
|
|
1551
1748
|
}
|
|
1552
1749
|
} catch (error) {
|
|
1553
1750
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1554
|
-
|
|
1555
|
-
|
|
1751
|
+
logger.error(`交易模块初始化失败: ${errorMsg}`);
|
|
1752
|
+
logger.line("", "当前不会跟单任何预测消息");
|
|
1556
1753
|
}
|
|
1557
1754
|
}
|
|
1558
1755
|
telegramService.onChatIdsChanged((chatIds) => {
|
|
1559
|
-
const telegram =
|
|
1756
|
+
const telegram = processLock.configLocal.getItem("telegram");
|
|
1560
1757
|
if (!telegram) {
|
|
1561
|
-
|
|
1758
|
+
logger.error("警告: 无法保存频道 ID,telegram 配置不存在");
|
|
1562
1759
|
return;
|
|
1563
1760
|
}
|
|
1564
1761
|
telegram.targetChatIds = chatIds.length > 0 ? chatIds : void 0;
|
|
1565
|
-
|
|
1566
|
-
|
|
1762
|
+
processLock.configLocal.setItem("telegram", telegram);
|
|
1763
|
+
logger.line("💾", `已保存频道 ID 列表: ${chatIds.length} 个频道`);
|
|
1567
1764
|
});
|
|
1568
1765
|
telegramService.onSignal(async (signal) => {
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1766
|
+
logger.info(`📨 收到预测信号:`);
|
|
1767
|
+
logger.line("", `事件: ${signal.metadata.eventId}`);
|
|
1768
|
+
logger.line(
|
|
1572
1769
|
"",
|
|
1573
1770
|
`方向: ${signal.metadata.outcome === "Up" ? "上涨 📈" : "下跌 📉"}`
|
|
1574
1771
|
);
|
|
1575
|
-
|
|
1576
|
-
|
|
1772
|
+
logger.line("", `Asset: ${signal.metadata.assetId}`);
|
|
1773
|
+
logger.line(
|
|
1577
1774
|
"",
|
|
1578
1775
|
`结束时间: ${new Date(signal.metadata.endTime).toLocaleString()}`
|
|
1579
1776
|
);
|
|
@@ -1582,24 +1779,23 @@ async function main() {
|
|
|
1582
1779
|
await trader.executeSignal(signal);
|
|
1583
1780
|
} catch (error) {
|
|
1584
1781
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1585
|
-
|
|
1782
|
+
logger.error(`交易执行失败: ${errorMsg}`);
|
|
1586
1783
|
}
|
|
1587
1784
|
}
|
|
1588
1785
|
});
|
|
1589
|
-
if (process.env.POLYCOPY_DAEMON === "1") {
|
|
1590
|
-
enterDaemonMode();
|
|
1591
|
-
return;
|
|
1592
|
-
}
|
|
1593
1786
|
let isShuttingDown = false;
|
|
1594
1787
|
const shutdown = async () => {
|
|
1595
1788
|
if (isShuttingDown) return;
|
|
1596
1789
|
isShuttingDown = true;
|
|
1597
1790
|
process.removeAllListeners("SIGINT");
|
|
1598
1791
|
process.removeAllListeners("SIGTERM");
|
|
1599
|
-
|
|
1600
|
-
|
|
1792
|
+
logger.blank();
|
|
1793
|
+
logger.info("正在退出...");
|
|
1601
1794
|
await Promise.all([trader.shutdown(), telegramService.stop()]);
|
|
1602
|
-
await
|
|
1795
|
+
await logger.flush();
|
|
1796
|
+
if (process.env.POLYCOPY_DAEMON_CHILD !== "1") {
|
|
1797
|
+
processLock.removePid();
|
|
1798
|
+
}
|
|
1603
1799
|
process.exit(0);
|
|
1604
1800
|
};
|
|
1605
1801
|
process.prependListener("SIGINT", () => {
|
|
@@ -1614,7 +1810,13 @@ async function main() {
|
|
|
1614
1810
|
}
|
|
1615
1811
|
});
|
|
1616
1812
|
}
|
|
1617
|
-
main().catch((error) => {
|
|
1618
|
-
|
|
1813
|
+
main().catch(async (error) => {
|
|
1814
|
+
const message = error instanceof Error ? error.stack || error.message : String(error);
|
|
1815
|
+
logger.error(`程序运行出错:
|
|
1816
|
+
${message}`);
|
|
1817
|
+
await logger.flush();
|
|
1818
|
+
if (process.env.POLYCOPY_DAEMON_CHILD !== "1") {
|
|
1819
|
+
processLock.removePid();
|
|
1820
|
+
}
|
|
1619
1821
|
process.exit(1);
|
|
1620
1822
|
});
|