@agenticmail/enterprise 0.5.443 → 0.5.445
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/CHANGELOG.md +1 -0
- package/dist/agent-tools-QXSZOYCX.js +14629 -0
- package/dist/chunk-46UEEFMW.js +2214 -0
- package/dist/chunk-5NJ3WE4B.js +7592 -0
- package/dist/chunk-B73UUNMT.js +7689 -0
- package/dist/chunk-NQRWBIX4.js +1728 -0
- package/dist/chunk-QROWKQVL.js +5387 -0
- package/dist/chunk-XNMDISPO.js +1728 -0
- package/dist/chunk-XTRFUWIR.js +1645 -0
- package/dist/chunk-YXSIPPBW.js +5596 -0
- package/dist/cli-agent-7TB4WA57.js +2761 -0
- package/dist/cli-serve-4YXKRC3R.js +322 -0
- package/dist/cli-serve-WTRKWP5I.js +322 -0
- package/dist/cli.js +3 -3
- package/dist/index.js +6 -6
- package/dist/polymarket-ZVUDH7EV.js +17 -0
- package/dist/polymarket-runtime-6LTI2BL7.js +108 -0
- package/dist/polymarket-watcher-2JCPZUKA.js +23 -0
- package/dist/runtime-RDDBAMV3.js +50 -0
- package/dist/server-34DOMSXN.js +36 -0
- package/dist/server-T5S3OKDE.js +36 -0
- package/dist/setup-G456OG4S.js +20 -0
- package/dist/setup-QUDFN4L6.js +20 -0
- package/logs/cloudflared-error.log +78 -0
- package/package.json +1 -1
|
@@ -0,0 +1,2214 @@
|
|
|
1
|
+
// src/agent-tools/tools/polymarket-watcher.ts
|
|
2
|
+
var WATCHER_TABLES = [
|
|
3
|
+
`CREATE TABLE IF NOT EXISTS poly_watchers (
|
|
4
|
+
id TEXT PRIMARY KEY,
|
|
5
|
+
agent_id TEXT NOT NULL,
|
|
6
|
+
type TEXT NOT NULL,
|
|
7
|
+
name TEXT,
|
|
8
|
+
config TEXT NOT NULL DEFAULT '{}',
|
|
9
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
10
|
+
interval_ms INTEGER NOT NULL DEFAULT 60000,
|
|
11
|
+
last_run TEXT,
|
|
12
|
+
last_alert TEXT,
|
|
13
|
+
alert_count INTEGER DEFAULT 0,
|
|
14
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
15
|
+
)`,
|
|
16
|
+
`CREATE TABLE IF NOT EXISTS poly_watcher_events (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
agent_id TEXT NOT NULL,
|
|
19
|
+
watcher_id TEXT,
|
|
20
|
+
type TEXT NOT NULL,
|
|
21
|
+
severity TEXT NOT NULL DEFAULT 'info',
|
|
22
|
+
title TEXT NOT NULL,
|
|
23
|
+
summary TEXT,
|
|
24
|
+
data TEXT DEFAULT '{}',
|
|
25
|
+
acknowledged INTEGER DEFAULT 0,
|
|
26
|
+
routed INTEGER DEFAULT 0,
|
|
27
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
28
|
+
)`,
|
|
29
|
+
`CREATE TABLE IF NOT EXISTS poly_price_cache (
|
|
30
|
+
token_id TEXT PRIMARY KEY,
|
|
31
|
+
price REAL,
|
|
32
|
+
prev_price REAL,
|
|
33
|
+
volume_24h REAL,
|
|
34
|
+
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
35
|
+
)`,
|
|
36
|
+
`CREATE TABLE IF NOT EXISTS poly_crypto_cache (
|
|
37
|
+
symbol TEXT PRIMARY KEY,
|
|
38
|
+
price REAL,
|
|
39
|
+
change_24h REAL,
|
|
40
|
+
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
41
|
+
)`,
|
|
42
|
+
// AI analysis cache — avoid re-analyzing the same content
|
|
43
|
+
`CREATE TABLE IF NOT EXISTS poly_analysis_cache (
|
|
44
|
+
content_hash TEXT PRIMARY KEY,
|
|
45
|
+
analysis TEXT NOT NULL,
|
|
46
|
+
model TEXT,
|
|
47
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
48
|
+
)`,
|
|
49
|
+
// Watcher engine config — per-agent model settings
|
|
50
|
+
`CREATE TABLE IF NOT EXISTS poly_watcher_config (
|
|
51
|
+
agent_id TEXT PRIMARY KEY,
|
|
52
|
+
ai_model TEXT DEFAULT 'grok-3-mini',
|
|
53
|
+
ai_provider TEXT DEFAULT 'xai',
|
|
54
|
+
ai_api_key TEXT,
|
|
55
|
+
use_org_key INTEGER DEFAULT 1,
|
|
56
|
+
analysis_budget_daily INTEGER DEFAULT 100,
|
|
57
|
+
analysis_count_today INTEGER DEFAULT 0,
|
|
58
|
+
analysis_date TEXT,
|
|
59
|
+
max_spawn_per_hour INTEGER DEFAULT 6,
|
|
60
|
+
enabled INTEGER DEFAULT 1,
|
|
61
|
+
config TEXT DEFAULT '{}',
|
|
62
|
+
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
63
|
+
)`,
|
|
64
|
+
// Sentiment history — tracks sentiment trends over time
|
|
65
|
+
`CREATE TABLE IF NOT EXISTS poly_sentiment_history (
|
|
66
|
+
id TEXT PRIMARY KEY,
|
|
67
|
+
agent_id TEXT NOT NULL,
|
|
68
|
+
topic TEXT NOT NULL,
|
|
69
|
+
sentiment REAL NOT NULL,
|
|
70
|
+
confidence REAL,
|
|
71
|
+
source TEXT,
|
|
72
|
+
analysis TEXT,
|
|
73
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
74
|
+
)`,
|
|
75
|
+
// Signal correlation buffer — stores recent signals for cross-correlation
|
|
76
|
+
`CREATE TABLE IF NOT EXISTS poly_signal_buffer (
|
|
77
|
+
id TEXT PRIMARY KEY,
|
|
78
|
+
agent_id TEXT NOT NULL,
|
|
79
|
+
signal_type TEXT NOT NULL,
|
|
80
|
+
topic TEXT NOT NULL,
|
|
81
|
+
data TEXT DEFAULT '{}',
|
|
82
|
+
sentiment REAL,
|
|
83
|
+
impact TEXT,
|
|
84
|
+
expires_at TEXT,
|
|
85
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
86
|
+
)`
|
|
87
|
+
];
|
|
88
|
+
async function initWatcherTables(edb) {
|
|
89
|
+
if (!edb) return;
|
|
90
|
+
for (const sql of WATCHER_TABLES) {
|
|
91
|
+
try {
|
|
92
|
+
await edb.run(sql);
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
await edb.run(`ALTER TABLE poly_watcher_config ADD COLUMN use_org_key INTEGER DEFAULT 1`);
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function dateAgo(hours) {
|
|
102
|
+
return new Date(Date.now() - hours * 36e5).toISOString();
|
|
103
|
+
}
|
|
104
|
+
function dateAgoMin(minutes) {
|
|
105
|
+
return new Date(Date.now() - minutes * 6e4).toISOString();
|
|
106
|
+
}
|
|
107
|
+
function dateAhead(hours) {
|
|
108
|
+
return new Date(Date.now() + hours * 36e5).toISOString();
|
|
109
|
+
}
|
|
110
|
+
async function callAnalysisLLM(prompt, config, opts) {
|
|
111
|
+
const baseUrls = {
|
|
112
|
+
xai: "https://api.x.ai/v1",
|
|
113
|
+
openai: "https://api.openai.com/v1",
|
|
114
|
+
groq: "https://api.groq.com/openai/v1",
|
|
115
|
+
cerebras: "https://api.cerebras.ai/v1",
|
|
116
|
+
together: "https://api.together.xyz/v1",
|
|
117
|
+
openrouter: "https://openrouter.ai/api/v1",
|
|
118
|
+
deepseek: "https://api.deepseek.com/v1",
|
|
119
|
+
fireworks: "https://api.fireworks.ai/inference/v1",
|
|
120
|
+
anthropic: "https://api.anthropic.com"
|
|
121
|
+
};
|
|
122
|
+
const isAnthropic = config.provider === "anthropic";
|
|
123
|
+
const baseUrl = baseUrls[config.provider] || baseUrls.openai;
|
|
124
|
+
let res;
|
|
125
|
+
if (isAnthropic) {
|
|
126
|
+
res = await fetch(`${baseUrl}/v1/messages`, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: {
|
|
129
|
+
"Content-Type": "application/json",
|
|
130
|
+
"x-api-key": config.apiKey,
|
|
131
|
+
"anthropic-version": "2023-06-01"
|
|
132
|
+
},
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
model: config.model,
|
|
135
|
+
max_tokens: opts?.maxTokens || 500,
|
|
136
|
+
messages: [{ role: "user", content: prompt }]
|
|
137
|
+
}),
|
|
138
|
+
signal: AbortSignal.timeout(opts?.timeoutMs || 15e3)
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
res = await fetch(`${baseUrl}/chat/completions`, {
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers: {
|
|
144
|
+
"Content-Type": "application/json",
|
|
145
|
+
"Authorization": `Bearer ${config.apiKey}`
|
|
146
|
+
},
|
|
147
|
+
body: JSON.stringify({
|
|
148
|
+
model: config.model,
|
|
149
|
+
messages: [{ role: "user", content: prompt }],
|
|
150
|
+
max_tokens: opts?.maxTokens || 500,
|
|
151
|
+
temperature: opts?.temperature || 0.3
|
|
152
|
+
}),
|
|
153
|
+
signal: AbortSignal.timeout(opts?.timeoutMs || 15e3)
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (!res.ok) {
|
|
157
|
+
const err = await res.text().catch(() => "");
|
|
158
|
+
throw new Error(`LLM API error ${res.status}: ${err.slice(0, 200)}`);
|
|
159
|
+
}
|
|
160
|
+
const data = await res.json();
|
|
161
|
+
if (isAnthropic) {
|
|
162
|
+
return data.content?.[0]?.text || "";
|
|
163
|
+
}
|
|
164
|
+
return data.choices?.[0]?.message?.content || "";
|
|
165
|
+
}
|
|
166
|
+
async function getAIConfig(agentId, edb) {
|
|
167
|
+
try {
|
|
168
|
+
const cfg = await edb.get(`SELECT * FROM poly_watcher_config WHERE agent_id = ?`, [agentId]);
|
|
169
|
+
if (!cfg) return null;
|
|
170
|
+
const apiKey = cfg.ai_api_key || "";
|
|
171
|
+
if (!apiKey) return null;
|
|
172
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
173
|
+
if (cfg.analysis_date !== today) {
|
|
174
|
+
await edb.run(`UPDATE poly_watcher_config SET analysis_count_today = 0, analysis_date = ? WHERE agent_id = ?`, [today, agentId]);
|
|
175
|
+
return { provider: cfg.ai_provider, model: cfg.ai_model, apiKey, budgetRemaining: cfg.analysis_budget_daily || 100 };
|
|
176
|
+
}
|
|
177
|
+
const remaining = (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0);
|
|
178
|
+
if (remaining <= 0) return null;
|
|
179
|
+
return { provider: cfg.ai_provider, model: cfg.ai_model, apiKey, budgetRemaining: remaining };
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function incrementAnalysisCount(agentId, edb) {
|
|
185
|
+
try {
|
|
186
|
+
await edb.run(`UPDATE poly_watcher_config SET analysis_count_today = analysis_count_today + 1, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`, [agentId]);
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function getCachedAnalysis(contentHash, edb) {
|
|
191
|
+
try {
|
|
192
|
+
const row = await edb.get(`SELECT analysis FROM poly_analysis_cache WHERE content_hash = ? AND created_at > ?`, [contentHash, dateAgo(1)]);
|
|
193
|
+
if (row) return JSON.parse(row.analysis);
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
async function cacheAnalysis(contentHash, analysis, model, edb) {
|
|
199
|
+
try {
|
|
200
|
+
await edb.run(
|
|
201
|
+
`INSERT OR REPLACE INTO poly_analysis_cache (content_hash, analysis, model, created_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)`,
|
|
202
|
+
[contentHash, JSON.stringify(analysis), model]
|
|
203
|
+
);
|
|
204
|
+
} catch {
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function hashContent(content) {
|
|
208
|
+
let hash = 0;
|
|
209
|
+
for (let i = 0; i < content.length; i++) {
|
|
210
|
+
const chr = content.charCodeAt(i);
|
|
211
|
+
hash = (hash << 5) - hash + chr;
|
|
212
|
+
hash |= 0;
|
|
213
|
+
}
|
|
214
|
+
return "h" + Math.abs(hash).toString(36);
|
|
215
|
+
}
|
|
216
|
+
async function analyzeWithAI(content, context, agentId, edb) {
|
|
217
|
+
const aiConfig = await getAIConfig(agentId, edb);
|
|
218
|
+
if (!aiConfig) return null;
|
|
219
|
+
const hash = hashContent(content + JSON.stringify(context));
|
|
220
|
+
const cached = await getCachedAnalysis(hash, edb);
|
|
221
|
+
if (cached) return cached;
|
|
222
|
+
const marketContext = context.watchedMarkets?.length ? `
|
|
223
|
+
Agent is currently watching/trading these Polymarket markets:
|
|
224
|
+
${context.watchedMarkets.map((m) => `- ${m}`).join("\n")}` : "";
|
|
225
|
+
const positionContext = context.positions?.length ? `
|
|
226
|
+
Agent has open positions in:
|
|
227
|
+
${context.positions.map((p) => `- ${p}`).join("\n")}` : "";
|
|
228
|
+
const prompt = `You are a quantitative market intelligence analyst for a prediction market trading system (Polymarket).
|
|
229
|
+
|
|
230
|
+
Analyze the following content and assess its potential impact on prediction markets.
|
|
231
|
+
${marketContext}${positionContext}
|
|
232
|
+
|
|
233
|
+
CONTENT TO ANALYZE:
|
|
234
|
+
${content}
|
|
235
|
+
|
|
236
|
+
Respond in EXACT JSON format (no markdown, no code blocks):
|
|
237
|
+
{
|
|
238
|
+
"impact": "none|low|medium|high|critical",
|
|
239
|
+
"sentiment": <float -1.0 to 1.0, negative=bearish, positive=bullish>,
|
|
240
|
+
"confidence": <float 0.0 to 1.0, how confident in this assessment>,
|
|
241
|
+
"reasoning": "<1-2 sentence explanation of WHY this matters for prediction markets>",
|
|
242
|
+
"affected_markets": ["<list of market topics/questions this could affect>"],
|
|
243
|
+
"recommended_action": "<what should the trader do: buy/sell/hedge/monitor/ignore>",
|
|
244
|
+
"predicted_outcome": "<what you think will happen based on this signal>",
|
|
245
|
+
"time_horizon": "<how soon: immediate/hours/days/weeks>"
|
|
246
|
+
}`;
|
|
247
|
+
try {
|
|
248
|
+
const response = await callAnalysisLLM(prompt, {
|
|
249
|
+
provider: aiConfig.provider,
|
|
250
|
+
model: aiConfig.model,
|
|
251
|
+
apiKey: aiConfig.apiKey
|
|
252
|
+
}, { maxTokens: 400, temperature: 0.2 });
|
|
253
|
+
await incrementAnalysisCount(agentId, edb);
|
|
254
|
+
let jsonStr = response.trim();
|
|
255
|
+
if (jsonStr.startsWith("```")) {
|
|
256
|
+
jsonStr = jsonStr.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
|
|
257
|
+
}
|
|
258
|
+
const analysis = JSON.parse(jsonStr);
|
|
259
|
+
await cacheAnalysis(hash, analysis, aiConfig.model, edb);
|
|
260
|
+
return analysis;
|
|
261
|
+
} catch (e) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async function analyzeGeopolitical(headlines, watchedTopics, agentId, edb) {
|
|
266
|
+
const aiConfig = await getAIConfig(agentId, edb);
|
|
267
|
+
if (!aiConfig) return null;
|
|
268
|
+
const hash = hashContent(headlines.map((h) => h.title).join("|") + watchedTopics.join(","));
|
|
269
|
+
const cached = await getCachedAnalysis(hash, edb);
|
|
270
|
+
if (cached) return cached;
|
|
271
|
+
const prompt = `You are a geopolitical intelligence analyst working for a prediction market hedge fund.
|
|
272
|
+
|
|
273
|
+
Your job: Identify patterns in current events that predict future outcomes relevant to prediction markets (Polymarket).
|
|
274
|
+
|
|
275
|
+
CURRENT HEADLINES:
|
|
276
|
+
${headlines.map((h, i) => `${i + 1}. ${h.title}${h.source ? ` (${h.source})` : ""}${h.date ? ` [${h.date}]` : ""}`).join("\n")}
|
|
277
|
+
|
|
278
|
+
TOPICS THE TRADER IS WATCHING:
|
|
279
|
+
${watchedTopics.map((t) => `- ${t}`).join("\n")}
|
|
280
|
+
|
|
281
|
+
ANALYSIS REQUIRED:
|
|
282
|
+
1. Connect dots between these headlines \u2014 what patterns emerge?
|
|
283
|
+
2. What events do these patterns predict could happen soon?
|
|
284
|
+
3. Which prediction markets would be most affected?
|
|
285
|
+
4. What's the confidence level and time horizon?
|
|
286
|
+
|
|
287
|
+
Think like a CIA analyst briefing a trader. Focus on ACTIONABLE intelligence.
|
|
288
|
+
|
|
289
|
+
Respond in EXACT JSON format (no markdown, no code blocks):
|
|
290
|
+
{
|
|
291
|
+
"impact": "none|low|medium|high|critical",
|
|
292
|
+
"sentiment": <float -1.0 to 1.0>,
|
|
293
|
+
"confidence": <float 0.0 to 1.0>,
|
|
294
|
+
"reasoning": "<2-3 sentence analysis connecting the dots between events and predicting what happens next>",
|
|
295
|
+
"affected_markets": ["<specific prediction market topics this affects>"],
|
|
296
|
+
"recommended_action": "<specific trading recommendation>",
|
|
297
|
+
"predicted_outcome": "<what the analyst predicts will happen based on patterns>",
|
|
298
|
+
"time_horizon": "<immediate/hours/days/weeks>"
|
|
299
|
+
}`;
|
|
300
|
+
try {
|
|
301
|
+
const response = await callAnalysisLLM(prompt, {
|
|
302
|
+
provider: aiConfig.provider,
|
|
303
|
+
model: aiConfig.model,
|
|
304
|
+
apiKey: aiConfig.apiKey
|
|
305
|
+
}, { maxTokens: 500, temperature: 0.3 });
|
|
306
|
+
await incrementAnalysisCount(agentId, edb);
|
|
307
|
+
let jsonStr = response.trim();
|
|
308
|
+
if (jsonStr.startsWith("```")) {
|
|
309
|
+
jsonStr = jsonStr.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
|
|
310
|
+
}
|
|
311
|
+
const analysis = JSON.parse(jsonStr);
|
|
312
|
+
await cacheAnalysis(hash, analysis, aiConfig.model, edb);
|
|
313
|
+
return analysis;
|
|
314
|
+
} catch {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async function analyzeSignalCorrelation(signals, agentId, edb) {
|
|
319
|
+
const aiConfig = await getAIConfig(agentId, edb);
|
|
320
|
+
if (!aiConfig || signals.length < 2) return null;
|
|
321
|
+
const hash = hashContent(signals.map((s) => s.type + s.topic + s.summary).join("|"));
|
|
322
|
+
const cached = await getCachedAnalysis(hash, edb);
|
|
323
|
+
if (cached) return cached;
|
|
324
|
+
const prompt = `You are a quantitative signal correlation analyst for a prediction market fund.
|
|
325
|
+
|
|
326
|
+
Multiple independent signals have fired within a short timeframe. Your job: determine if they're connected and what they predict together.
|
|
327
|
+
|
|
328
|
+
RECENT SIGNALS:
|
|
329
|
+
${signals.map((s, i) => `${i + 1}. [${s.type.toUpperCase()}] ${s.topic}: ${s.summary}${s.sentiment != null ? ` (sentiment: ${s.sentiment.toFixed(2)})` : ""}${s.impact ? ` (impact: ${s.impact})` : ""}`).join("\n")}
|
|
330
|
+
|
|
331
|
+
ANALYSIS:
|
|
332
|
+
1. Are these signals correlated? What's the common thread?
|
|
333
|
+
2. Does the convergence increase confidence in any direction?
|
|
334
|
+
3. Is this a cascade pattern (one event causing the others)?
|
|
335
|
+
4. What's the combined signal saying that individual signals miss?
|
|
336
|
+
|
|
337
|
+
Respond in EXACT JSON format (no markdown, no code blocks):
|
|
338
|
+
{
|
|
339
|
+
"impact": "none|low|medium|high|critical",
|
|
340
|
+
"sentiment": <float -1.0 to 1.0, combined signal direction>,
|
|
341
|
+
"confidence": <float 0.0 to 1.0, how confident the signals are correlated>,
|
|
342
|
+
"reasoning": "<2-3 sentences explaining the correlation pattern and prediction>",
|
|
343
|
+
"affected_markets": ["<markets where the combined signal is actionable>"],
|
|
344
|
+
"recommended_action": "<trading action based on signal convergence>",
|
|
345
|
+
"predicted_outcome": "<what the combined signal predicts>",
|
|
346
|
+
"time_horizon": "<immediate/hours/days/weeks>"
|
|
347
|
+
}`;
|
|
348
|
+
try {
|
|
349
|
+
const response = await callAnalysisLLM(prompt, {
|
|
350
|
+
provider: aiConfig.provider,
|
|
351
|
+
model: aiConfig.model,
|
|
352
|
+
apiKey: aiConfig.apiKey
|
|
353
|
+
}, { maxTokens: 400, temperature: 0.2 });
|
|
354
|
+
await incrementAnalysisCount(agentId, edb);
|
|
355
|
+
let jsonStr = response.trim();
|
|
356
|
+
if (jsonStr.startsWith("```")) {
|
|
357
|
+
jsonStr = jsonStr.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
|
|
358
|
+
}
|
|
359
|
+
const analysis = JSON.parse(jsonStr);
|
|
360
|
+
await cacheAnalysis(hash, analysis, aiConfig.model, edb);
|
|
361
|
+
return analysis;
|
|
362
|
+
} catch {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
var TICK_MS = 15e3;
|
|
367
|
+
var IDLE_CHECK_MS = 6e4;
|
|
368
|
+
var GAMMA_API = "https://gamma-api.polymarket.com";
|
|
369
|
+
var CLOB_API = "https://clob.polymarket.com";
|
|
370
|
+
var engineInterval = null;
|
|
371
|
+
var idleInterval = null;
|
|
372
|
+
var lastCryptoFetch = 0;
|
|
373
|
+
var cryptoCache = {};
|
|
374
|
+
var _engineDb = null;
|
|
375
|
+
var _engineOpts = null;
|
|
376
|
+
var _engineStartedAt = null;
|
|
377
|
+
var _engineTickCount = 0;
|
|
378
|
+
var _engineEventCount = 0;
|
|
379
|
+
var _engineAnalysisCount = 0;
|
|
380
|
+
var _spawnCount = 0;
|
|
381
|
+
var _lastAlertCheckMs = 0;
|
|
382
|
+
var _lastExitCheckMs = 0;
|
|
383
|
+
var _lastProactiveCheckMs = 0;
|
|
384
|
+
var _lastSpawnByAgent = {};
|
|
385
|
+
var SPAWN_COOLDOWN_MS = 5 * 6e4;
|
|
386
|
+
var MAX_BATCH_EVENTS = 10;
|
|
387
|
+
function getWatcherEngineStatus() {
|
|
388
|
+
return {
|
|
389
|
+
running: !!engineInterval,
|
|
390
|
+
idle: !!idleInterval && !engineInterval,
|
|
391
|
+
startedAt: _engineStartedAt,
|
|
392
|
+
tickCount: _engineTickCount,
|
|
393
|
+
eventCount: _engineEventCount,
|
|
394
|
+
analysisCount: _engineAnalysisCount,
|
|
395
|
+
spawnCount: _spawnCount,
|
|
396
|
+
tickMs: TICK_MS,
|
|
397
|
+
idleCheckMs: IDLE_CHECK_MS,
|
|
398
|
+
spawnCooldownMs: SPAWN_COOLDOWN_MS
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function startWatcherEngine(db, opts) {
|
|
402
|
+
_engineDb = db;
|
|
403
|
+
_engineOpts = opts || {};
|
|
404
|
+
const log = opts?.log || console.log;
|
|
405
|
+
if (idleInterval || engineInterval) return;
|
|
406
|
+
log("[poly-watcher] AI-powered engine registered \u2014 will activate when watchers exist");
|
|
407
|
+
const checkAndToggle = async () => {
|
|
408
|
+
const edb = db.getEngineDB?.();
|
|
409
|
+
if (!edb) return;
|
|
410
|
+
try {
|
|
411
|
+
const row = await edb.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`);
|
|
412
|
+
const count = row?.cnt || 0;
|
|
413
|
+
if (count > 0 && !engineInterval) {
|
|
414
|
+
_startFastLoop(db, _engineOpts);
|
|
415
|
+
} else if (count === 0 && engineInterval) {
|
|
416
|
+
_stopFastLoop(log);
|
|
417
|
+
}
|
|
418
|
+
} catch {
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
checkAndToggle();
|
|
422
|
+
idleInterval = setInterval(checkAndToggle, IDLE_CHECK_MS);
|
|
423
|
+
}
|
|
424
|
+
function setWatcherRuntime(getRuntime, getAgentConfig) {
|
|
425
|
+
if (_engineOpts) {
|
|
426
|
+
_engineOpts.getRuntime = getRuntime;
|
|
427
|
+
if (getAgentConfig) _engineOpts.getAgentConfig = getAgentConfig;
|
|
428
|
+
const log = _engineOpts.log || console.log;
|
|
429
|
+
log("[poly-watcher] Runtime reference injected \u2014 agent wake enabled");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function _startFastLoop(db, opts) {
|
|
433
|
+
if (engineInterval) return;
|
|
434
|
+
const log = opts.log || console.log;
|
|
435
|
+
log("[poly-watcher] Active watchers found \u2014 starting 15s tick loop");
|
|
436
|
+
_engineStartedAt = Date.now();
|
|
437
|
+
engineInterval = setInterval(async () => {
|
|
438
|
+
const edb = db.getEngineDB?.();
|
|
439
|
+
if (!edb) return;
|
|
440
|
+
_engineTickCount++;
|
|
441
|
+
try {
|
|
442
|
+
const now = Date.now();
|
|
443
|
+
const watchers = await edb.all(
|
|
444
|
+
`SELECT * FROM poly_watchers WHERE status = 'active'`
|
|
445
|
+
) || [];
|
|
446
|
+
if (watchers.length === 0) {
|
|
447
|
+
_stopFastLoop(log);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const eventsByAgent = {};
|
|
451
|
+
const priceCache = {};
|
|
452
|
+
for (const w of watchers) {
|
|
453
|
+
const lastRun = w.last_run ? new Date(w.last_run).getTime() : 0;
|
|
454
|
+
if (now - lastRun < (w.interval_ms || 6e4)) continue;
|
|
455
|
+
try {
|
|
456
|
+
const config = JSON.parse(w.config || "{}");
|
|
457
|
+
const events = await runWatcher(w.type, config, w.agent_id, edb);
|
|
458
|
+
for (const evt of events) {
|
|
459
|
+
const id = crypto.randomUUID();
|
|
460
|
+
if (config.auto_action && evt.severity !== "info") {
|
|
461
|
+
try {
|
|
462
|
+
const aa = config.auto_action;
|
|
463
|
+
const tokenId = aa.token_id || evt.data?.token_id;
|
|
464
|
+
if (tokenId && (aa.side || aa.action)) {
|
|
465
|
+
const { executeOrder } = await import("./polymarket-ZVUDH7EV.js");
|
|
466
|
+
const tradeId = `watcher_${w.id}_${Date.now()}`;
|
|
467
|
+
let tradePrice = aa.price || evt.data?.current_price || 0;
|
|
468
|
+
if (!tradePrice || tradePrice <= 0) {
|
|
469
|
+
try {
|
|
470
|
+
const mid = await fetch(`${CLOB_API}/midpoint?token_id=${tokenId}`, { signal: AbortSignal.timeout(5e3) }).then((r) => r.json());
|
|
471
|
+
tradePrice = parseFloat(mid?.mid || "0");
|
|
472
|
+
} catch {
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (!tradePrice || tradePrice <= 0) {
|
|
476
|
+
log(`[watcher] Auto-trade skipped for ${w.name}: could not resolve price for token ${tokenId}`);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (tradePrice >= 0.99 || tradePrice <= 0.01) {
|
|
480
|
+
const sellSide = (aa.side || String(aa.action).toUpperCase()) === "SELL";
|
|
481
|
+
if (sellSide) {
|
|
482
|
+
log(`[watcher] Auto-trade skipped for ${w.name}: market appears resolved (price=${tradePrice})`);
|
|
483
|
+
evt.data = evt.data || {};
|
|
484
|
+
evt.data.skipped = `Market resolved (price=${tradePrice}), SELL not executed`;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const tradeParams = {
|
|
489
|
+
token_id: tokenId,
|
|
490
|
+
side: aa.side || (String(aa.action).toUpperCase() === "SELL" ? "SELL" : "BUY"),
|
|
491
|
+
size: aa.size || aa.shares || 0,
|
|
492
|
+
price: tradePrice,
|
|
493
|
+
market_question: aa.market_question || evt.title || "Auto-trade from watcher",
|
|
494
|
+
order_type: "GTC"
|
|
495
|
+
};
|
|
496
|
+
const dbCompat = edb.query ? edb : { query: edb.all?.bind(edb), execute: edb.run?.bind(edb), run: edb.run?.bind(edb), get: edb.get?.bind(edb), all: edb.all?.bind(edb) };
|
|
497
|
+
log(`[watcher] Auto-executing trade for watcher ${w.name}: ${tradeParams.side} ${tradeParams.size} @ ${tradeParams.price}`);
|
|
498
|
+
const result = await executeOrder(w.agent_id, dbCompat, tradeId, tradeParams, "auto_watcher");
|
|
499
|
+
evt.data = evt.data || {};
|
|
500
|
+
evt.data.auto_trade_result = result;
|
|
501
|
+
evt.summary = (evt.summary || "") + ` | Auto-trade: ${tradeParams.side} ${tradeParams.size} shares`;
|
|
502
|
+
log(`[watcher] Auto-trade result for watcher ${w.name}: ${JSON.stringify(result).slice(0, 200)}`);
|
|
503
|
+
}
|
|
504
|
+
} catch (atErr) {
|
|
505
|
+
log(`[watcher] Auto-trade failed for watcher ${w.name}: ${atErr.message}`);
|
|
506
|
+
evt.data = evt.data || {};
|
|
507
|
+
evt.data.auto_trade_error = atErr.message;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
await edb.run(
|
|
511
|
+
`INSERT INTO poly_watcher_events (id, agent_id, watcher_id, type, severity, title, summary, data) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
512
|
+
[id, w.agent_id, w.id, evt.type, evt.severity, evt.title, evt.summary || "", JSON.stringify(evt.data || {})]
|
|
513
|
+
);
|
|
514
|
+
_engineEventCount++;
|
|
515
|
+
if (!eventsByAgent[w.agent_id]) {
|
|
516
|
+
eventsByAgent[w.agent_id] = { events: [], maxSeverity: "info" };
|
|
517
|
+
}
|
|
518
|
+
const batch = eventsByAgent[w.agent_id];
|
|
519
|
+
batch.events.push(evt);
|
|
520
|
+
if (evt.severity === "critical") batch.maxSeverity = "critical";
|
|
521
|
+
else if (evt.severity === "warning" && batch.maxSeverity !== "critical") batch.maxSeverity = "warning";
|
|
522
|
+
if (opts.onEvent) {
|
|
523
|
+
try {
|
|
524
|
+
opts.onEvent(w.agent_id, evt);
|
|
525
|
+
} catch {
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
await edb.run(
|
|
530
|
+
`UPDATE poly_watchers SET last_run = CURRENT_TIMESTAMP, alert_count = alert_count + ? WHERE id = ?`,
|
|
531
|
+
[events.length, w.id]
|
|
532
|
+
);
|
|
533
|
+
if (events.length > 0) {
|
|
534
|
+
await edb.run(`UPDATE poly_watchers SET last_alert = CURRENT_TIMESTAMP WHERE id = ?`, [w.id]);
|
|
535
|
+
}
|
|
536
|
+
} catch (e) {
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (now - _lastAlertCheckMs < 6e4) {
|
|
540
|
+
} else try {
|
|
541
|
+
_lastAlertCheckMs = now;
|
|
542
|
+
const activeAlerts = await edb.all(
|
|
543
|
+
`SELECT * FROM poly_price_alerts WHERE triggered = 0`
|
|
544
|
+
).catch(() => []) || [];
|
|
545
|
+
for (const alert of activeAlerts) {
|
|
546
|
+
try {
|
|
547
|
+
let currentPrice = priceCache[alert.token_id];
|
|
548
|
+
if (currentPrice === void 0) {
|
|
549
|
+
const mid = await fetch(`${CLOB_API}/midpoint?token_id=${alert.token_id}`, { signal: AbortSignal.timeout(5e3) }).then((r) => r.json());
|
|
550
|
+
currentPrice = parseFloat(mid?.mid || "0");
|
|
551
|
+
priceCache[alert.token_id] = currentPrice;
|
|
552
|
+
}
|
|
553
|
+
if (!currentPrice || currentPrice <= 0) {
|
|
554
|
+
try {
|
|
555
|
+
const marketInfo = await fetch(`${GAMMA_API}/markets?clob_token_ids=${alert.token_id}&limit=1`, { signal: AbortSignal.timeout(5e3) }).then((r) => r.ok ? r.json() : null).then((arr) => Array.isArray(arr) ? arr[0] : arr).catch(() => null);
|
|
556
|
+
const isClosed = marketInfo?.closed || marketInfo?.resolved || marketInfo?.active === false;
|
|
557
|
+
if (isClosed || !marketInfo) {
|
|
558
|
+
const reason2 = marketInfo?.resolved ? "resolved" : marketInfo?.closed ? "closed" : "unavailable";
|
|
559
|
+
log(`[alerts] Market ${reason2} for alert ${alert.id} (${alert.market_question || alert.token_id.slice(0, 16)}). Auto-cancelling.`);
|
|
560
|
+
await edb.run(`UPDATE poly_price_alerts SET triggered = 1, triggered_at = CURRENT_TIMESTAMP WHERE id = ?`, [alert.id]);
|
|
561
|
+
if (alert.bracket_group) {
|
|
562
|
+
await edb.run(
|
|
563
|
+
`UPDATE poly_price_alerts SET triggered = 1, triggered_at = CURRENT_TIMESTAMP WHERE bracket_group = ? AND id != ? AND triggered = 0`,
|
|
564
|
+
[alert.bracket_group, alert.id]
|
|
565
|
+
);
|
|
566
|
+
log(`[bracket] Market ${reason2} \u2014 cancelled entire bracket group ${alert.bracket_group}`);
|
|
567
|
+
}
|
|
568
|
+
const evtId = crypto.randomUUID();
|
|
569
|
+
await edb.run(
|
|
570
|
+
`INSERT INTO poly_watcher_events (id, agent_id, watcher_id, type, severity, title, summary, data) VALUES (?,?,?,?,?,?,?,?)`,
|
|
571
|
+
[
|
|
572
|
+
evtId,
|
|
573
|
+
alert.agent_id,
|
|
574
|
+
"alert_" + alert.id,
|
|
575
|
+
"market_resolved",
|
|
576
|
+
"warning",
|
|
577
|
+
`Market ${reason2}: ${alert.market_question || alert.token_id.slice(0, 16)}`,
|
|
578
|
+
`Alert auto-cancelled because market is ${reason2}. ${alert.bracket_group ? "Bracket group also cancelled." : ""}`,
|
|
579
|
+
JSON.stringify({ alert_id: alert.id, token_id: alert.token_id, reason: reason2, bracket_group: alert.bracket_group || null })
|
|
580
|
+
]
|
|
581
|
+
);
|
|
582
|
+
_engineEventCount++;
|
|
583
|
+
if (!eventsByAgent[alert.agent_id]) eventsByAgent[alert.agent_id] = { events: [], maxSeverity: "info" };
|
|
584
|
+
eventsByAgent[alert.agent_id].events.push({
|
|
585
|
+
type: "market_resolved",
|
|
586
|
+
severity: "warning",
|
|
587
|
+
title: `Market ${reason2}: ${alert.market_question || alert.token_id.slice(0, 16)}`,
|
|
588
|
+
summary: `Alert auto-cancelled. Market is ${reason2}.`,
|
|
589
|
+
data: { alert_id: alert.id, reason: reason2 }
|
|
590
|
+
});
|
|
591
|
+
if (eventsByAgent[alert.agent_id].maxSeverity === "info") eventsByAgent[alert.agent_id].maxSeverity = "warning";
|
|
592
|
+
}
|
|
593
|
+
} catch {
|
|
594
|
+
}
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (currentPrice >= 0.99 || currentPrice <= 0.01) {
|
|
598
|
+
try {
|
|
599
|
+
const marketInfo = await fetch(`${GAMMA_API}/markets?clob_token_ids=${alert.token_id}&limit=1`, { signal: AbortSignal.timeout(5e3) }).then((r) => r.ok ? r.json() : null).then((arr) => Array.isArray(arr) ? arr[0] : arr).catch(() => null);
|
|
600
|
+
if (marketInfo?.resolved || marketInfo?.closed) {
|
|
601
|
+
const reason2 = marketInfo.resolved ? "resolved" : "closed";
|
|
602
|
+
const winning = currentPrice >= 0.99;
|
|
603
|
+
log(`[alerts] Market ${reason2} (${winning ? "YES won" : "NO won"}) for alert ${alert.id}. Auto-cancelling.`);
|
|
604
|
+
await edb.run(`UPDATE poly_price_alerts SET triggered = 1, triggered_at = CURRENT_TIMESTAMP WHERE id = ?`, [alert.id]);
|
|
605
|
+
if (alert.bracket_group) {
|
|
606
|
+
await edb.run(
|
|
607
|
+
`UPDATE poly_price_alerts SET triggered = 1, triggered_at = CURRENT_TIMESTAMP WHERE bracket_group = ? AND id != ? AND triggered = 0`,
|
|
608
|
+
[alert.bracket_group, alert.id]
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
const evtId = crypto.randomUUID();
|
|
612
|
+
await edb.run(
|
|
613
|
+
`INSERT INTO poly_watcher_events (id, agent_id, watcher_id, type, severity, title, summary, data) VALUES (?,?,?,?,?,?,?,?)`,
|
|
614
|
+
[
|
|
615
|
+
evtId,
|
|
616
|
+
alert.agent_id,
|
|
617
|
+
"alert_" + alert.id,
|
|
618
|
+
"market_resolved",
|
|
619
|
+
"critical",
|
|
620
|
+
`Market ${reason2}: ${alert.market_question || alert.token_id.slice(0, 16)}`,
|
|
621
|
+
`Market ${reason2} at ${(currentPrice * 100).toFixed(1)}\xA2. ${winning ? "Position WON" : "Position LOST"}. All alerts cancelled.`,
|
|
622
|
+
JSON.stringify({ alert_id: alert.id, token_id: alert.token_id, reason: reason2, final_price: currentPrice, bracket_group: alert.bracket_group || null })
|
|
623
|
+
]
|
|
624
|
+
);
|
|
625
|
+
_engineEventCount++;
|
|
626
|
+
if (!eventsByAgent[alert.agent_id]) eventsByAgent[alert.agent_id] = { events: [], maxSeverity: "info" };
|
|
627
|
+
eventsByAgent[alert.agent_id].events.push({
|
|
628
|
+
type: "market_resolved",
|
|
629
|
+
severity: "critical",
|
|
630
|
+
title: `Market ${reason2}: ${alert.market_question || alert.token_id.slice(0, 16)}`,
|
|
631
|
+
summary: `Resolved at ${(currentPrice * 100).toFixed(1)}\xA2. ${winning ? "WON" : "LOST"}.`,
|
|
632
|
+
data: { alert_id: alert.id, reason: reason2, final_price: currentPrice }
|
|
633
|
+
});
|
|
634
|
+
eventsByAgent[alert.agent_id].maxSeverity = "critical";
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
} catch {
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
let fire = false;
|
|
641
|
+
let reason = "";
|
|
642
|
+
if (alert.condition === "above" && alert.target_price && currentPrice >= alert.target_price) {
|
|
643
|
+
fire = true;
|
|
644
|
+
reason = `Price ${(currentPrice * 100).toFixed(1)}\xA2 crossed above target ${(alert.target_price * 100).toFixed(1)}\xA2`;
|
|
645
|
+
} else if (alert.condition === "below" && alert.target_price && currentPrice <= alert.target_price) {
|
|
646
|
+
fire = true;
|
|
647
|
+
reason = `Price ${(currentPrice * 100).toFixed(1)}\xA2 dropped below target ${(alert.target_price * 100).toFixed(1)}\xA2`;
|
|
648
|
+
} else if (alert.condition === "pct_change" && alert.pct_change && alert.base_price) {
|
|
649
|
+
const change = Math.abs(currentPrice - alert.base_price) / alert.base_price * 100;
|
|
650
|
+
if (change >= alert.pct_change) {
|
|
651
|
+
fire = true;
|
|
652
|
+
const dir = currentPrice > alert.base_price ? "up" : "down";
|
|
653
|
+
reason = `Price moved ${dir} ${change.toFixed(1)}% (threshold: ${alert.pct_change}%)`;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (fire) {
|
|
657
|
+
const recentDup = await edb.all(
|
|
658
|
+
`SELECT id FROM poly_watcher_events WHERE watcher_id = ? AND created_at > ?`,
|
|
659
|
+
["alert_" + alert.id, dateAgoMin(5)]
|
|
660
|
+
).catch(() => []);
|
|
661
|
+
if (recentDup && recentDup.length > 0) continue;
|
|
662
|
+
const label = alert.market_question || alert.token_id.slice(0, 16) + "\u2026";
|
|
663
|
+
const evt = {
|
|
664
|
+
type: "price_alert",
|
|
665
|
+
severity: "critical",
|
|
666
|
+
// Alerts always wake the agent — they are explicit user-set triggers
|
|
667
|
+
title: `[ALERT] ${label}`,
|
|
668
|
+
summary: reason,
|
|
669
|
+
data: { alert_id: alert.id, token_id: alert.token_id, current_price: currentPrice, condition: alert.condition, target_price: alert.target_price, source: "poly_set_alert" }
|
|
670
|
+
};
|
|
671
|
+
const evtId = crypto.randomUUID();
|
|
672
|
+
await edb.run(
|
|
673
|
+
`INSERT INTO poly_watcher_events (id, agent_id, watcher_id, type, severity, title, summary, data) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
674
|
+
[evtId, alert.agent_id, "alert_" + alert.id, evt.type, evt.severity, evt.title, evt.summary, JSON.stringify(evt.data)]
|
|
675
|
+
);
|
|
676
|
+
_engineEventCount++;
|
|
677
|
+
if (!eventsByAgent[alert.agent_id]) {
|
|
678
|
+
eventsByAgent[alert.agent_id] = { events: [], maxSeverity: "info" };
|
|
679
|
+
}
|
|
680
|
+
eventsByAgent[alert.agent_id].events.push(evt);
|
|
681
|
+
eventsByAgent[alert.agent_id].maxSeverity = "critical";
|
|
682
|
+
if (alert.auto_trade_config) {
|
|
683
|
+
try {
|
|
684
|
+
const atc = typeof alert.auto_trade_config === "string" ? JSON.parse(alert.auto_trade_config) : alert.auto_trade_config;
|
|
685
|
+
if (atc && (atc.side || atc.action)) {
|
|
686
|
+
const tokenId = atc.token_id || alert.token_id;
|
|
687
|
+
let marketResolved = false;
|
|
688
|
+
if (currentPrice >= 0.99 || currentPrice <= 0.01) {
|
|
689
|
+
marketResolved = true;
|
|
690
|
+
}
|
|
691
|
+
if (marketResolved && (atc.side || atc.action)?.toUpperCase() === "SELL") {
|
|
692
|
+
log(`[watcher] Skipping auto-trade for alert ${alert.id}: market appears resolved (price=${currentPrice}). Cancelling alert.`);
|
|
693
|
+
await edb.run(`UPDATE poly_price_alerts SET triggered = 1, triggered_at = CURRENT_TIMESTAMP WHERE id = ?`, [alert.id]);
|
|
694
|
+
evt.data = evt.data || {};
|
|
695
|
+
evt.data.skipped = `Market resolved (price=${currentPrice}), trade not executed`;
|
|
696
|
+
evt.summary += ` | Skipped: market resolved`;
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
const { executeOrder } = await import("./polymarket-ZVUDH7EV.js");
|
|
700
|
+
const tradeId = `auto_${alert.id}_${Date.now()}`;
|
|
701
|
+
const tradeParams = {
|
|
702
|
+
token_id: tokenId,
|
|
703
|
+
side: atc.side || (String(atc.action).toUpperCase() === "SELL" ? "SELL" : "BUY"),
|
|
704
|
+
size: atc.size || atc.shares || 0,
|
|
705
|
+
price: atc.price || currentPrice,
|
|
706
|
+
market_question: alert.market_question || "Auto-trade from alert",
|
|
707
|
+
order_type: "GTC"
|
|
708
|
+
};
|
|
709
|
+
const dbCompat = edb.query ? edb : { query: edb.all?.bind(edb), execute: edb.run?.bind(edb), run: edb.run?.bind(edb), get: edb.get?.bind(edb), all: edb.all?.bind(edb) };
|
|
710
|
+
console.log(`[watcher] Auto-executing trade for alert ${alert.id}: ${tradeParams.side} ${tradeParams.size} @ ${tradeParams.price}`);
|
|
711
|
+
const result = await executeOrder(alert.agent_id, dbCompat, tradeId, tradeParams, "auto_alert");
|
|
712
|
+
evt.data = evt.data || {};
|
|
713
|
+
evt.data.auto_trade_result = result;
|
|
714
|
+
evt.summary += ` | Auto-trade: ${tradeParams.side} ${tradeParams.size} shares`;
|
|
715
|
+
console.log(`[watcher] Auto-trade result for alert ${alert.id}:`, JSON.stringify(result).slice(0, 200));
|
|
716
|
+
}
|
|
717
|
+
} catch (atErr) {
|
|
718
|
+
console.error(`[watcher] Auto-trade failed for alert ${alert.id}:`, atErr.message);
|
|
719
|
+
evt.data = evt.data || {};
|
|
720
|
+
evt.data.auto_trade_error = atErr.message;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (evt.data?.auto_trade_result || evt.data?.auto_trade_error) {
|
|
724
|
+
await edb.run(
|
|
725
|
+
`UPDATE poly_watcher_events SET summary = ?, data = ? WHERE id = ?`,
|
|
726
|
+
[evt.summary, JSON.stringify(evt.data), evtId]
|
|
727
|
+
).catch(() => {
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
if (alert.repeat_alert) {
|
|
731
|
+
await edb.run(`UPDATE poly_price_alerts SET base_price = ? WHERE id = ?`, [currentPrice, alert.id]);
|
|
732
|
+
} else {
|
|
733
|
+
await edb.run(`UPDATE poly_price_alerts SET triggered = 1, triggered_at = CURRENT_TIMESTAMP WHERE id = ?`, [alert.id]);
|
|
734
|
+
}
|
|
735
|
+
if (alert.bracket_group) {
|
|
736
|
+
try {
|
|
737
|
+
const { cancelBracketSibling } = await import("./polymarket-runtime-6LTI2BL7.js");
|
|
738
|
+
const dbCompat2 = edb.query ? edb : { query: edb.all?.bind(edb), execute: edb.run?.bind(edb), run: edb.run?.bind(edb), get: edb.get?.bind(edb), all: edb.all?.bind(edb) };
|
|
739
|
+
const cancelledId = await cancelBracketSibling(dbCompat2, alert.id, alert.bracket_group);
|
|
740
|
+
if (cancelledId) {
|
|
741
|
+
const role = alert.bracket_role === "take_profit" ? "Take-Profit" : "Stop-Loss";
|
|
742
|
+
const siblingRole = alert.bracket_role === "take_profit" ? "Stop-Loss" : "Take-Profit";
|
|
743
|
+
evt.summary += ` | ${role} hit \u2192 cancelled ${siblingRole} (${cancelledId})`;
|
|
744
|
+
log(`[bracket] ${role} fired (${alert.id}), cancelled ${siblingRole} (${cancelledId}) in group ${alert.bracket_group}`);
|
|
745
|
+
}
|
|
746
|
+
} catch (bracketErr) {
|
|
747
|
+
log(`[bracket] Failed to cancel sibling for ${alert.id}: ${bracketErr.message}`);
|
|
748
|
+
}
|
|
749
|
+
try {
|
|
750
|
+
await edb.run(
|
|
751
|
+
`UPDATE poly_exit_rules SET status = 'bracket_exited' WHERE agent_id = ? AND token_id = ? AND status = 'active'`,
|
|
752
|
+
[alert.agent_id, alert.token_id]
|
|
753
|
+
);
|
|
754
|
+
log(`[bracket\u2192exit] Deactivated exit rules for token ${alert.token_id} (bracket fired)`);
|
|
755
|
+
} catch {
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (!alert.bracket_group && alert.auto_trade_config) {
|
|
759
|
+
try {
|
|
760
|
+
const atc = typeof alert.auto_trade_config === "string" ? JSON.parse(alert.auto_trade_config) : alert.auto_trade_config;
|
|
761
|
+
if (atc?.side === "SELL" || atc?.action === "SELL") {
|
|
762
|
+
await edb.run(
|
|
763
|
+
`UPDATE poly_exit_rules SET status = 'alert_exited' WHERE agent_id = ? AND token_id = ? AND status = 'active'`,
|
|
764
|
+
[alert.agent_id, alert.token_id]
|
|
765
|
+
);
|
|
766
|
+
log(`[alert\u2192exit] Deactivated exit rules for token ${alert.token_id} (alert auto-sell fired)`);
|
|
767
|
+
}
|
|
768
|
+
} catch {
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (opts.onEvent) {
|
|
772
|
+
try {
|
|
773
|
+
opts.onEvent(alert.agent_id, evt);
|
|
774
|
+
} catch {
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
} catch {
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
} catch {
|
|
782
|
+
}
|
|
783
|
+
for (const [agentId, batch] of Object.entries(eventsByAgent)) {
|
|
784
|
+
if (batch.maxSeverity === "critical") {
|
|
785
|
+
await _maybeSpawnAgent(agentId, batch.events, edb, opts, log);
|
|
786
|
+
} else if (batch.maxSeverity === "warning") {
|
|
787
|
+
await _maybeSpawnAgent(agentId, batch.events, edb, opts, log);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (now - (_lastExitCheckMs || 0) < 6e4) {
|
|
791
|
+
} else try {
|
|
792
|
+
_lastExitCheckMs = now;
|
|
793
|
+
const exitRules = await edb.all(
|
|
794
|
+
`SELECT * FROM poly_exit_rules WHERE status = 'active'`
|
|
795
|
+
).catch(() => []) || [];
|
|
796
|
+
for (const rule of exitRules) {
|
|
797
|
+
try {
|
|
798
|
+
let currentPrice = priceCache?.[rule.token_id];
|
|
799
|
+
if (currentPrice === void 0) {
|
|
800
|
+
const mid = await fetch(`${CLOB_API}/midpoint?token_id=${rule.token_id}`, { signal: AbortSignal.timeout(5e3) }).then((r) => r.json());
|
|
801
|
+
currentPrice = parseFloat(mid?.mid || "0");
|
|
802
|
+
}
|
|
803
|
+
if (!currentPrice) {
|
|
804
|
+
try {
|
|
805
|
+
const marketInfo = await fetch(`${GAMMA_API}/markets?clob_token_ids=${rule.token_id}&limit=1`, { signal: AbortSignal.timeout(5e3) }).then((r) => r.ok ? r.json() : null).then((arr) => Array.isArray(arr) ? arr[0] : arr).catch(() => null);
|
|
806
|
+
if (marketInfo?.resolved || marketInfo?.closed || !marketInfo) {
|
|
807
|
+
await edb.run(`UPDATE poly_exit_rules SET status = 'market_resolved' WHERE id = ?`, [rule.id]);
|
|
808
|
+
log(`[exit-rules] Market resolved/closed \u2014 deactivated exit rule ${rule.id}`);
|
|
809
|
+
}
|
|
810
|
+
} catch {
|
|
811
|
+
}
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
if (currentPrice > (rule.highest_price || 0)) {
|
|
815
|
+
await edb.run(`UPDATE poly_exit_rules SET highest_price = ? WHERE id = ?`, [currentPrice, rule.id]).catch(() => {
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
let fireType = "";
|
|
819
|
+
let fireReason = "";
|
|
820
|
+
let sellPrice = currentPrice;
|
|
821
|
+
if (rule.trailing_stop_pct && rule.highest_price) {
|
|
822
|
+
const highestForTrail = Math.max(rule.highest_price, currentPrice);
|
|
823
|
+
const trailPrice = highestForTrail * (1 - rule.trailing_stop_pct / 100);
|
|
824
|
+
if (currentPrice <= trailPrice) {
|
|
825
|
+
fireType = "TRAILING_STOP";
|
|
826
|
+
fireReason = `Price ${(currentPrice * 100).toFixed(1)}\xA2 dropped ${rule.trailing_stop_pct}% from high of ${(highestForTrail * 100).toFixed(1)}\xA2 (trail trigger: ${(trailPrice * 100).toFixed(1)}\xA2)`;
|
|
827
|
+
sellPrice = currentPrice;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (!fireType && rule.time_exit) {
|
|
831
|
+
const exitTime = new Date(rule.time_exit).getTime();
|
|
832
|
+
if (Date.now() >= exitTime) {
|
|
833
|
+
fireType = "TIME_EXIT";
|
|
834
|
+
fireReason = `Time-based exit triggered (deadline: ${rule.time_exit}). Current price: ${(currentPrice * 100).toFixed(1)}\xA2`;
|
|
835
|
+
sellPrice = currentPrice;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (!fireType && rule.take_profit && currentPrice >= rule.take_profit) {
|
|
839
|
+
fireType = "TAKE_PROFIT";
|
|
840
|
+
fireReason = `Take-profit hit: ${(currentPrice * 100).toFixed(1)}\xA2 >= ${(rule.take_profit * 100).toFixed(1)}\xA2`;
|
|
841
|
+
sellPrice = currentPrice;
|
|
842
|
+
}
|
|
843
|
+
if (!fireType && rule.stop_loss && currentPrice <= rule.stop_loss) {
|
|
844
|
+
fireType = "STOP_LOSS";
|
|
845
|
+
fireReason = `Stop-loss hit: ${(currentPrice * 100).toFixed(1)}\xA2 <= ${(rule.stop_loss * 100).toFixed(1)}\xA2`;
|
|
846
|
+
sellPrice = currentPrice;
|
|
847
|
+
}
|
|
848
|
+
if (fireType) {
|
|
849
|
+
const recentDup = await edb.all(
|
|
850
|
+
`SELECT id FROM poly_watcher_events WHERE watcher_id = ? AND created_at > ?`,
|
|
851
|
+
["exit_" + rule.id, dateAgoMin(5)]
|
|
852
|
+
).catch(() => []);
|
|
853
|
+
if (recentDup && recentDup.length > 0) continue;
|
|
854
|
+
log(`[exit-rules] ${fireType} fired for ${rule.token_id}: ${fireReason}`);
|
|
855
|
+
let autoTradeResult = null;
|
|
856
|
+
let autoTradeError = null;
|
|
857
|
+
const sellSize = rule.position_size || 0;
|
|
858
|
+
if (sellSize > 0 && (currentPrice >= 0.99 || currentPrice <= 0.01)) {
|
|
859
|
+
log(`[exit-rules] Skipping auto-sell for ${rule.id}: market resolved (price=${currentPrice}). Deactivating rule.`);
|
|
860
|
+
await edb.run(`UPDATE poly_exit_rules SET status = 'cancelled' WHERE id = ?`, [rule.id]);
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (sellSize > 0) {
|
|
864
|
+
try {
|
|
865
|
+
const { executeOrder } = await import("./polymarket-ZVUDH7EV.js");
|
|
866
|
+
const tradeId = `exit_${rule.id}_${Date.now()}`;
|
|
867
|
+
const tradeParams = {
|
|
868
|
+
token_id: rule.token_id,
|
|
869
|
+
side: "SELL",
|
|
870
|
+
size: sellSize,
|
|
871
|
+
price: sellPrice,
|
|
872
|
+
market_question: `Auto-exit (${fireType}) \u2014 ${fireReason}`,
|
|
873
|
+
order_type: "GTC"
|
|
874
|
+
};
|
|
875
|
+
const dbCompat = edb.query ? edb : { query: edb.all?.bind(edb), execute: edb.run?.bind(edb), run: edb.run?.bind(edb), get: edb.get?.bind(edb), all: edb.all?.bind(edb) };
|
|
876
|
+
log(`[exit-rules] Auto-selling: ${sellSize} shares @ ${sellPrice}`);
|
|
877
|
+
autoTradeResult = await executeOrder(rule.agent_id, dbCompat, tradeId, tradeParams, "auto_exit_" + fireType.toLowerCase());
|
|
878
|
+
log(`[exit-rules] Auto-sell result: ${JSON.stringify(autoTradeResult).slice(0, 200)}`);
|
|
879
|
+
} catch (e) {
|
|
880
|
+
autoTradeError = e.message;
|
|
881
|
+
log(`[exit-rules] Auto-sell FAILED: ${e.message}`);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
await edb.run(`UPDATE poly_exit_rules SET status = 'triggered' WHERE id = ?`, [rule.id]);
|
|
885
|
+
try {
|
|
886
|
+
const relatedBrackets = await edb.all(
|
|
887
|
+
`SELECT id, bracket_group FROM poly_price_alerts WHERE agent_id = ? AND token_id = ? AND bracket_group IS NOT NULL AND triggered = 0`,
|
|
888
|
+
[rule.agent_id, rule.token_id]
|
|
889
|
+
).catch(() => []);
|
|
890
|
+
if (relatedBrackets && relatedBrackets.length > 0) {
|
|
891
|
+
await edb.run(
|
|
892
|
+
`UPDATE poly_price_alerts SET triggered = 1, triggered_at = CURRENT_TIMESTAMP WHERE agent_id = ? AND token_id = ? AND bracket_group IS NOT NULL AND triggered = 0`,
|
|
893
|
+
[rule.agent_id, rule.token_id]
|
|
894
|
+
);
|
|
895
|
+
log(`[exit-rules] Cross-cancelled ${relatedBrackets.length} bracket alert(s) for token ${rule.token_id}`);
|
|
896
|
+
}
|
|
897
|
+
} catch {
|
|
898
|
+
}
|
|
899
|
+
try {
|
|
900
|
+
await edb.run(
|
|
901
|
+
`UPDATE poly_price_alerts SET triggered = 1, triggered_at = CURRENT_TIMESTAMP WHERE agent_id = ? AND token_id = ? AND bracket_group IS NULL AND triggered = 0 AND (condition = 'above' OR condition = 'below')`,
|
|
902
|
+
[rule.agent_id, rule.token_id]
|
|
903
|
+
);
|
|
904
|
+
} catch {
|
|
905
|
+
}
|
|
906
|
+
const evtId = crypto.randomUUID();
|
|
907
|
+
const evt = {
|
|
908
|
+
type: "exit_triggered",
|
|
909
|
+
severity: "critical",
|
|
910
|
+
title: `[EXIT ${fireType}] ${rule.token_id.slice(0, 16)}\u2026`,
|
|
911
|
+
summary: `${fireReason}${autoTradeResult ? ` | Auto-SELL: ${sellSize} shares` : ""}${autoTradeError ? ` | SELL FAILED: ${autoTradeError}` : ""}`,
|
|
912
|
+
data: { exit_rule_id: rule.id, token_id: rule.token_id, fire_type: fireType, current_price: currentPrice, entry_price: rule.entry_price, auto_trade_result: autoTradeResult, auto_trade_error: autoTradeError }
|
|
913
|
+
};
|
|
914
|
+
await edb.run(
|
|
915
|
+
`INSERT INTO poly_watcher_events (id, agent_id, watcher_id, type, severity, title, summary, data) VALUES (?,?,?,?,?,?,?,?)`,
|
|
916
|
+
[evtId, rule.agent_id, "exit_" + rule.id, evt.type, evt.severity, evt.title, evt.summary, JSON.stringify(evt.data)]
|
|
917
|
+
);
|
|
918
|
+
_engineEventCount++;
|
|
919
|
+
if (!eventsByAgent[rule.agent_id]) eventsByAgent[rule.agent_id] = { events: [], maxSeverity: "info" };
|
|
920
|
+
eventsByAgent[rule.agent_id].events.push(evt);
|
|
921
|
+
eventsByAgent[rule.agent_id].maxSeverity = "critical";
|
|
922
|
+
}
|
|
923
|
+
} catch {
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
} catch {
|
|
927
|
+
}
|
|
928
|
+
for (const [agentId, batch] of Object.entries(eventsByAgent)) {
|
|
929
|
+
if (batch.maxSeverity === "critical" || batch.maxSeverity === "warning") {
|
|
930
|
+
await _maybeSpawnAgent(agentId, batch.events, edb, opts, log);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
const PROACTIVE_INTERVAL_MS = 15 * 6e4;
|
|
934
|
+
const proactiveElapsed = now - (_lastProactiveCheckMs || 0);
|
|
935
|
+
if (proactiveElapsed >= PROACTIVE_INTERVAL_MS) {
|
|
936
|
+
_lastProactiveCheckMs = now;
|
|
937
|
+
try {
|
|
938
|
+
const traderAgents = [...new Set(watchers.map((w) => w.agent_id))];
|
|
939
|
+
for (const agentId of traderAgents) {
|
|
940
|
+
const todayTrades = await edb.get(
|
|
941
|
+
`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at > ?`,
|
|
942
|
+
[agentId, dateAgo(24)]
|
|
943
|
+
).catch(() => ({ cnt: 0 }));
|
|
944
|
+
const tradeCount = parseInt(todayTrades?.cnt || "0");
|
|
945
|
+
const goal = await edb.get(
|
|
946
|
+
`SELECT target_value FROM poly_goals WHERE type = 'min_trades_daily' AND enabled = 1`,
|
|
947
|
+
[]
|
|
948
|
+
).catch(() => null);
|
|
949
|
+
const targetTrades = goal?.target_value || 15;
|
|
950
|
+
if (tradeCount < targetTrades) {
|
|
951
|
+
try {
|
|
952
|
+
const walletRow = await edb.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]).catch(() => null);
|
|
953
|
+
if (walletRow?.address) {
|
|
954
|
+
const balRes = await fetch(`https://rpc.ankr.com/polygon`, {
|
|
955
|
+
method: "POST",
|
|
956
|
+
headers: { "Content-Type": "application/json" },
|
|
957
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_call", params: [{ to: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", data: "0x70a08231000000000000000000000000" + walletRow.address.slice(2).toLowerCase() }, "latest"] }),
|
|
958
|
+
signal: AbortSignal.timeout(5e3)
|
|
959
|
+
});
|
|
960
|
+
const balData = await balRes.json();
|
|
961
|
+
const balUSDC = parseInt(balData?.result || "0", 16) / 1e6;
|
|
962
|
+
if (balUSDC < 5) {
|
|
963
|
+
log(`[poly-watcher] Proactive: skipped agent ${agentId.slice(0, 8)} \u2014 balance $${balUSDC.toFixed(2)} < $5 minimum`);
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
} catch {
|
|
968
|
+
}
|
|
969
|
+
const lastSpawn = _lastSpawnByAgent[agentId] || 0;
|
|
970
|
+
if (now - lastSpawn >= SPAWN_COOLDOWN_MS) {
|
|
971
|
+
const agentRow = await edb.get(
|
|
972
|
+
`SELECT config FROM managed_agents WHERE id = ?`,
|
|
973
|
+
[agentId]
|
|
974
|
+
).catch(() => null);
|
|
975
|
+
const agentConfig = typeof agentRow?.config === "string" ? JSON.parse(agentRow.config) : agentRow?.config;
|
|
976
|
+
const dep = agentConfig?.deployment;
|
|
977
|
+
const port = dep?.port || dep?.config?.local?.port || 3101;
|
|
978
|
+
const secret = process.env.AGENT_RUNTIME_SECRET || "";
|
|
979
|
+
const wakeMsg = `[PROACTIVE TRADING CHECK] You have ${tradeCount}/${targetTrades} trades today \u2014 ${targetTrades - tradeCount} more needed.
|
|
980
|
+
|
|
981
|
+
MANDATORY SEQUENCE (do ALL of these IN ORDER before placing any new trades):
|
|
982
|
+
|
|
983
|
+
1. poly_watcher_events action=check \u2014 Check for unread signals first
|
|
984
|
+
2. poly_goals action=check \u2014 Review your performance targets
|
|
985
|
+
3. poly_drawdown_monitor action=check \u2014 Check portfolio-level risk. HALT if drawdown > 15%
|
|
986
|
+
4. poly_calibration \u2014 Review your prediction accuracy. Are you over/under-confident?
|
|
987
|
+
5. poly_pnl_attribution \u2014 Which strategies and categories are making/losing money?
|
|
988
|
+
6. poly_strategy_performance \u2014 Which strategies are actually profitable? Double down on winners.
|
|
989
|
+
7. poly_get_positions \u2014 Review all open positions, check P&L drift
|
|
990
|
+
8. poly_exit_strategy action=check \u2014 Check if any exit conditions triggered
|
|
991
|
+
|
|
992
|
+
ONLY AFTER completing the above analysis:
|
|
993
|
+
9. poly_screen_markets with strategies: high_volume, momentum, contested, closing_soon
|
|
994
|
+
10. poly_search_markets for sports: NBA, MLB, Premier League, Champions League, UFC
|
|
995
|
+
11. For EACH candidate: run poly_quick_analysis + poly_resolution_risk + poly_manipulation_detector
|
|
996
|
+
12. Use poly_kelly_criterion for position sizing \u2014 do NOT just buy random $5 positions
|
|
997
|
+
13. For orders > $50: use poly_scale_in (TWAP/VWAP), NOT market orders
|
|
998
|
+
14. For time-sensitive opportunities: use poly_sniper with trailing limits
|
|
999
|
+
15. Consider poly_hedge for correlated positions
|
|
1000
|
+
|
|
1001
|
+
QUALITY > QUANTITY. Each trade must have analysis backing it. No blind trades.`;
|
|
1002
|
+
const messaging = agentConfig?.messagingChannels || {};
|
|
1003
|
+
const managerName = agentConfig?.manager?.name || agentConfig?.managerName || "Manager";
|
|
1004
|
+
const managerEmail = agentConfig?.manager?.email || agentConfig?.managerEmail || "";
|
|
1005
|
+
let wakeSource = "system";
|
|
1006
|
+
let wakeSenderId = "watcher@system";
|
|
1007
|
+
let wakeSpaceId = "watcher_proactive";
|
|
1008
|
+
if (messaging.telegram?.chatId) {
|
|
1009
|
+
wakeSource = "telegram";
|
|
1010
|
+
wakeSenderId = messaging.telegram.chatId;
|
|
1011
|
+
wakeSpaceId = messaging.telegram.chatId;
|
|
1012
|
+
} else if (messaging.whatsapp?.phoneNumber) {
|
|
1013
|
+
wakeSource = "whatsapp";
|
|
1014
|
+
wakeSenderId = messaging.whatsapp.phoneNumber;
|
|
1015
|
+
wakeSpaceId = messaging.whatsapp.phoneNumber;
|
|
1016
|
+
} else if (messaging.google_chat?.spaceId) {
|
|
1017
|
+
wakeSource = "google_chat";
|
|
1018
|
+
wakeSenderId = managerEmail || "watcher@system";
|
|
1019
|
+
wakeSpaceId = messaging.google_chat.spaceId;
|
|
1020
|
+
} else if (managerEmail) {
|
|
1021
|
+
wakeSource = "email";
|
|
1022
|
+
wakeSenderId = managerEmail;
|
|
1023
|
+
wakeSpaceId = managerEmail;
|
|
1024
|
+
}
|
|
1025
|
+
try {
|
|
1026
|
+
const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {
|
|
1027
|
+
method: "POST",
|
|
1028
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${secret}` },
|
|
1029
|
+
body: JSON.stringify({
|
|
1030
|
+
source: wakeSource,
|
|
1031
|
+
senderName: managerName,
|
|
1032
|
+
senderEmail: wakeSenderId,
|
|
1033
|
+
spaceName: "DM",
|
|
1034
|
+
spaceId: wakeSpaceId,
|
|
1035
|
+
threadId: "",
|
|
1036
|
+
isDM: true,
|
|
1037
|
+
messageText: wakeMsg,
|
|
1038
|
+
isManager: true
|
|
1039
|
+
}),
|
|
1040
|
+
signal: AbortSignal.timeout(1e4)
|
|
1041
|
+
});
|
|
1042
|
+
if (resp.ok) {
|
|
1043
|
+
log(`[poly-watcher] Proactive trading: woke agent ${agentId.slice(0, 8)} (${tradeCount}/${targetTrades} trades today)`);
|
|
1044
|
+
_lastSpawnByAgent[agentId] = now;
|
|
1045
|
+
} else {
|
|
1046
|
+
const body = await resp.text().catch(() => "");
|
|
1047
|
+
log(`[poly-watcher] Proactive: wake failed (${resp.status}): ${body.slice(0, 200)}`);
|
|
1048
|
+
}
|
|
1049
|
+
} catch (e) {
|
|
1050
|
+
log(`[poly-watcher] Proactive: wake error: ${e.message}`);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
} catch {
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
await edb.run(`DELETE FROM poly_watcher_events WHERE acknowledged = 1 AND created_at < ?`, [dateAgo(168)]).catch(() => {
|
|
1059
|
+
});
|
|
1060
|
+
await edb.run(`DELETE FROM poly_analysis_cache WHERE created_at < ?`, [dateAgo(24)]).catch(() => {
|
|
1061
|
+
});
|
|
1062
|
+
await edb.run(`DELETE FROM poly_signal_buffer WHERE expires_at < ?`, [(/* @__PURE__ */ new Date()).toISOString()]).catch(() => {
|
|
1063
|
+
});
|
|
1064
|
+
await edb.run(`DELETE FROM poly_price_alerts WHERE triggered = 1 AND triggered_at < ?`, [dateAgo(720)]).catch(() => {
|
|
1065
|
+
});
|
|
1066
|
+
} catch (e) {
|
|
1067
|
+
}
|
|
1068
|
+
}, TICK_MS);
|
|
1069
|
+
}
|
|
1070
|
+
async function _maybeSpawnAgent(agentId, events, edb, opts, log) {
|
|
1071
|
+
const getRuntime = opts.getRuntime;
|
|
1072
|
+
if (!getRuntime) return;
|
|
1073
|
+
const runtime = getRuntime();
|
|
1074
|
+
if (!runtime) return;
|
|
1075
|
+
const lastSpawn = _lastSpawnByAgent[agentId] || 0;
|
|
1076
|
+
const now = Date.now();
|
|
1077
|
+
if (now - lastSpawn < SPAWN_COOLDOWN_MS) {
|
|
1078
|
+
log(`[poly-watcher] Spawn cooldown for ${agentId} (${Math.ceil((SPAWN_COOLDOWN_MS - (now - lastSpawn)) / 1e3)}s)`);
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
try {
|
|
1082
|
+
const activeSessions = await runtime.listSessions?.(agentId, { status: "active", limit: 1 });
|
|
1083
|
+
if (activeSessions && activeSessions.length > 0) {
|
|
1084
|
+
const sessionId = activeSessions[0].id;
|
|
1085
|
+
const msg = _buildWakeMessage(events);
|
|
1086
|
+
try {
|
|
1087
|
+
await runtime.sendMessage(sessionId, msg);
|
|
1088
|
+
log(`[poly-watcher] Sent ${events.length} signals to active session ${sessionId}`);
|
|
1089
|
+
_lastSpawnByAgent[agentId] = now;
|
|
1090
|
+
_spawnCount++;
|
|
1091
|
+
} catch (e) {
|
|
1092
|
+
log(`[poly-watcher] Failed to send to session: ${e.message}`);
|
|
1093
|
+
}
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
} catch {
|
|
1097
|
+
}
|
|
1098
|
+
try {
|
|
1099
|
+
let orgId = "default";
|
|
1100
|
+
if (opts.getAgentConfig) {
|
|
1101
|
+
const cfg = opts.getAgentConfig(agentId);
|
|
1102
|
+
if (cfg?.org_id) orgId = cfg.org_id;
|
|
1103
|
+
}
|
|
1104
|
+
const msg = _buildWakeMessage(events);
|
|
1105
|
+
const session = await runtime.spawnSession({ agentId, orgId, message: msg });
|
|
1106
|
+
_lastSpawnByAgent[agentId] = now;
|
|
1107
|
+
_spawnCount++;
|
|
1108
|
+
log(`[poly-watcher] [ALERT] Spawned session ${session.id} for ${agentId} \u2014 ${events.length} signals`);
|
|
1109
|
+
await edb.run(
|
|
1110
|
+
`INSERT INTO poly_watcher_events (id, agent_id, watcher_id, type, severity, title, summary, data) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1111
|
+
[
|
|
1112
|
+
crypto.randomUUID(),
|
|
1113
|
+
agentId,
|
|
1114
|
+
null,
|
|
1115
|
+
"agent_spawn",
|
|
1116
|
+
"info",
|
|
1117
|
+
`Agent session spawned with ${events.length} signals`,
|
|
1118
|
+
`Session ${session.id}`,
|
|
1119
|
+
JSON.stringify({ sessionId: session.id, eventCount: events.length, events: events.slice(0, 5).map((e) => e.title) })
|
|
1120
|
+
]
|
|
1121
|
+
).catch(() => {
|
|
1122
|
+
});
|
|
1123
|
+
} catch (e) {
|
|
1124
|
+
log(`[poly-watcher] Spawn failed for ${agentId}: ${e.message}`);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
function _buildWakeMessage(events) {
|
|
1128
|
+
const critical = events.filter((e) => e.severity === "critical");
|
|
1129
|
+
const warning = events.filter((e) => e.severity === "warning");
|
|
1130
|
+
const info = events.filter((e) => e.severity === "info");
|
|
1131
|
+
let msg = `[ALERT] WATCHER ALERT \u2014 ${events.length} signal${events.length > 1 ? "s" : ""} detected:
|
|
1132
|
+
|
|
1133
|
+
`;
|
|
1134
|
+
const autoTrades = events.filter((e) => e.data?.auto_trade_result || e.data?.auto_trade_error);
|
|
1135
|
+
if (autoTrades.length > 0) {
|
|
1136
|
+
msg += `\u26A1 AUTO-TRADES EXECUTED (${autoTrades.length}):
|
|
1137
|
+
`;
|
|
1138
|
+
for (const e of autoTrades) {
|
|
1139
|
+
if (e.data?.auto_trade_result) {
|
|
1140
|
+
msg += ` \u2705 ${e.title} \u2014 Trade placed. REVIEW THIS IMMEDIATELY.
|
|
1141
|
+
`;
|
|
1142
|
+
} else if (e.data?.auto_trade_error) {
|
|
1143
|
+
msg += ` \u274C ${e.title} \u2014 Trade FAILED: ${e.data.auto_trade_error}. Manual action needed.
|
|
1144
|
+
`;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
msg += `
|
|
1148
|
+
You were woken because trades were auto-executed. IMPORTANT ACTIONS:
|
|
1149
|
+
`;
|
|
1150
|
+
msg += `1. Check positions with poly_get_positions and verify the trades are correct.
|
|
1151
|
+
`;
|
|
1152
|
+
msg += `2. Check your USDC balance \u2014 if you have idle cash from sells, REINVEST IT. Find new opportunities with poly_search_markets and deploy capital.
|
|
1153
|
+
`;
|
|
1154
|
+
msg += `3. Set up new bracket orders (TP/SL) for any new positions.
|
|
1155
|
+
|
|
1156
|
+
`;
|
|
1157
|
+
}
|
|
1158
|
+
if (critical.length > 0) {
|
|
1159
|
+
msg += `CRITICAL (${critical.length}):
|
|
1160
|
+
`;
|
|
1161
|
+
for (const e of critical.slice(0, MAX_BATCH_EVENTS)) {
|
|
1162
|
+
msg += ` \u2022 ${e.title}${e.summary ? " \u2014 " + e.summary : ""}
|
|
1163
|
+
`;
|
|
1164
|
+
if (e.data?.reasoning) msg += ` AI Analysis: ${e.data.reasoning}
|
|
1165
|
+
`;
|
|
1166
|
+
if (e.data?.recommended_action) msg += ` Recommended: ${e.data.recommended_action}
|
|
1167
|
+
`;
|
|
1168
|
+
}
|
|
1169
|
+
msg += "\n";
|
|
1170
|
+
}
|
|
1171
|
+
if (warning.length > 0) {
|
|
1172
|
+
msg += `WARNING (${warning.length}):
|
|
1173
|
+
`;
|
|
1174
|
+
for (const e of warning.slice(0, MAX_BATCH_EVENTS)) {
|
|
1175
|
+
msg += ` \u2022 ${e.title}${e.summary ? " \u2014 " + e.summary : ""}
|
|
1176
|
+
`;
|
|
1177
|
+
}
|
|
1178
|
+
msg += "\n";
|
|
1179
|
+
}
|
|
1180
|
+
if (info.length > 0) {
|
|
1181
|
+
msg += `INFO (${info.length}):
|
|
1182
|
+
`;
|
|
1183
|
+
for (const e of info.slice(0, 3)) msg += ` \u2022 ${e.title}
|
|
1184
|
+
`;
|
|
1185
|
+
if (info.length > 3) msg += ` ... and ${info.length - 3} more
|
|
1186
|
+
`;
|
|
1187
|
+
msg += "\n";
|
|
1188
|
+
}
|
|
1189
|
+
const hasNews = events.some((e) => e.type === "news_intelligence" || e.type === "geopolitical");
|
|
1190
|
+
if (hasNews && !autoTrades.length) {
|
|
1191
|
+
msg += `
|
|
1192
|
+
\u{1F4F0} NEWS INTELLIGENCE DETECTED \u2014 ACTION REQUIRED:
|
|
1193
|
+
`;
|
|
1194
|
+
msg += `1. Read the headlines above carefully. Extract key topics, names, events.
|
|
1195
|
+
`;
|
|
1196
|
+
msg += `2. Use poly_search_markets to find Polymarket markets related to these headlines.
|
|
1197
|
+
`;
|
|
1198
|
+
msg += `3. For each relevant market: check the odds, assess if the news shifts probability, and evaluate if there's an edge.
|
|
1199
|
+
`;
|
|
1200
|
+
msg += `4. If you find a market with a clear edge (news makes an outcome more/less likely than current odds suggest), place a trade.
|
|
1201
|
+
`;
|
|
1202
|
+
msg += `5. Check your USDC balance with poly_get_balance \u2014 deploy idle cash into high-conviction opportunities.
|
|
1203
|
+
`;
|
|
1204
|
+
msg += `6. Information is power in prediction markets. Headlines that move real-world probabilities before the market prices them in = alpha.
|
|
1205
|
+
`;
|
|
1206
|
+
msg += `
|
|
1207
|
+
Do NOT just acknowledge this. RESEARCH and ACT on the intelligence.
|
|
1208
|
+
`;
|
|
1209
|
+
} else {
|
|
1210
|
+
msg += `
|
|
1211
|
+
Run poly_watcher_events action=check for full details. Assess and act.
|
|
1212
|
+
`;
|
|
1213
|
+
msg += `ALWAYS check your USDC balance \u2014 idle cash should be deployed into new positions. Money sitting is money wasted.
|
|
1214
|
+
`;
|
|
1215
|
+
}
|
|
1216
|
+
return msg;
|
|
1217
|
+
}
|
|
1218
|
+
function _stopFastLoop(log) {
|
|
1219
|
+
if (engineInterval) {
|
|
1220
|
+
clearInterval(engineInterval);
|
|
1221
|
+
engineInterval = null;
|
|
1222
|
+
log("[poly-watcher] No active watchers \u2014 paused (idle polling continues)");
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
function controlWatcherEngine(action) {
|
|
1226
|
+
const log = _engineOpts?.log || console.log;
|
|
1227
|
+
if (action === "stop") {
|
|
1228
|
+
_stopFastLoop(log);
|
|
1229
|
+
if (idleInterval) {
|
|
1230
|
+
clearInterval(idleInterval);
|
|
1231
|
+
idleInterval = null;
|
|
1232
|
+
}
|
|
1233
|
+
log("[poly-watcher] Engine fully stopped");
|
|
1234
|
+
} else if (action === "start" && _engineDb) {
|
|
1235
|
+
startWatcherEngine(_engineDb, _engineOpts || void 0);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
function stopWatcherEngine() {
|
|
1239
|
+
if (engineInterval) {
|
|
1240
|
+
clearInterval(engineInterval);
|
|
1241
|
+
engineInterval = null;
|
|
1242
|
+
}
|
|
1243
|
+
if (idleInterval) {
|
|
1244
|
+
clearInterval(idleInterval);
|
|
1245
|
+
idleInterval = null;
|
|
1246
|
+
}
|
|
1247
|
+
_engineStartedAt = null;
|
|
1248
|
+
}
|
|
1249
|
+
async function runWatcher(type, config, agentId, edb) {
|
|
1250
|
+
switch (type) {
|
|
1251
|
+
case "price_level":
|
|
1252
|
+
return checkPriceLevel(config);
|
|
1253
|
+
case "price_change":
|
|
1254
|
+
return checkPriceChange(config, edb);
|
|
1255
|
+
case "market_scan":
|
|
1256
|
+
return checkMarketScan(config, edb);
|
|
1257
|
+
case "news_intelligence":
|
|
1258
|
+
return checkNewsIntelligence(config, agentId, edb);
|
|
1259
|
+
case "news_monitor":
|
|
1260
|
+
return checkNewsIntelligence(config, agentId, edb);
|
|
1261
|
+
// Alias for backward compat
|
|
1262
|
+
case "crypto_price":
|
|
1263
|
+
return checkCryptoPrice(config);
|
|
1264
|
+
case "resolution_watch":
|
|
1265
|
+
return checkResolution(config);
|
|
1266
|
+
case "portfolio_drift":
|
|
1267
|
+
return checkPortfolioDrift(config, agentId, edb);
|
|
1268
|
+
case "volume_surge":
|
|
1269
|
+
return checkVolumeSurge(config, edb);
|
|
1270
|
+
case "geopolitical":
|
|
1271
|
+
return checkGeopolitical(config, agentId, edb);
|
|
1272
|
+
case "cross_signal":
|
|
1273
|
+
return checkCrossSignal(config, agentId, edb);
|
|
1274
|
+
case "arbitrage_scan":
|
|
1275
|
+
return checkArbitrageScan(config);
|
|
1276
|
+
case "sentiment_shift":
|
|
1277
|
+
return checkSentimentShift(config, agentId, edb);
|
|
1278
|
+
default:
|
|
1279
|
+
return [];
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
async function checkPriceLevel(config) {
|
|
1283
|
+
const { token_id, direction, threshold, market_question } = config;
|
|
1284
|
+
if (!token_id || !threshold) return [];
|
|
1285
|
+
try {
|
|
1286
|
+
const res = await fetch(`${CLOB_API}/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(5e3) });
|
|
1287
|
+
const data = await res.json();
|
|
1288
|
+
const price = parseFloat(data.mid || data.price || "0");
|
|
1289
|
+
if (!price) return [];
|
|
1290
|
+
const label = market_question || token_id.slice(0, 12) + "\u2026";
|
|
1291
|
+
if (direction === "above" && price >= threshold) {
|
|
1292
|
+
return [{
|
|
1293
|
+
type: "price_level",
|
|
1294
|
+
severity: "warning",
|
|
1295
|
+
title: `[UP] ${label} crossed ${threshold}`,
|
|
1296
|
+
summary: `Current: ${price.toFixed(4)} (threshold: ${threshold})`,
|
|
1297
|
+
data: { token_id, price, threshold, direction }
|
|
1298
|
+
}];
|
|
1299
|
+
}
|
|
1300
|
+
if (direction === "below" && price <= threshold) {
|
|
1301
|
+
return [{
|
|
1302
|
+
type: "price_level",
|
|
1303
|
+
severity: "warning",
|
|
1304
|
+
title: `[DOWN] ${label} dropped below ${threshold}`,
|
|
1305
|
+
summary: `Current: ${price.toFixed(4)} (threshold: ${threshold})`,
|
|
1306
|
+
data: { token_id, price, threshold, direction }
|
|
1307
|
+
}];
|
|
1308
|
+
}
|
|
1309
|
+
} catch {
|
|
1310
|
+
}
|
|
1311
|
+
return [];
|
|
1312
|
+
}
|
|
1313
|
+
async function checkPriceChange(config, edb) {
|
|
1314
|
+
const { token_id, pct_threshold, market_question } = config;
|
|
1315
|
+
if (!token_id || !pct_threshold) return [];
|
|
1316
|
+
try {
|
|
1317
|
+
const res = await fetch(`${CLOB_API}/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(5e3) });
|
|
1318
|
+
const data = await res.json();
|
|
1319
|
+
const price = parseFloat(data.mid || data.price || "0");
|
|
1320
|
+
if (!price) return [];
|
|
1321
|
+
const cached = await edb.get(`SELECT * FROM poly_price_cache WHERE token_id = ?`, [token_id]);
|
|
1322
|
+
await edb.run(
|
|
1323
|
+
`INSERT INTO poly_price_cache (token_id, price, prev_price, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
|
1324
|
+
ON CONFLICT(token_id) DO UPDATE SET prev_price = price, price = ?, updated_at = CURRENT_TIMESTAMP`,
|
|
1325
|
+
[token_id, price, cached?.price || price, price]
|
|
1326
|
+
);
|
|
1327
|
+
if (cached?.price && cached.price > 0) {
|
|
1328
|
+
const changePct = (price - cached.price) / cached.price * 100;
|
|
1329
|
+
if (Math.abs(changePct) >= pct_threshold) {
|
|
1330
|
+
const label = market_question || token_id.slice(0, 12) + "\u2026";
|
|
1331
|
+
return [{
|
|
1332
|
+
type: "price_change",
|
|
1333
|
+
severity: Math.abs(changePct) >= pct_threshold * 2 ? "critical" : "warning",
|
|
1334
|
+
title: `${changePct > 0 ? "[UP]" : "[DOWN]"} ${label} moved ${changePct > 0 ? "+" : ""}${changePct.toFixed(1)}%`,
|
|
1335
|
+
summary: `${cached.price.toFixed(4)} \u2192 ${price.toFixed(4)}`,
|
|
1336
|
+
data: { token_id, price, prev_price: cached.price, change_pct: changePct }
|
|
1337
|
+
}];
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
} catch {
|
|
1341
|
+
}
|
|
1342
|
+
return [];
|
|
1343
|
+
}
|
|
1344
|
+
async function checkMarketScan(config, edb) {
|
|
1345
|
+
const { keywords, min_volume, min_liquidity } = config;
|
|
1346
|
+
if (!keywords || !keywords.length) return [];
|
|
1347
|
+
try {
|
|
1348
|
+
const q = encodeURIComponent(keywords.join(" "));
|
|
1349
|
+
const res = await fetch(`${GAMMA_API}/markets?closed=false&limit=10&order=volume24hr&ascending=false&search=${q}`, { signal: AbortSignal.timeout(8e3) });
|
|
1350
|
+
const markets = await res.json();
|
|
1351
|
+
const events = [];
|
|
1352
|
+
for (const m of markets.slice(0, 5)) {
|
|
1353
|
+
if (min_volume && (m.volume24hr || 0) < min_volume) continue;
|
|
1354
|
+
if (min_liquidity && (m.liquidityClob || 0) < min_liquidity) continue;
|
|
1355
|
+
const existing = await edb.get(
|
|
1356
|
+
`SELECT id FROM poly_watcher_events WHERE type = 'market_scan' AND data LIKE ? AND created_at > ?`,
|
|
1357
|
+
["%" + m.conditionId + "%", dateAgo(24)]
|
|
1358
|
+
).catch(() => null);
|
|
1359
|
+
if (existing) continue;
|
|
1360
|
+
events.push({
|
|
1361
|
+
type: "market_scan",
|
|
1362
|
+
severity: "info",
|
|
1363
|
+
title: `[SCAN] New market: ${m.question?.slice(0, 80)}`,
|
|
1364
|
+
summary: `Volume: $${(m.volume24hr || 0).toFixed(0)} | Liquidity: $${(m.liquidityClob || 0).toFixed(0)}`,
|
|
1365
|
+
data: { condition_id: m.conditionId, question: m.question, volume: m.volume24hr, liquidity: m.liquidityClob }
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
return events;
|
|
1369
|
+
} catch {
|
|
1370
|
+
}
|
|
1371
|
+
return [];
|
|
1372
|
+
}
|
|
1373
|
+
async function checkNewsIntelligence(config, agentId, edb) {
|
|
1374
|
+
const { keywords, sources, watched_markets } = config;
|
|
1375
|
+
if (!keywords || !keywords.length) return [];
|
|
1376
|
+
try {
|
|
1377
|
+
const newsPromises = [];
|
|
1378
|
+
const q = encodeURIComponent(keywords.join(" OR "));
|
|
1379
|
+
newsPromises.push(
|
|
1380
|
+
fetch(`https://news.google.com/rss/search?q=${q}&hl=en-US&gl=US&ceid=US:en`, { signal: AbortSignal.timeout(8e3) }).then((r) => r.text()).then((xml) => {
|
|
1381
|
+
const items = xml.split("<item>").slice(1, 8);
|
|
1382
|
+
return items.map((item) => {
|
|
1383
|
+
const titleMatch = item.match(/<title><!\[CDATA\[(.*?)\]\]><\/title>|<title>(.*?)<\/title>/);
|
|
1384
|
+
const linkMatch = item.match(/<link>(.*?)<\/link>/);
|
|
1385
|
+
const pubMatch = item.match(/<pubDate>(.*?)<\/pubDate>/);
|
|
1386
|
+
return {
|
|
1387
|
+
title: titleMatch?.[1] || titleMatch?.[2] || "",
|
|
1388
|
+
source: "Google News",
|
|
1389
|
+
url: linkMatch?.[1] || "",
|
|
1390
|
+
date: pubMatch?.[1] || ""
|
|
1391
|
+
};
|
|
1392
|
+
}).filter((n) => n.title);
|
|
1393
|
+
}).catch(() => [])
|
|
1394
|
+
);
|
|
1395
|
+
newsPromises.push(
|
|
1396
|
+
fetch("https://rsshub.app/apnews/topics/world-news", { signal: AbortSignal.timeout(8e3) }).then((r) => r.text()).then((xml) => {
|
|
1397
|
+
const items = xml.split("<item>").slice(1, 6);
|
|
1398
|
+
return items.map((item) => {
|
|
1399
|
+
const titleMatch = item.match(/<title><!\[CDATA\[(.*?)\]\]><\/title>|<title>(.*?)<\/title>/);
|
|
1400
|
+
const linkMatch = item.match(/<link>(.*?)<\/link>/);
|
|
1401
|
+
return { title: titleMatch?.[1] || titleMatch?.[2] || "", source: "AP News", url: linkMatch?.[1] || "" };
|
|
1402
|
+
}).filter((n) => n.title);
|
|
1403
|
+
}).catch(() => [])
|
|
1404
|
+
);
|
|
1405
|
+
newsPromises.push(
|
|
1406
|
+
fetch("https://rsshub.app/reuters/world", { signal: AbortSignal.timeout(8e3) }).then((r) => r.text()).then((xml) => {
|
|
1407
|
+
const items = xml.split("<item>").slice(1, 6);
|
|
1408
|
+
return items.map((item) => {
|
|
1409
|
+
const titleMatch = item.match(/<title><!\[CDATA\[(.*?)\]\]><\/title>|<title>(.*?)<\/title>/);
|
|
1410
|
+
const linkMatch = item.match(/<link>(.*?)<\/link>/);
|
|
1411
|
+
return { title: titleMatch?.[1] || titleMatch?.[2] || "", source: "Reuters", url: linkMatch?.[1] || "" };
|
|
1412
|
+
}).filter((n) => n.title);
|
|
1413
|
+
}).catch(() => [])
|
|
1414
|
+
);
|
|
1415
|
+
const allNews = (await Promise.all(newsPromises)).flat();
|
|
1416
|
+
if (allNews.length === 0) return [];
|
|
1417
|
+
const now = Date.now();
|
|
1418
|
+
const relevant = allNews.filter((n) => {
|
|
1419
|
+
if (n.date) {
|
|
1420
|
+
const age = now - new Date(n.date).getTime();
|
|
1421
|
+
if (age > 6 * 60 * 60 * 1e3) return false;
|
|
1422
|
+
}
|
|
1423
|
+
const lower = n.title.toLowerCase();
|
|
1424
|
+
return keywords.some((k) => lower.includes(k.toLowerCase()));
|
|
1425
|
+
});
|
|
1426
|
+
if (relevant.length === 0) return [];
|
|
1427
|
+
const fresh = [];
|
|
1428
|
+
for (const n of relevant) {
|
|
1429
|
+
const titleKey = n.title.slice(0, 50).replace(/[%_\\]/g, "\\$&");
|
|
1430
|
+
const existing = await edb.get(
|
|
1431
|
+
`SELECT id FROM poly_watcher_events WHERE agent_id = ? AND title LIKE ? AND created_at > ?`,
|
|
1432
|
+
[agentId, "%" + titleKey + "%", dateAgo(6)]
|
|
1433
|
+
).catch(() => null);
|
|
1434
|
+
if (!existing) fresh.push(n);
|
|
1435
|
+
}
|
|
1436
|
+
if (fresh.length === 0) return [];
|
|
1437
|
+
const analysis = await analyzeWithAI(
|
|
1438
|
+
`Recent headlines relevant to prediction markets:
|
|
1439
|
+
${fresh.map((n) => `- ${n.title} (${n.source})`).join("\n")}`,
|
|
1440
|
+
{ watchedMarkets: watched_markets },
|
|
1441
|
+
agentId,
|
|
1442
|
+
edb
|
|
1443
|
+
);
|
|
1444
|
+
if (analysis) _engineAnalysisCount++;
|
|
1445
|
+
const events = [];
|
|
1446
|
+
const impactToSeverity = {
|
|
1447
|
+
none: "info",
|
|
1448
|
+
low: "info",
|
|
1449
|
+
medium: "warning",
|
|
1450
|
+
high: "warning",
|
|
1451
|
+
critical: "critical"
|
|
1452
|
+
};
|
|
1453
|
+
let severity = analysis ? impactToSeverity[analysis.impact] || "info" : "info";
|
|
1454
|
+
if (analysis && analysis.impact === "none") {
|
|
1455
|
+
return [];
|
|
1456
|
+
}
|
|
1457
|
+
if (severity === "info" && watched_markets?.length) {
|
|
1458
|
+
const headlineText = fresh.map((n) => n.title.toLowerCase()).join(" ");
|
|
1459
|
+
const positionKeywords = ["iran", "invade", "invasion", "military", "troops", "nfl", "bills", "saints", "eu", "withdraw", "gta", "milan", "inter"];
|
|
1460
|
+
const matches = positionKeywords.filter((k) => headlineText.includes(k));
|
|
1461
|
+
if (matches.length > 0) {
|
|
1462
|
+
severity = "warning";
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
events.push({
|
|
1466
|
+
type: "news_intelligence",
|
|
1467
|
+
severity,
|
|
1468
|
+
title: `[NEWS] ${fresh[0].title.slice(0, 80)}${fresh.length > 1 ? ` (+${fresh.length - 1} more)` : ""}`,
|
|
1469
|
+
summary: analysis ? `AI Impact: ${analysis.impact.toUpperCase()} | Sentiment: ${analysis.sentiment > 0 ? "+" : ""}${analysis.sentiment.toFixed(2)} | ${analysis.reasoning}` : `${fresh.length} relevant headlines found. Keywords: ${keywords.join(", ")}`,
|
|
1470
|
+
data: {
|
|
1471
|
+
headlines: fresh.slice(0, 5).map((n) => ({ title: n.title, source: n.source, url: n.url })),
|
|
1472
|
+
content_hash: hashContent(fresh.map((n) => n.title).join("|")),
|
|
1473
|
+
...analysis || {}
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
if (analysis && analysis.impact !== "none") {
|
|
1477
|
+
await edb.run(
|
|
1478
|
+
`INSERT INTO poly_signal_buffer (id, agent_id, signal_type, topic, data, sentiment, impact, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1479
|
+
[crypto.randomUUID(), agentId, "news", keywords.join(","), JSON.stringify({ headlines: fresh.slice(0, 3).map((n) => n.title), analysis }), analysis.sentiment, analysis.impact, dateAhead(2)]
|
|
1480
|
+
).catch(() => {
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
return events;
|
|
1484
|
+
} catch {
|
|
1485
|
+
}
|
|
1486
|
+
return [];
|
|
1487
|
+
}
|
|
1488
|
+
async function checkGeopolitical(config, agentId, edb) {
|
|
1489
|
+
const { regions, topics, watched_markets } = config;
|
|
1490
|
+
const searchTerms = [
|
|
1491
|
+
...regions || ["us", "china", "russia", "ukraine", "iran", "israel", "north korea"],
|
|
1492
|
+
...topics || ["sanctions", "military", "trade war", "election", "tariff", "nato", "diplomacy"]
|
|
1493
|
+
];
|
|
1494
|
+
try {
|
|
1495
|
+
const queries = [
|
|
1496
|
+
searchTerms.slice(0, 4).join(" OR "),
|
|
1497
|
+
"geopolitics " + (regions || ["world"]).join(" "),
|
|
1498
|
+
"breaking international " + (topics || ["conflict"]).join(" ")
|
|
1499
|
+
];
|
|
1500
|
+
const allHeadlines = [];
|
|
1501
|
+
for (const query of queries) {
|
|
1502
|
+
try {
|
|
1503
|
+
const q = encodeURIComponent(query);
|
|
1504
|
+
const res = await fetch(`https://news.google.com/rss/search?q=${q}&hl=en-US&gl=US&ceid=US:en`, { signal: AbortSignal.timeout(8e3) });
|
|
1505
|
+
const xml = await res.text();
|
|
1506
|
+
const items = xml.split("<item>").slice(1, 6);
|
|
1507
|
+
for (const item of items) {
|
|
1508
|
+
const titleMatch = item.match(/<title><!\[CDATA\[(.*?)\]\]><\/title>|<title>(.*?)<\/title>/);
|
|
1509
|
+
const pubMatch = item.match(/<pubDate>(.*?)<\/pubDate>/);
|
|
1510
|
+
const headline = titleMatch?.[1] || titleMatch?.[2] || "";
|
|
1511
|
+
if (!headline) continue;
|
|
1512
|
+
if (pubMatch?.[1]) {
|
|
1513
|
+
const age = Date.now() - new Date(pubMatch[1]).getTime();
|
|
1514
|
+
if (age > 12 * 60 * 60 * 1e3) continue;
|
|
1515
|
+
}
|
|
1516
|
+
allHeadlines.push({ title: headline, source: "Google News", date: pubMatch?.[1] });
|
|
1517
|
+
}
|
|
1518
|
+
} catch {
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
if (allHeadlines.length === 0) return [];
|
|
1522
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1523
|
+
const unique = allHeadlines.filter((h) => {
|
|
1524
|
+
const key = h.title.toLowerCase().slice(0, 50);
|
|
1525
|
+
if (seen.has(key)) return false;
|
|
1526
|
+
seen.add(key);
|
|
1527
|
+
return true;
|
|
1528
|
+
});
|
|
1529
|
+
const watcherTopics = watched_markets || [];
|
|
1530
|
+
const positions = await edb.all(
|
|
1531
|
+
`SELECT DISTINCT market_question FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`,
|
|
1532
|
+
[agentId]
|
|
1533
|
+
).catch(() => []) || [];
|
|
1534
|
+
const positionTopics = positions.map((p) => p.market_question).filter(Boolean);
|
|
1535
|
+
const analysis = await analyzeGeopolitical(
|
|
1536
|
+
unique.slice(0, 10),
|
|
1537
|
+
[...watcherTopics, ...positionTopics],
|
|
1538
|
+
agentId,
|
|
1539
|
+
edb
|
|
1540
|
+
);
|
|
1541
|
+
if (!analysis || analysis.impact === "none" || analysis.impact === "low") return [];
|
|
1542
|
+
_engineAnalysisCount++;
|
|
1543
|
+
const impactToSeverity = {
|
|
1544
|
+
medium: "warning",
|
|
1545
|
+
high: "warning",
|
|
1546
|
+
critical: "critical"
|
|
1547
|
+
};
|
|
1548
|
+
await edb.run(
|
|
1549
|
+
`INSERT INTO poly_signal_buffer (id, agent_id, signal_type, topic, data, sentiment, impact, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1550
|
+
[
|
|
1551
|
+
crypto.randomUUID(),
|
|
1552
|
+
agentId,
|
|
1553
|
+
"geopolitical",
|
|
1554
|
+
(regions || ["world"]).join(","),
|
|
1555
|
+
JSON.stringify({ headlines: unique.slice(0, 5).map((h) => h.title), analysis }),
|
|
1556
|
+
analysis.sentiment,
|
|
1557
|
+
analysis.impact,
|
|
1558
|
+
dateAhead(4)
|
|
1559
|
+
]
|
|
1560
|
+
).catch(() => {
|
|
1561
|
+
});
|
|
1562
|
+
return [{
|
|
1563
|
+
type: "geopolitical",
|
|
1564
|
+
severity: impactToSeverity[analysis.impact] || "warning",
|
|
1565
|
+
title: `[GEO] Geopolitical: ${analysis.predicted_outcome?.slice(0, 80) || "Significant development detected"}`,
|
|
1566
|
+
summary: `Impact: ${analysis.impact.toUpperCase()} | ${analysis.reasoning}`,
|
|
1567
|
+
data: {
|
|
1568
|
+
headlines: unique.slice(0, 5).map((h) => h.title),
|
|
1569
|
+
...analysis
|
|
1570
|
+
}
|
|
1571
|
+
}];
|
|
1572
|
+
} catch {
|
|
1573
|
+
}
|
|
1574
|
+
return [];
|
|
1575
|
+
}
|
|
1576
|
+
async function checkCrossSignal(config, agentId, edb) {
|
|
1577
|
+
const { min_signals, correlation_window_hours } = config;
|
|
1578
|
+
const minSignals = min_signals || 3;
|
|
1579
|
+
const windowHours = correlation_window_hours || 2;
|
|
1580
|
+
try {
|
|
1581
|
+
const signals = await edb.all(
|
|
1582
|
+
`SELECT * FROM poly_signal_buffer WHERE agent_id = ? AND created_at > ? ORDER BY created_at DESC`,
|
|
1583
|
+
[agentId, dateAgo(windowHours)]
|
|
1584
|
+
) || [];
|
|
1585
|
+
if (signals.length < minSignals) return [];
|
|
1586
|
+
const signalIds = signals.map((s) => s.id).sort().join(",");
|
|
1587
|
+
const hash = hashContent(signalIds);
|
|
1588
|
+
const existing = await edb.get(
|
|
1589
|
+
`SELECT id FROM poly_watcher_events WHERE agent_id = ? AND type = 'cross_signal' AND data LIKE ? AND created_at > ?`,
|
|
1590
|
+
[agentId, "%" + hash + "%", dateAgo(1)]
|
|
1591
|
+
).catch(() => null);
|
|
1592
|
+
if (existing) return [];
|
|
1593
|
+
const signalSummaries = signals.map((s) => {
|
|
1594
|
+
const data = JSON.parse(s.data || "{}");
|
|
1595
|
+
return {
|
|
1596
|
+
type: s.signal_type,
|
|
1597
|
+
topic: s.topic,
|
|
1598
|
+
sentiment: s.sentiment,
|
|
1599
|
+
impact: s.impact,
|
|
1600
|
+
summary: data.analysis?.reasoning || data.headlines?.join("; ") || s.topic
|
|
1601
|
+
};
|
|
1602
|
+
});
|
|
1603
|
+
const analysis = await analyzeSignalCorrelation(signalSummaries, agentId, edb);
|
|
1604
|
+
if (!analysis || analysis.impact === "none" || analysis.impact === "low") return [];
|
|
1605
|
+
_engineAnalysisCount++;
|
|
1606
|
+
const impactToSeverity = {
|
|
1607
|
+
medium: "warning",
|
|
1608
|
+
high: "critical",
|
|
1609
|
+
critical: "critical"
|
|
1610
|
+
};
|
|
1611
|
+
return [{
|
|
1612
|
+
type: "cross_signal",
|
|
1613
|
+
severity: impactToSeverity[analysis.impact] || "warning",
|
|
1614
|
+
title: `[CORR] Signal convergence: ${signals.length} correlated signals`,
|
|
1615
|
+
summary: `Impact: ${analysis.impact.toUpperCase()} | ${analysis.reasoning}`,
|
|
1616
|
+
data: {
|
|
1617
|
+
signal_count: signals.length,
|
|
1618
|
+
signal_types: [...new Set(signals.map((s) => s.signal_type))],
|
|
1619
|
+
correlation_hash: hash,
|
|
1620
|
+
...analysis
|
|
1621
|
+
}
|
|
1622
|
+
}];
|
|
1623
|
+
} catch {
|
|
1624
|
+
}
|
|
1625
|
+
return [];
|
|
1626
|
+
}
|
|
1627
|
+
async function checkSentimentShift(config, agentId, edb) {
|
|
1628
|
+
const { topic, keywords, shift_threshold } = config;
|
|
1629
|
+
if (!topic && (!keywords || !keywords.length)) return [];
|
|
1630
|
+
const threshold = shift_threshold || 0.3;
|
|
1631
|
+
const searchTopic = topic || keywords.join(" ");
|
|
1632
|
+
try {
|
|
1633
|
+
const q = encodeURIComponent(searchTopic);
|
|
1634
|
+
const res = await fetch(`https://news.google.com/rss/search?q=${q}&hl=en-US&gl=US&ceid=US:en`, { signal: AbortSignal.timeout(8e3) });
|
|
1635
|
+
const xml = await res.text();
|
|
1636
|
+
const items = xml.split("<item>").slice(1, 8);
|
|
1637
|
+
const headlines = [];
|
|
1638
|
+
for (const item of items) {
|
|
1639
|
+
const titleMatch = item.match(/<title><!\[CDATA\[(.*?)\]\]><\/title>|<title>(.*?)<\/title>/);
|
|
1640
|
+
const pubMatch = item.match(/<pubDate>(.*?)<\/pubDate>/);
|
|
1641
|
+
const headline = titleMatch?.[1] || titleMatch?.[2] || "";
|
|
1642
|
+
if (!headline) continue;
|
|
1643
|
+
if (pubMatch?.[1]) {
|
|
1644
|
+
const age = Date.now() - new Date(pubMatch[1]).getTime();
|
|
1645
|
+
if (age > 12 * 60 * 60 * 1e3) continue;
|
|
1646
|
+
}
|
|
1647
|
+
headlines.push(headline);
|
|
1648
|
+
}
|
|
1649
|
+
if (headlines.length === 0) return [];
|
|
1650
|
+
const analysis = await analyzeWithAI(
|
|
1651
|
+
`Analyze overall sentiment for "${searchTopic}" based on these headlines:
|
|
1652
|
+
${headlines.map((h) => `- ${h}`).join("\n")}
|
|
1653
|
+
|
|
1654
|
+
Focus on: Is sentiment shifting? Which direction? How fast?`,
|
|
1655
|
+
{ topic: searchTopic },
|
|
1656
|
+
agentId,
|
|
1657
|
+
edb
|
|
1658
|
+
);
|
|
1659
|
+
if (!analysis) return [];
|
|
1660
|
+
_engineAnalysisCount++;
|
|
1661
|
+
const prevSentiment = await edb.get(
|
|
1662
|
+
`SELECT sentiment, created_at FROM poly_sentiment_history WHERE agent_id = ? AND topic = ? ORDER BY created_at DESC LIMIT 1`,
|
|
1663
|
+
[agentId, searchTopic]
|
|
1664
|
+
).catch(() => null);
|
|
1665
|
+
await edb.run(
|
|
1666
|
+
`INSERT INTO poly_sentiment_history (id, agent_id, topic, sentiment, confidence, source, analysis) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
1667
|
+
[crypto.randomUUID(), agentId, searchTopic, analysis.sentiment, analysis.confidence, "news", analysis.reasoning]
|
|
1668
|
+
).catch(() => {
|
|
1669
|
+
});
|
|
1670
|
+
if (!prevSentiment) return [];
|
|
1671
|
+
const shift = Math.abs(analysis.sentiment - prevSentiment.sentiment);
|
|
1672
|
+
if (shift < threshold) return [];
|
|
1673
|
+
const direction = analysis.sentiment > prevSentiment.sentiment ? "positive" : "negative";
|
|
1674
|
+
await edb.run(
|
|
1675
|
+
`INSERT INTO poly_signal_buffer (id, agent_id, signal_type, topic, data, sentiment, impact, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1676
|
+
[
|
|
1677
|
+
crypto.randomUUID(),
|
|
1678
|
+
agentId,
|
|
1679
|
+
"sentiment_shift",
|
|
1680
|
+
searchTopic,
|
|
1681
|
+
JSON.stringify({ prev: prevSentiment.sentiment, current: analysis.sentiment, shift, direction, analysis }),
|
|
1682
|
+
analysis.sentiment,
|
|
1683
|
+
shift >= threshold * 2 ? "high" : "medium",
|
|
1684
|
+
dateAhead(2)
|
|
1685
|
+
]
|
|
1686
|
+
).catch(() => {
|
|
1687
|
+
});
|
|
1688
|
+
return [{
|
|
1689
|
+
type: "sentiment_shift",
|
|
1690
|
+
severity: shift >= threshold * 2 ? "critical" : "warning",
|
|
1691
|
+
title: `${direction === "positive" ? "[UP]" : "[DOWN]"} Sentiment shift on "${searchTopic}": ${direction} (${shift > 0 ? "+" : ""}${shift.toFixed(2)})`,
|
|
1692
|
+
summary: `${prevSentiment.sentiment.toFixed(2)} \u2192 ${analysis.sentiment.toFixed(2)} | ${analysis.reasoning}`,
|
|
1693
|
+
data: {
|
|
1694
|
+
topic: searchTopic,
|
|
1695
|
+
prev_sentiment: prevSentiment.sentiment,
|
|
1696
|
+
current_sentiment: analysis.sentiment,
|
|
1697
|
+
shift,
|
|
1698
|
+
direction,
|
|
1699
|
+
...analysis
|
|
1700
|
+
}
|
|
1701
|
+
}];
|
|
1702
|
+
} catch {
|
|
1703
|
+
}
|
|
1704
|
+
return [];
|
|
1705
|
+
}
|
|
1706
|
+
async function checkCryptoPrice(config) {
|
|
1707
|
+
const { symbols, pct_threshold } = config;
|
|
1708
|
+
const coins = symbols || ["bitcoin", "ethereum"];
|
|
1709
|
+
const threshold = pct_threshold || 3;
|
|
1710
|
+
try {
|
|
1711
|
+
if (Date.now() - lastCryptoFetch < 12e4 && Object.keys(cryptoCache).length > 0) return [];
|
|
1712
|
+
const ids = coins.join(",");
|
|
1713
|
+
const res = await fetch(`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_change=true`, { signal: AbortSignal.timeout(5e3) });
|
|
1714
|
+
const data = await res.json();
|
|
1715
|
+
lastCryptoFetch = Date.now();
|
|
1716
|
+
const events = [];
|
|
1717
|
+
for (const coin of coins) {
|
|
1718
|
+
const info = data[coin];
|
|
1719
|
+
if (!info) continue;
|
|
1720
|
+
const price = info.usd;
|
|
1721
|
+
const change = info.usd_24h_change || 0;
|
|
1722
|
+
const prev = cryptoCache[coin];
|
|
1723
|
+
cryptoCache[coin] = { price, change_24h: change };
|
|
1724
|
+
if (prev && Math.abs(change) >= threshold) {
|
|
1725
|
+
events.push({
|
|
1726
|
+
type: "crypto_price",
|
|
1727
|
+
severity: Math.abs(change) >= threshold * 2 ? "critical" : "warning",
|
|
1728
|
+
title: `${change > 0 ? "[UP]" : "[DOWN]"} ${coin.toUpperCase()} ${change > 0 ? "+" : ""}${change.toFixed(1)}% (24h)`,
|
|
1729
|
+
summary: `$${price.toLocaleString()} | Threshold: \xB1${threshold}%`,
|
|
1730
|
+
data: { coin, price, change_24h: change }
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return events;
|
|
1735
|
+
} catch {
|
|
1736
|
+
}
|
|
1737
|
+
return [];
|
|
1738
|
+
}
|
|
1739
|
+
async function checkResolution(config) {
|
|
1740
|
+
const { hours_before, categories } = config;
|
|
1741
|
+
const hoursThreshold = hours_before || 48;
|
|
1742
|
+
try {
|
|
1743
|
+
const now = /* @__PURE__ */ new Date();
|
|
1744
|
+
const cutoff = new Date(now.getTime() + hoursThreshold * 60 * 60 * 1e3).toISOString();
|
|
1745
|
+
let url = `${GAMMA_API}/markets?closed=false&end_date_max=${cutoff}&limit=10&order=endDate&ascending=true`;
|
|
1746
|
+
if (categories?.length) url += "&tag=" + categories[0];
|
|
1747
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(8e3) });
|
|
1748
|
+
const markets = await res.json();
|
|
1749
|
+
const events = [];
|
|
1750
|
+
for (const m of markets.slice(0, 5)) {
|
|
1751
|
+
if (!m.endDate) continue;
|
|
1752
|
+
const hoursLeft = (new Date(m.endDate).getTime() - now.getTime()) / (60 * 60 * 1e3);
|
|
1753
|
+
if (hoursLeft < 0 || hoursLeft > hoursThreshold) continue;
|
|
1754
|
+
events.push({
|
|
1755
|
+
type: "resolution_watch",
|
|
1756
|
+
severity: hoursLeft < 6 ? "critical" : hoursLeft < 24 ? "warning" : "info",
|
|
1757
|
+
title: `[TIME] Resolving in ${hoursLeft < 1 ? Math.round(hoursLeft * 60) + "m" : Math.round(hoursLeft) + "h"}: ${m.question?.slice(0, 80)}`,
|
|
1758
|
+
summary: `End: ${new Date(m.endDate).toLocaleString()}`,
|
|
1759
|
+
data: { condition_id: m.conditionId, question: m.question, end_date: m.endDate, hours_left: hoursLeft }
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
return events;
|
|
1763
|
+
} catch {
|
|
1764
|
+
}
|
|
1765
|
+
return [];
|
|
1766
|
+
}
|
|
1767
|
+
async function checkPortfolioDrift(config, agentId, edb) {
|
|
1768
|
+
const { pnl_threshold_pct, pnl_threshold_usd } = config;
|
|
1769
|
+
if (!pnl_threshold_pct && !pnl_threshold_usd) return [];
|
|
1770
|
+
try {
|
|
1771
|
+
const positions = await edb.all(
|
|
1772
|
+
`SELECT * FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`,
|
|
1773
|
+
[agentId]
|
|
1774
|
+
) || [];
|
|
1775
|
+
if (positions.length === 0) return [];
|
|
1776
|
+
let totalInvested = 0, totalPnl = 0;
|
|
1777
|
+
for (const p of positions) {
|
|
1778
|
+
totalInvested += (p.entry_price || 0) * (p.size || 0);
|
|
1779
|
+
totalPnl += p.pnl || 0;
|
|
1780
|
+
}
|
|
1781
|
+
const driftPct = totalInvested > 0 ? totalPnl / totalInvested * 100 : 0;
|
|
1782
|
+
if (pnl_threshold_pct && Math.abs(driftPct) >= pnl_threshold_pct) {
|
|
1783
|
+
return [{
|
|
1784
|
+
type: "portfolio_drift",
|
|
1785
|
+
severity: driftPct < 0 ? "critical" : "warning",
|
|
1786
|
+
title: `${driftPct < 0 ? "[LOSS]" : "[GAIN]"} Portfolio ${driftPct > 0 ? "+" : ""}${driftPct.toFixed(1)}% drift`,
|
|
1787
|
+
summary: `P&L: $${totalPnl.toFixed(2)} across ${positions.length} positions`,
|
|
1788
|
+
data: { total_pnl: totalPnl, drift_pct: driftPct, position_count: positions.length }
|
|
1789
|
+
}];
|
|
1790
|
+
}
|
|
1791
|
+
if (pnl_threshold_usd && Math.abs(totalPnl) >= pnl_threshold_usd) {
|
|
1792
|
+
return [{
|
|
1793
|
+
type: "portfolio_drift",
|
|
1794
|
+
severity: totalPnl < 0 ? "critical" : "warning",
|
|
1795
|
+
title: `${totalPnl < 0 ? "[LOSS]" : "[GAIN]"} Portfolio P&L hit $${Math.abs(totalPnl).toFixed(2)}`,
|
|
1796
|
+
summary: `Threshold: $${pnl_threshold_usd} | Positions: ${positions.length}`,
|
|
1797
|
+
data: { total_pnl: totalPnl, threshold: pnl_threshold_usd }
|
|
1798
|
+
}];
|
|
1799
|
+
}
|
|
1800
|
+
} catch {
|
|
1801
|
+
}
|
|
1802
|
+
return [];
|
|
1803
|
+
}
|
|
1804
|
+
async function checkVolumeSurge(config, edb) {
|
|
1805
|
+
const { token_id, surge_multiplier, market_question } = config;
|
|
1806
|
+
if (!token_id) return [];
|
|
1807
|
+
const multiplier = surge_multiplier || 3;
|
|
1808
|
+
try {
|
|
1809
|
+
const res = await fetch(`${GAMMA_API}/markets?condition_id=${token_id}&limit=1`, { signal: AbortSignal.timeout(5e3) });
|
|
1810
|
+
const markets = await res.json();
|
|
1811
|
+
const m = markets?.[0];
|
|
1812
|
+
if (!m) return [];
|
|
1813
|
+
const vol = m.volume24hr || 0;
|
|
1814
|
+
const cached = await edb.get(`SELECT * FROM poly_price_cache WHERE token_id = ?`, [token_id]);
|
|
1815
|
+
const prevVol = cached?.volume_24h || 0;
|
|
1816
|
+
await edb.run(
|
|
1817
|
+
`INSERT INTO poly_price_cache (token_id, price, volume_24h, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
|
1818
|
+
ON CONFLICT(token_id) DO UPDATE SET volume_24h = ?, updated_at = CURRENT_TIMESTAMP`,
|
|
1819
|
+
[token_id, m.outcomePrices ? JSON.parse(m.outcomePrices)[0] : 0, vol, vol]
|
|
1820
|
+
);
|
|
1821
|
+
if (prevVol > 0 && vol >= prevVol * multiplier) {
|
|
1822
|
+
const label = market_question || m.question?.slice(0, 60) || token_id.slice(0, 12);
|
|
1823
|
+
return [{
|
|
1824
|
+
type: "volume_surge",
|
|
1825
|
+
severity: "warning",
|
|
1826
|
+
title: `[VOL] Volume surge on ${label}: ${(vol / prevVol).toFixed(1)}x`,
|
|
1827
|
+
summary: `$${prevVol.toFixed(0)} \u2192 $${vol.toFixed(0)} (${multiplier}x threshold)`,
|
|
1828
|
+
data: { token_id, volume: vol, prev_volume: prevVol, multiplier: vol / prevVol }
|
|
1829
|
+
}];
|
|
1830
|
+
}
|
|
1831
|
+
} catch {
|
|
1832
|
+
}
|
|
1833
|
+
return [];
|
|
1834
|
+
}
|
|
1835
|
+
async function checkArbitrageScan(config) {
|
|
1836
|
+
const { min_edge_pct, categories } = config;
|
|
1837
|
+
const minEdge = min_edge_pct || 2;
|
|
1838
|
+
try {
|
|
1839
|
+
let url = `${GAMMA_API}/markets?closed=false&limit=20&order=volume24hr&ascending=false`;
|
|
1840
|
+
if (categories?.length) url += "&tag=" + categories[0];
|
|
1841
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(8e3) });
|
|
1842
|
+
const markets = await res.json();
|
|
1843
|
+
const events = [];
|
|
1844
|
+
for (const m of markets) {
|
|
1845
|
+
if (!m.outcomePrices) continue;
|
|
1846
|
+
try {
|
|
1847
|
+
const prices = JSON.parse(m.outcomePrices);
|
|
1848
|
+
const sum = prices.reduce((s, p) => s + p, 0);
|
|
1849
|
+
const edge = Math.abs(1 - sum) * 100;
|
|
1850
|
+
if (edge >= minEdge) {
|
|
1851
|
+
events.push({
|
|
1852
|
+
type: "arbitrage_scan",
|
|
1853
|
+
severity: edge >= minEdge * 2 ? "warning" : "info",
|
|
1854
|
+
title: `[ARB] Arbitrage: ${edge.toFixed(1)}% edge on ${m.question?.slice(0, 60)}`,
|
|
1855
|
+
summary: `Outcome prices sum: ${sum.toFixed(4)} (ideal: 1.0000)`,
|
|
1856
|
+
data: { condition_id: m.conditionId, question: m.question, prices, edge_pct: edge }
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
} catch {
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
return events.slice(0, 3);
|
|
1863
|
+
} catch {
|
|
1864
|
+
}
|
|
1865
|
+
return [];
|
|
1866
|
+
}
|
|
1867
|
+
function createWatcherTools(deps) {
|
|
1868
|
+
return [
|
|
1869
|
+
{
|
|
1870
|
+
name: "poly_watcher",
|
|
1871
|
+
description: "Manage automated market monitors with AI-powered analysis. Create watchers that run 24/7, analyze news with LLM intelligence, detect geopolitical patterns, and generate actionable signals.\n\nActions:\n- create: Set up a new watcher\n- list: View all watchers\n- delete: Remove a watcher\n- pause/resume: Toggle watcher\n\nTypes:\n- price_level: Alert when token price crosses threshold\n- price_change: Alert on % price movement\n- market_scan: Discover new markets matching keywords\n- news_intelligence: AI-analyzed news with market impact assessment\n- crypto_price: BTC/ETH price tracker\n- resolution_watch: Markets approaching resolution\n- portfolio_drift: P&L exceeds threshold\n- volume_surge: Unusual volume detection\n- geopolitical: AI scans geopolitical developments, predicts market impact\n- cross_signal: AI correlates multiple signals to detect emerging patterns\n- arbitrage_scan: Cross-market mispricing\n- sentiment_shift: Tracks sentiment changes over time with AI",
|
|
1872
|
+
parameters: {
|
|
1873
|
+
type: "object",
|
|
1874
|
+
properties: {
|
|
1875
|
+
action: { type: "string", enum: ["create", "list", "delete", "pause", "resume"], description: "Action to perform" },
|
|
1876
|
+
id: { type: "string", description: "Watcher ID (for delete/pause/resume)" },
|
|
1877
|
+
watcher_type: { type: "string", enum: ["price_level", "price_change", "market_scan", "news_intelligence", "crypto_price", "resolution_watch", "portfolio_drift", "volume_surge", "geopolitical", "cross_signal", "arbitrage_scan", "sentiment_shift"], description: "Type of watcher" },
|
|
1878
|
+
name: { type: "string", description: "Friendly name for the watcher" },
|
|
1879
|
+
config: { type: "object", description: "Watcher configuration (varies by type)" },
|
|
1880
|
+
interval_minutes: { type: "number", description: "Check interval in minutes" }
|
|
1881
|
+
},
|
|
1882
|
+
required: ["action"]
|
|
1883
|
+
},
|
|
1884
|
+
handler: async (args) => {
|
|
1885
|
+
const edb = deps.db.getEngineDB?.();
|
|
1886
|
+
if (!edb) return { error: "Database not available" };
|
|
1887
|
+
if (args.action === "list") {
|
|
1888
|
+
const rows = await edb.all(`SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC`, [deps.agentId]) || [];
|
|
1889
|
+
return { watchers: rows.map((r) => ({ ...r, config: JSON.parse(r.config || "{}") })) };
|
|
1890
|
+
}
|
|
1891
|
+
if (args.action === "create") {
|
|
1892
|
+
if (!args.watcher_type) return { error: "watcher_type required" };
|
|
1893
|
+
const defaultIntervals = {
|
|
1894
|
+
price_level: 3e4,
|
|
1895
|
+
price_change: 6e4,
|
|
1896
|
+
market_scan: 3e5,
|
|
1897
|
+
news_intelligence: 3e5,
|
|
1898
|
+
crypto_price: 12e4,
|
|
1899
|
+
resolution_watch: 9e5,
|
|
1900
|
+
portfolio_drift: 6e4,
|
|
1901
|
+
volume_surge: 3e5,
|
|
1902
|
+
geopolitical: 6e5,
|
|
1903
|
+
cross_signal: 3e5,
|
|
1904
|
+
arbitrage_scan: 6e5,
|
|
1905
|
+
sentiment_shift: 9e5
|
|
1906
|
+
};
|
|
1907
|
+
const intervalMs = args.interval_minutes ? args.interval_minutes * 6e4 : defaultIntervals[args.watcher_type] || 6e4;
|
|
1908
|
+
const id = crypto.randomUUID();
|
|
1909
|
+
await edb.run(
|
|
1910
|
+
`INSERT INTO poly_watchers (id, agent_id, type, name, config, interval_ms) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
1911
|
+
[id, deps.agentId, args.watcher_type, args.name || args.watcher_type, JSON.stringify(args.config || {}), intervalMs]
|
|
1912
|
+
);
|
|
1913
|
+
const needsAI = ["news_intelligence", "geopolitical", "cross_signal", "sentiment_shift"].includes(args.watcher_type);
|
|
1914
|
+
let aiNote = "";
|
|
1915
|
+
if (needsAI) {
|
|
1916
|
+
const aiCfg = await getAIConfig(deps.agentId, edb);
|
|
1917
|
+
if (!aiCfg) {
|
|
1918
|
+
aiNote = "\n\u26A0\uFE0F AI analysis NOT configured. This watcher requires an AI model. Run poly_watcher_config to set up a model (e.g., Grok for real-time X/Twitter intelligence, GPT-4o-mini for cheap analysis).";
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
return { success: true, id, interval_seconds: intervalMs / 1e3, requires_ai: needsAI, message: `Watcher '${args.name || args.watcher_type}' created.${aiNote}` };
|
|
1922
|
+
}
|
|
1923
|
+
if (args.action === "delete") {
|
|
1924
|
+
if (!args.id) return { error: "id required" };
|
|
1925
|
+
await edb.run(`DELETE FROM poly_watchers WHERE id = ? AND agent_id = ?`, [args.id, deps.agentId]);
|
|
1926
|
+
return { success: true };
|
|
1927
|
+
}
|
|
1928
|
+
if (args.action === "pause" || args.action === "resume") {
|
|
1929
|
+
if (!args.id) return { error: "id required" };
|
|
1930
|
+
await edb.run(
|
|
1931
|
+
`UPDATE poly_watchers SET status = ? WHERE id = ? AND agent_id = ?`,
|
|
1932
|
+
[args.action === "pause" ? "paused" : "active", args.id, deps.agentId]
|
|
1933
|
+
);
|
|
1934
|
+
return { success: true, status: args.action === "pause" ? "paused" : "active" };
|
|
1935
|
+
}
|
|
1936
|
+
return { error: "Unknown action: " + args.action };
|
|
1937
|
+
}
|
|
1938
|
+
},
|
|
1939
|
+
{
|
|
1940
|
+
name: "poly_watcher_config",
|
|
1941
|
+
description: "Configure the AI model used for background market analysis. The watcher engine uses a separate, cheap model for continuous intelligence gathering.\n\nRecommended models:\n- xai/grok-3-mini: Best for real-time analysis (has X/Twitter access)\n- openai/gpt-4o-mini: Cheap, fast, good general analysis\n- groq/llama-3.3-70b-versatile: Free tier available, fast\n- deepseek/deepseek-chat: Very cheap, good reasoning\n\nActions:\n- get: View current config\n- set: Update config\n- stats: View analysis budget usage",
|
|
1942
|
+
parameters: {
|
|
1943
|
+
type: "object",
|
|
1944
|
+
properties: {
|
|
1945
|
+
action: { type: "string", enum: ["get", "set", "stats"], description: "Action to perform" },
|
|
1946
|
+
ai_provider: { type: "string", enum: ["xai", "openai", "groq", "cerebras", "together", "openrouter", "deepseek", "fireworks"], description: "AI provider" },
|
|
1947
|
+
ai_model: { type: "string", description: "Model ID (e.g., grok-3-mini, gpt-4o-mini, llama-3.3-70b-versatile)" },
|
|
1948
|
+
ai_api_key: { type: "string", description: "API key for the AI provider" },
|
|
1949
|
+
analysis_budget_daily: { type: "number", description: "Max AI analysis calls per day (default: 100)" },
|
|
1950
|
+
max_spawn_per_hour: { type: "number", description: "Max agent session spawns per hour (default: 6)" }
|
|
1951
|
+
},
|
|
1952
|
+
required: ["action"]
|
|
1953
|
+
},
|
|
1954
|
+
handler: async (args) => {
|
|
1955
|
+
const edb = deps.db.getEngineDB?.();
|
|
1956
|
+
if (!edb) return { error: "Database not available" };
|
|
1957
|
+
if (args.action === "get") {
|
|
1958
|
+
const cfg = await edb.get(`SELECT * FROM poly_watcher_config WHERE agent_id = ?`, [deps.agentId]);
|
|
1959
|
+
if (!cfg) return { configured: false, message: "No AI config set. Run poly_watcher_config action=set to configure a model for background analysis." };
|
|
1960
|
+
return {
|
|
1961
|
+
configured: true,
|
|
1962
|
+
provider: cfg.ai_provider,
|
|
1963
|
+
model: cfg.ai_model,
|
|
1964
|
+
has_api_key: !!cfg.ai_api_key,
|
|
1965
|
+
budget_daily: cfg.analysis_budget_daily,
|
|
1966
|
+
used_today: cfg.analysis_count_today,
|
|
1967
|
+
remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),
|
|
1968
|
+
max_spawn_per_hour: cfg.max_spawn_per_hour
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
if (args.action === "set") {
|
|
1972
|
+
const existing = await edb.get(`SELECT * FROM poly_watcher_config WHERE agent_id = ?`, [deps.agentId]);
|
|
1973
|
+
if (existing) {
|
|
1974
|
+
const updates = [];
|
|
1975
|
+
const vals = [];
|
|
1976
|
+
if (args.ai_provider) {
|
|
1977
|
+
updates.push("ai_provider = ?");
|
|
1978
|
+
vals.push(args.ai_provider);
|
|
1979
|
+
}
|
|
1980
|
+
if (args.ai_model) {
|
|
1981
|
+
updates.push("ai_model = ?");
|
|
1982
|
+
vals.push(args.ai_model);
|
|
1983
|
+
}
|
|
1984
|
+
if (args.ai_api_key) {
|
|
1985
|
+
updates.push("ai_api_key = ?");
|
|
1986
|
+
vals.push(args.ai_api_key);
|
|
1987
|
+
}
|
|
1988
|
+
if (args.analysis_budget_daily) {
|
|
1989
|
+
updates.push("analysis_budget_daily = ?");
|
|
1990
|
+
vals.push(args.analysis_budget_daily);
|
|
1991
|
+
}
|
|
1992
|
+
if (args.max_spawn_per_hour) {
|
|
1993
|
+
updates.push("max_spawn_per_hour = ?");
|
|
1994
|
+
vals.push(args.max_spawn_per_hour);
|
|
1995
|
+
}
|
|
1996
|
+
updates.push("updated_at = CURRENT_TIMESTAMP");
|
|
1997
|
+
vals.push(deps.agentId);
|
|
1998
|
+
await edb.run(`UPDATE poly_watcher_config SET ${updates.join(", ")} WHERE agent_id = ?`, vals);
|
|
1999
|
+
} else {
|
|
2000
|
+
await edb.run(
|
|
2001
|
+
`INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
2002
|
+
[deps.agentId, args.ai_provider || "xai", args.ai_model || "grok-3-mini", args.ai_api_key || "", args.analysis_budget_daily || 100, args.max_spawn_per_hour || 6]
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
return { success: true, message: `AI config updated. Provider: ${args.ai_provider || "xai"}, Model: ${args.ai_model || "grok-3-mini"}. Background analysis is now enabled.` };
|
|
2006
|
+
}
|
|
2007
|
+
if (args.action === "stats") {
|
|
2008
|
+
const cfg = await edb.get(`SELECT * FROM poly_watcher_config WHERE agent_id = ?`, [deps.agentId]);
|
|
2009
|
+
const totalAnalyses = await edb.get(`SELECT COUNT(*) as cnt FROM poly_analysis_cache`).catch(() => ({ cnt: 0 }));
|
|
2010
|
+
const sentimentRecords = await edb.get(`SELECT COUNT(*) as cnt FROM poly_sentiment_history WHERE agent_id = ?`, [deps.agentId]).catch(() => ({ cnt: 0 }));
|
|
2011
|
+
const signalBuffer = await edb.get(`SELECT COUNT(*) as cnt FROM poly_signal_buffer WHERE agent_id = ?`, [deps.agentId]).catch(() => ({ cnt: 0 }));
|
|
2012
|
+
return {
|
|
2013
|
+
provider: cfg?.ai_provider || "not configured",
|
|
2014
|
+
model: cfg?.ai_model || "not configured",
|
|
2015
|
+
budget_daily: cfg?.analysis_budget_daily || 0,
|
|
2016
|
+
used_today: cfg?.analysis_count_today || 0,
|
|
2017
|
+
cached_analyses: totalAnalyses?.cnt || 0,
|
|
2018
|
+
sentiment_records: sentimentRecords?.cnt || 0,
|
|
2019
|
+
active_signals: signalBuffer?.cnt || 0,
|
|
2020
|
+
engine_total_analyses: _engineAnalysisCount
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
return { error: "Unknown action" };
|
|
2024
|
+
}
|
|
2025
|
+
},
|
|
2026
|
+
{
|
|
2027
|
+
name: "poly_watcher_events",
|
|
2028
|
+
description: "Check automation signals generated by your watchers. ALWAYS check this at the start of every session.\n\nActions:\n- check: Get unacknowledged signals (most important)\n- list: Get all recent signals\n- acknowledge: Mark a signal as read\n- acknowledge_all: Mark all as read",
|
|
2029
|
+
parameters: {
|
|
2030
|
+
type: "object",
|
|
2031
|
+
properties: {
|
|
2032
|
+
action: { type: "string", enum: ["check", "list", "acknowledge", "acknowledge_all"] },
|
|
2033
|
+
id: { type: "string", description: "Event ID (for acknowledge)" },
|
|
2034
|
+
severity: { type: "string", enum: ["critical", "warning", "info"] },
|
|
2035
|
+
limit: { type: "number", description: "Max events (default: 50)" }
|
|
2036
|
+
},
|
|
2037
|
+
required: ["action"]
|
|
2038
|
+
},
|
|
2039
|
+
handler: async (args) => {
|
|
2040
|
+
const edb = deps.db.getEngineDB?.();
|
|
2041
|
+
if (!edb) return { error: "Database not available" };
|
|
2042
|
+
if (args.action === "check") {
|
|
2043
|
+
const events = await edb.all(
|
|
2044
|
+
`SELECT * FROM poly_watcher_events WHERE agent_id = ? AND acknowledged = 0 ORDER BY severity DESC, created_at DESC LIMIT ?`,
|
|
2045
|
+
[deps.agentId, args.limit || 50]
|
|
2046
|
+
) || [];
|
|
2047
|
+
const critical = events.filter((e) => e.severity === "critical").length;
|
|
2048
|
+
const warning = events.filter((e) => e.severity === "warning").length;
|
|
2049
|
+
return {
|
|
2050
|
+
total_unread: events.length,
|
|
2051
|
+
critical,
|
|
2052
|
+
warning,
|
|
2053
|
+
events: events.map((e) => ({ ...e, data: JSON.parse(e.data || "{}") })),
|
|
2054
|
+
summary: events.length === 0 ? "No new signals. All clear." : `${events.length} unread (${critical} critical, ${warning} warning). Review critical signals immediately.`
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
if (args.action === "list") {
|
|
2058
|
+
let sql = `SELECT * FROM poly_watcher_events WHERE agent_id = ?`;
|
|
2059
|
+
const params = [deps.agentId];
|
|
2060
|
+
if (args.severity) {
|
|
2061
|
+
sql += ` AND severity = ?`;
|
|
2062
|
+
params.push(args.severity);
|
|
2063
|
+
}
|
|
2064
|
+
sql += ` ORDER BY created_at DESC LIMIT ?`;
|
|
2065
|
+
params.push(args.limit || 50);
|
|
2066
|
+
const events = await edb.all(sql, params) || [];
|
|
2067
|
+
return { events: events.map((e) => ({ ...e, data: JSON.parse(e.data || "{}") })) };
|
|
2068
|
+
}
|
|
2069
|
+
if (args.action === "acknowledge") {
|
|
2070
|
+
if (!args.id) return { error: "id required" };
|
|
2071
|
+
await edb.run(`UPDATE poly_watcher_events SET acknowledged = 1 WHERE id = ? AND agent_id = ?`, [args.id, deps.agentId]);
|
|
2072
|
+
return { success: true };
|
|
2073
|
+
}
|
|
2074
|
+
if (args.action === "acknowledge_all") {
|
|
2075
|
+
await edb.run(`UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0`, [deps.agentId]);
|
|
2076
|
+
return { success: true };
|
|
2077
|
+
}
|
|
2078
|
+
return { error: "Unknown action" };
|
|
2079
|
+
}
|
|
2080
|
+
},
|
|
2081
|
+
{
|
|
2082
|
+
name: "poly_setup_monitors",
|
|
2083
|
+
description: "Quick setup: Create a comprehensive monitoring suite for your trading operation. Sets up price alerts, AI news intelligence, geopolitical scanner, cross-signal correlation, and more.\n\nRequires: AI config (run poly_watcher_config first to set up a model).",
|
|
2084
|
+
parameters: {
|
|
2085
|
+
type: "object",
|
|
2086
|
+
properties: {
|
|
2087
|
+
keywords: { type: "array", items: { type: "string" }, description: 'Keywords to monitor (e.g. ["bitcoin", "trump", "fed", "ukraine"])' },
|
|
2088
|
+
regions: { type: "array", items: { type: "string" }, description: "Geopolitical regions to watch (default: major powers)" },
|
|
2089
|
+
crypto_threshold_pct: { type: "number", description: "Crypto change % to alert (default: 3)" },
|
|
2090
|
+
portfolio_drift_pct: { type: "number", description: "Portfolio P&L % to alert (default: 5)" },
|
|
2091
|
+
sentiment_topics: { type: "array", items: { type: "string" }, description: 'Topics to track sentiment shifts (e.g. ["bitcoin", "fed policy"])' }
|
|
2092
|
+
},
|
|
2093
|
+
required: []
|
|
2094
|
+
},
|
|
2095
|
+
handler: async (args) => {
|
|
2096
|
+
const edb = deps.db.getEngineDB?.();
|
|
2097
|
+
if (!edb) return { error: "Database not available" };
|
|
2098
|
+
const created = [];
|
|
2099
|
+
const mkId = () => crypto.randomUUID();
|
|
2100
|
+
const aiCfg = await getAIConfig(deps.agentId, edb);
|
|
2101
|
+
await edb.run(
|
|
2102
|
+
`INSERT INTO poly_watchers (id, agent_id, type, name, config, interval_ms) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
2103
|
+
[mkId(), deps.agentId, "crypto_price", "BTC/ETH Tracker", JSON.stringify({
|
|
2104
|
+
symbols: ["bitcoin", "ethereum"],
|
|
2105
|
+
pct_threshold: args.crypto_threshold_pct || 3
|
|
2106
|
+
}), 12e4]
|
|
2107
|
+
);
|
|
2108
|
+
created.push("BTC/ETH price tracker (2m interval, \xB1" + (args.crypto_threshold_pct || 3) + "%)");
|
|
2109
|
+
await edb.run(
|
|
2110
|
+
`INSERT INTO poly_watchers (id, agent_id, type, name, config, interval_ms) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
2111
|
+
[mkId(), deps.agentId, "resolution_watch", "Expiring Markets", JSON.stringify({ hours_before: 48 }), 9e5]
|
|
2112
|
+
);
|
|
2113
|
+
created.push("Resolution watcher (15m interval, 48h horizon)");
|
|
2114
|
+
await edb.run(
|
|
2115
|
+
`INSERT INTO poly_watchers (id, agent_id, type, name, config, interval_ms) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
2116
|
+
[mkId(), deps.agentId, "portfolio_drift", "Portfolio P&L Alert", JSON.stringify({
|
|
2117
|
+
pnl_threshold_pct: args.portfolio_drift_pct || 5
|
|
2118
|
+
}), 6e4]
|
|
2119
|
+
);
|
|
2120
|
+
created.push("Portfolio drift alert (1m interval, \xB1" + (args.portfolio_drift_pct || 5) + "%)");
|
|
2121
|
+
await edb.run(
|
|
2122
|
+
`INSERT INTO poly_watchers (id, agent_id, type, name, config, interval_ms) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
2123
|
+
[mkId(), deps.agentId, "arbitrage_scan", "Arbitrage Scanner", JSON.stringify({ min_edge_pct: 2 }), 6e5]
|
|
2124
|
+
);
|
|
2125
|
+
created.push("Arbitrage scanner (10m interval, \u22652% edge)");
|
|
2126
|
+
if (args.keywords?.length) {
|
|
2127
|
+
await edb.run(
|
|
2128
|
+
`INSERT INTO poly_watchers (id, agent_id, type, name, config, interval_ms) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
2129
|
+
[mkId(), deps.agentId, "news_intelligence", "AI News Scanner", JSON.stringify({
|
|
2130
|
+
keywords: args.keywords,
|
|
2131
|
+
watched_markets: args.keywords
|
|
2132
|
+
}), 3e5]
|
|
2133
|
+
);
|
|
2134
|
+
created.push("AI news intelligence (5m interval, keywords: " + args.keywords.join(", ") + ")" + (!aiCfg ? " \u26A0\uFE0F AI not configured" : ""));
|
|
2135
|
+
}
|
|
2136
|
+
await edb.run(
|
|
2137
|
+
`INSERT INTO poly_watchers (id, agent_id, type, name, config, interval_ms) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
2138
|
+
[mkId(), deps.agentId, "geopolitical", "Geopolitical Scanner", JSON.stringify({
|
|
2139
|
+
regions: args.regions || ["us", "china", "russia", "ukraine", "iran", "israel", "north korea"],
|
|
2140
|
+
topics: ["sanctions", "military", "trade war", "election", "tariff", "nato", "diplomacy", "war", "attack"],
|
|
2141
|
+
watched_markets: args.keywords || []
|
|
2142
|
+
}), 6e5]
|
|
2143
|
+
);
|
|
2144
|
+
created.push("Geopolitical scanner (10m interval)" + (!aiCfg ? " \u26A0\uFE0F AI not configured" : ""));
|
|
2145
|
+
await edb.run(
|
|
2146
|
+
`INSERT INTO poly_watchers (id, agent_id, type, name, config, interval_ms) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
2147
|
+
[mkId(), deps.agentId, "cross_signal", "Signal Correlator", JSON.stringify({
|
|
2148
|
+
min_signals: 3,
|
|
2149
|
+
correlation_window_hours: 2
|
|
2150
|
+
}), 3e5]
|
|
2151
|
+
);
|
|
2152
|
+
created.push("Cross-signal correlator (5m interval, min 3 signals)" + (!aiCfg ? " \u26A0\uFE0F AI not configured" : ""));
|
|
2153
|
+
const sentimentTopics = args.sentiment_topics || args.keywords?.slice(0, 3) || [];
|
|
2154
|
+
for (const topic of sentimentTopics) {
|
|
2155
|
+
await edb.run(
|
|
2156
|
+
`INSERT INTO poly_watchers (id, agent_id, type, name, config, interval_ms) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
2157
|
+
[mkId(), deps.agentId, "sentiment_shift", "Sentiment: " + topic, JSON.stringify({
|
|
2158
|
+
topic,
|
|
2159
|
+
keywords: [topic],
|
|
2160
|
+
shift_threshold: 0.3
|
|
2161
|
+
}), 9e5]
|
|
2162
|
+
);
|
|
2163
|
+
created.push('Sentiment tracker: "' + topic + '" (15m interval)' + (!aiCfg ? " \u26A0\uFE0F AI not configured" : ""));
|
|
2164
|
+
}
|
|
2165
|
+
const positions = await edb.all(
|
|
2166
|
+
`SELECT * FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`,
|
|
2167
|
+
[deps.agentId]
|
|
2168
|
+
).catch(() => []) || [];
|
|
2169
|
+
for (const p of positions) {
|
|
2170
|
+
const entry = p.entry_price || 0;
|
|
2171
|
+
if (!entry || !p.token_id) continue;
|
|
2172
|
+
await edb.run(
|
|
2173
|
+
`INSERT INTO poly_watchers (id, agent_id, type, name, config, interval_ms) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
2174
|
+
[mkId(), deps.agentId, "price_level", "SL: " + (p.market_question || p.token_id).slice(0, 30), JSON.stringify({
|
|
2175
|
+
token_id: p.token_id,
|
|
2176
|
+
direction: p.side === "BUY" ? "below" : "above",
|
|
2177
|
+
threshold: p.side === "BUY" ? entry * 0.85 : entry * 1.15,
|
|
2178
|
+
market_question: p.market_question
|
|
2179
|
+
}), 3e4]
|
|
2180
|
+
);
|
|
2181
|
+
await edb.run(
|
|
2182
|
+
`INSERT INTO poly_watchers (id, agent_id, type, name, config, interval_ms) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
2183
|
+
[mkId(), deps.agentId, "price_level", "TP: " + (p.market_question || p.token_id).slice(0, 30), JSON.stringify({
|
|
2184
|
+
token_id: p.token_id,
|
|
2185
|
+
direction: p.side === "BUY" ? "above" : "below",
|
|
2186
|
+
threshold: p.side === "BUY" ? entry * 1.25 : entry * 0.75,
|
|
2187
|
+
market_question: p.market_question
|
|
2188
|
+
}), 3e4]
|
|
2189
|
+
);
|
|
2190
|
+
created.push("SL/TP: " + (p.market_question || p.token_id).slice(0, 40));
|
|
2191
|
+
}
|
|
2192
|
+
return {
|
|
2193
|
+
success: true,
|
|
2194
|
+
monitors_created: created.length,
|
|
2195
|
+
ai_configured: !!aiCfg,
|
|
2196
|
+
details: created,
|
|
2197
|
+
message: `\u2705 ${created.length} monitors active.${!aiCfg ? "\n\n\u26A0\uFE0F AI analysis not configured. AI-powered watchers (news, geopolitical, correlation, sentiment) will run WITHOUT intelligence. Run poly_watcher_config action=set to enable AI analysis." : " AI intelligence enabled (" + aiCfg.provider + "/" + aiCfg.model + ")."}`
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
];
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
export {
|
|
2205
|
+
initWatcherTables,
|
|
2206
|
+
getAIConfig,
|
|
2207
|
+
analyzeWithAI,
|
|
2208
|
+
getWatcherEngineStatus,
|
|
2209
|
+
startWatcherEngine,
|
|
2210
|
+
setWatcherRuntime,
|
|
2211
|
+
controlWatcherEngine,
|
|
2212
|
+
stopWatcherEngine,
|
|
2213
|
+
createWatcherTools
|
|
2214
|
+
};
|