@catalyst-team/poly-mcp 0.1.1 → 0.1.2
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/README.md +240 -21
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +13 -2
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +98 -5
- package/dist/index.js.map +1 -1
- package/dist/sdk-instance.d.ts +27 -0
- package/dist/sdk-instance.d.ts.map +1 -0
- package/dist/sdk-instance.js +64 -0
- package/dist/sdk-instance.js.map +1 -0
- package/dist/server.d.ts +13 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +29 -27
- package/dist/server.js.map +1 -1
- package/dist/tools/guide.d.ts.map +1 -1
- package/dist/tools/guide.js +159 -1
- package/dist/tools/guide.js.map +1 -1
- package/dist/tools/index.d.ts +8 -4
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +20 -4
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/insider-detection.d.ts +175 -0
- package/dist/tools/insider-detection.d.ts.map +1 -0
- package/dist/tools/insider-detection.js +654 -0
- package/dist/tools/insider-detection.js.map +1 -0
- package/dist/tools/insider-signals.d.ts +56 -0
- package/dist/tools/insider-signals.d.ts.map +1 -0
- package/dist/tools/insider-signals.js +170 -0
- package/dist/tools/insider-signals.js.map +1 -0
- package/dist/tools/market.d.ts +25 -1
- package/dist/tools/market.d.ts.map +1 -1
- package/dist/tools/market.js +504 -12
- package/dist/tools/market.js.map +1 -1
- package/dist/tools/onchain.d.ts +240 -0
- package/dist/tools/onchain.d.ts.map +1 -0
- package/dist/tools/onchain.js +610 -0
- package/dist/tools/onchain.js.map +1 -0
- package/dist/tools/order.d.ts.map +1 -1
- package/dist/tools/order.js +13 -6
- package/dist/tools/order.js.map +1 -1
- package/dist/tools/trade.d.ts +15 -0
- package/dist/tools/trade.d.ts.map +1 -1
- package/dist/tools/trade.js +216 -39
- package/dist/tools/trade.js.map +1 -1
- package/dist/tools/trader.d.ts +4 -1
- package/dist/tools/trader.d.ts.map +1 -1
- package/dist/tools/trader.js +316 -4
- package/dist/tools/trader.js.map +1 -1
- package/dist/tools/wallet-classification.d.ts +166 -0
- package/dist/tools/wallet-classification.d.ts.map +1 -0
- package/dist/tools/wallet-classification.js +455 -0
- package/dist/tools/wallet-classification.js.map +1 -0
- package/dist/tools/wallet.d.ts +56 -7
- package/dist/tools/wallet.d.ts.map +1 -1
- package/dist/tools/wallet.js +141 -20
- package/dist/tools/wallet.js.map +1 -1
- package/dist/types.d.ts +269 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/wallet-manager.d.ts +67 -0
- package/dist/wallet-manager.d.ts.map +1 -0
- package/dist/wallet-manager.js +180 -0
- package/dist/wallet-manager.js.map +1 -0
- package/docs/01-mcp.md +554 -32
- package/docs/02-wallet-deep-research.md +344 -0
- package/docs/e2e-02/00-gap-analysis.md +211 -0
- package/docs/e2e-02/01-test-scenarios.md +530 -0
- package/docs/e2e-02/02-implementation-plan.md +190 -0
- package/docs/e2e-02/README.md +102 -0
- package/docs/reports/simonbanza-strategy-analysis-2025-12-25.md +420 -0
- package/docs/reports/smart-money-analysis-2025-12-23-cn.md +840 -0
- package/docs/reports/smart-money-trading-strategies-2025-12-25.md +440 -0
- package/docs/reports/weekly/01-v2.5.md +352 -0
- package/docs/reports/weekly/01.md +402 -0
- package/docs/reports/weekly/02-deep.md +558 -0
- package/docs/reports/weekly/02.md +505 -0
- package/docs/reports/weekly/03.md +437 -0
- package/docs/reports/weekly/04.md +418 -0
- package/docs/reports/weekly/05.md +485 -0
- package/docs/reports/weekly/06.md +436 -0
- package/docs/reports/weekly/07.md +381 -0
- package/docs/reports/weekly/08.md +502 -0
- package/docs/reports/weekly/09.md +441 -0
- package/docs/reports/weekly/10.md +511 -0
- package/docs/reports/weekly/README.md +188 -0
- package/docs/reports/weekly/prompt-v2.5.md +1019 -0
- package/docs/reports/weekly/prompt-v3.md +432 -0
- package/docs/reports/weekly/prompt.md +841 -0
- package/package.json +3 -2
- package/src/errors.ts +13 -2
- package/src/index.ts +286 -1
- package/src/sdk-instance.ts +78 -0
- package/src/server.ts +30 -28
- package/src/tools/guide.ts +160 -1
- package/src/tools/index.ts +65 -0
- package/src/tools/insider-detection.ts +899 -0
- package/src/tools/insider-signals.ts +213 -0
- package/src/tools/market.ts +569 -12
- package/src/tools/onchain.ts +738 -0
- package/src/tools/order.ts +25 -12
- package/src/tools/trade.ts +265 -53
- package/src/tools/trader.ts +350 -4
- package/src/tools/wallet-classification.ts +587 -0
- package/src/tools/wallet.ts +172 -23
- package/src/types.ts +294 -11
- package/src/wallet-manager.ts +209 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insider Detection Tools - MCP tools for detecting suspicious insider wallets
|
|
3
|
+
*
|
|
4
|
+
* These tools enable Agent to analyze wallets for insider trading patterns.
|
|
5
|
+
* Uses algorithms from @catalyst-team/smart-money package.
|
|
6
|
+
*
|
|
7
|
+
* Key features:
|
|
8
|
+
* - Analyze individual wallet for insider characteristics
|
|
9
|
+
* - Scan market trades for suspicious wallets
|
|
10
|
+
* - Get political markets with insider activity summary
|
|
11
|
+
* - Persistent storage in ~/.polymarket/insider-candidates.json
|
|
12
|
+
*
|
|
13
|
+
* @see docs/plans/03-insider-politic-markets/
|
|
14
|
+
*/
|
|
15
|
+
import { calculateInsiderScore, getInsiderLevelColor, getInsiderLevelDescription, isNewWallet, hasNoHistory, isSingleSidedBet, isLargePosition, isTimingSensitive, hasShortDepositWindow, hasLowPriceSensitivity, calculatePriceStandardDeviation, calculateReturnMultiple, categorizePoliticalMarket, INSIDER_THRESHOLDS, } from '@catalyst-team/smart-money';
|
|
16
|
+
import { wrapError, McpToolError, ErrorCode } from '../errors.js';
|
|
17
|
+
import * as fs from 'fs/promises';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import * as os from 'os';
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Storage
|
|
22
|
+
// ============================================================================
|
|
23
|
+
const STORAGE_DIR = path.join(os.homedir(), '.polymarket');
|
|
24
|
+
const CANDIDATES_FILE = path.join(STORAGE_DIR, 'insider-candidates.json');
|
|
25
|
+
async function loadCandidates() {
|
|
26
|
+
try {
|
|
27
|
+
await fs.mkdir(STORAGE_DIR, { recursive: true });
|
|
28
|
+
const data = await fs.readFile(CANDIDATES_FILE, 'utf-8');
|
|
29
|
+
return JSON.parse(data);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return {
|
|
33
|
+
version: 1,
|
|
34
|
+
candidates: {},
|
|
35
|
+
metadata: {
|
|
36
|
+
lastScanAt: 0,
|
|
37
|
+
totalCandidates: 0,
|
|
38
|
+
highScoreCount: 0,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function saveCandidates(store) {
|
|
44
|
+
await fs.mkdir(STORAGE_DIR, { recursive: true });
|
|
45
|
+
await fs.writeFile(CANDIDATES_FILE, JSON.stringify(store, null, 2));
|
|
46
|
+
}
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// Tool Definitions
|
|
49
|
+
// ============================================================================
|
|
50
|
+
export const insiderDetectionToolDefinitions = [
|
|
51
|
+
{
|
|
52
|
+
name: 'analyze_wallet_insider',
|
|
53
|
+
description: 'Analyze a wallet for insider trading characteristics. Returns InsiderScore (0-100) with detailed breakdown of features like new wallet, single-sided bet, large position, short deposit window, etc. Based on Venezuela/Greenland case studies.',
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
address: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
description: 'Wallet address (0x...)',
|
|
60
|
+
},
|
|
61
|
+
targetMarket: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
description: 'Optional: conditionId of target market for timing analysis',
|
|
64
|
+
},
|
|
65
|
+
eventTimestamp: {
|
|
66
|
+
type: 'number',
|
|
67
|
+
description: 'Optional: Unix timestamp of event for timing sensitivity',
|
|
68
|
+
},
|
|
69
|
+
saveCandidate: {
|
|
70
|
+
type: 'boolean',
|
|
71
|
+
description: 'Whether to save high-score candidates (default: true)',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
required: ['address'],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'scan_insider_wallets',
|
|
79
|
+
description: 'Scan recent trades in a market to detect suspicious insider wallets. Returns list of wallets with InsiderScore >= minScore threshold.',
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
conditionId: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: 'Market condition ID to scan',
|
|
86
|
+
},
|
|
87
|
+
minScore: {
|
|
88
|
+
type: 'number',
|
|
89
|
+
description: 'Minimum InsiderScore threshold (default: 60)',
|
|
90
|
+
},
|
|
91
|
+
limit: {
|
|
92
|
+
type: 'number',
|
|
93
|
+
description: 'Maximum trades to analyze (default: 100)',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
required: ['conditionId'],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'get_insider_candidates',
|
|
101
|
+
description: 'Get list of detected insider candidates from local storage. Supports filtering by score and sorting.',
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {
|
|
105
|
+
minScore: {
|
|
106
|
+
type: 'number',
|
|
107
|
+
description: 'Minimum InsiderScore (default: 0)',
|
|
108
|
+
},
|
|
109
|
+
maxScore: {
|
|
110
|
+
type: 'number',
|
|
111
|
+
description: 'Maximum InsiderScore (default: 100)',
|
|
112
|
+
},
|
|
113
|
+
sortBy: {
|
|
114
|
+
type: 'string',
|
|
115
|
+
enum: ['score', 'analyzedAt', 'potentialProfit'],
|
|
116
|
+
description: 'Sort field (default: score)',
|
|
117
|
+
},
|
|
118
|
+
sortOrder: {
|
|
119
|
+
type: 'string',
|
|
120
|
+
enum: ['asc', 'desc'],
|
|
121
|
+
description: 'Sort order (default: desc)',
|
|
122
|
+
},
|
|
123
|
+
limit: {
|
|
124
|
+
type: 'number',
|
|
125
|
+
description: 'Maximum candidates to return (default: 50)',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'get_political_markets',
|
|
132
|
+
description: 'Get political markets with insider activity summary. Filters markets by political keywords (election, geopolitics, policy, leadership) and shows insider activity level.',
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
category: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
enum: [
|
|
139
|
+
'election',
|
|
140
|
+
'geopolitics',
|
|
141
|
+
'policy',
|
|
142
|
+
'leadership',
|
|
143
|
+
'international',
|
|
144
|
+
'all',
|
|
145
|
+
],
|
|
146
|
+
description: 'Political category filter (default: all)',
|
|
147
|
+
},
|
|
148
|
+
active: {
|
|
149
|
+
type: 'boolean',
|
|
150
|
+
description: 'Only active markets (default: true)',
|
|
151
|
+
},
|
|
152
|
+
limit: {
|
|
153
|
+
type: 'number',
|
|
154
|
+
description: 'Maximum markets to return (default: 20)',
|
|
155
|
+
},
|
|
156
|
+
sortBy: {
|
|
157
|
+
type: 'string',
|
|
158
|
+
enum: ['volume', 'insiderActivity', 'newest'],
|
|
159
|
+
description: 'Sort field (default: volume)',
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// Helper Functions
|
|
167
|
+
// ============================================================================
|
|
168
|
+
/**
|
|
169
|
+
* Determine market type from market title/description
|
|
170
|
+
*/
|
|
171
|
+
function determineMarketType(title, description) {
|
|
172
|
+
const result = categorizePoliticalMarket(title, description);
|
|
173
|
+
if (result.isPolitical) {
|
|
174
|
+
return 'political';
|
|
175
|
+
}
|
|
176
|
+
const text = `${title} ${description || ''}`.toLowerCase();
|
|
177
|
+
if (text.includes('btc') ||
|
|
178
|
+
text.includes('eth') ||
|
|
179
|
+
text.includes('bitcoin') ||
|
|
180
|
+
text.includes('crypto') ||
|
|
181
|
+
text.includes('sol') ||
|
|
182
|
+
text.includes('xrp')) {
|
|
183
|
+
return 'crypto';
|
|
184
|
+
}
|
|
185
|
+
if (text.includes('nfl') ||
|
|
186
|
+
text.includes('nba') ||
|
|
187
|
+
text.includes('soccer') ||
|
|
188
|
+
text.includes('football') ||
|
|
189
|
+
text.includes('match')) {
|
|
190
|
+
return 'sports';
|
|
191
|
+
}
|
|
192
|
+
return 'other';
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Calculate characteristics from trader data
|
|
196
|
+
*/
|
|
197
|
+
async function calculateCharacteristics(sdk, address, marketInfo) {
|
|
198
|
+
// Get trader profile
|
|
199
|
+
const profile = await sdk.wallets.getWalletProfile(address);
|
|
200
|
+
// Get trader activity
|
|
201
|
+
const activityResult = await sdk.wallets.getWalletActivity(address, { limit: 500 });
|
|
202
|
+
const activity = activityResult.activities || [];
|
|
203
|
+
// Get positions
|
|
204
|
+
const positionsResult = await sdk.dataApi.getPositions(address, { limit: 500 });
|
|
205
|
+
const positions = positionsResult || [];
|
|
206
|
+
// Calculate wallet age
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
const firstActivity = activity.length > 0
|
|
209
|
+
? Math.min(...activity.map((a) => a.timestamp))
|
|
210
|
+
: now;
|
|
211
|
+
const walletAgeDays = Math.floor((now - firstActivity) / (1000 * 60 * 60 * 24));
|
|
212
|
+
// Calculate trade count
|
|
213
|
+
const trades = activity.filter((a) => a.type === 'TRADE');
|
|
214
|
+
const totalTradeCount = trades.length;
|
|
215
|
+
// Calculate YES bet ratio
|
|
216
|
+
const yesTrades = trades.filter((t) => t.outcome?.toLowerCase() === 'yes');
|
|
217
|
+
const yesBetRatio = totalTradeCount > 0 ? yesTrades.length / totalTradeCount : 0.5;
|
|
218
|
+
// Calculate max single trade
|
|
219
|
+
const maxSingleTradeUsd = trades.reduce((max, t) => Math.max(max, t.usdcValue || 0), 0);
|
|
220
|
+
// Calculate total volume
|
|
221
|
+
const totalVolume = trades.reduce((sum, t) => sum + (t.usdcValue || 0), 0);
|
|
222
|
+
// Get unique markets
|
|
223
|
+
const markets = [...new Set(trades.map((t) => t.conditionId).filter(Boolean))];
|
|
224
|
+
// Calculate deposit to trade time (if available)
|
|
225
|
+
const deposits = activity.filter((a) => a.type === 'SPLIT' || a.type === 'CONVERSION');
|
|
226
|
+
const firstTrade = trades.length > 0 ? trades[trades.length - 1] : null;
|
|
227
|
+
const firstDeposit = deposits.length > 0 ? deposits[deposits.length - 1] : null;
|
|
228
|
+
const depositToTradeMinutes = firstDeposit && firstTrade
|
|
229
|
+
? Math.max(0, (firstTrade.timestamp - firstDeposit.timestamp) / (1000 * 60))
|
|
230
|
+
: undefined;
|
|
231
|
+
// Calculate price standard deviation
|
|
232
|
+
const buyPrices = trades
|
|
233
|
+
.filter((t) => t.side === 'BUY')
|
|
234
|
+
.map((t) => t.price)
|
|
235
|
+
.filter((p) => p !== undefined && p > 0);
|
|
236
|
+
const priceStandardDeviation = buyPrices.length > 1 ? calculatePriceStandardDeviation(buyPrices) : undefined;
|
|
237
|
+
// Check for failed trades (rough heuristic: very low prices followed by success)
|
|
238
|
+
const hasFailedTrades = trades.some((t) => t.price && t.price < 0.02);
|
|
239
|
+
const successAfterFailure = hasFailedTrades && trades.some((t) => t.price && t.price > 0.05);
|
|
240
|
+
// Calculate return multiple from positions
|
|
241
|
+
const avgPositionPrice = positions.reduce((sum, p) => sum + (p.avgPrice || 0.5), 0) / Math.max(positions.length, 1);
|
|
242
|
+
const returnMultiple = calculateReturnMultiple(avgPositionPrice || 0.5);
|
|
243
|
+
// Determine market type
|
|
244
|
+
const primaryMarketTitle = positions[0]?.title || trades[0]?.marketTitle || '';
|
|
245
|
+
const marketType = determineMarketType(primaryMarketTitle);
|
|
246
|
+
// Calculate timing sensitivity (if event timestamp provided)
|
|
247
|
+
let hoursBeforeEvent;
|
|
248
|
+
if (marketInfo?.eventTimestamp && firstTrade) {
|
|
249
|
+
hoursBeforeEvent =
|
|
250
|
+
(marketInfo.eventTimestamp - firstTrade.timestamp) / (1000 * 60 * 60);
|
|
251
|
+
}
|
|
252
|
+
// Build characteristics
|
|
253
|
+
const characteristics = {
|
|
254
|
+
isNewWallet: isNewWallet(walletAgeDays),
|
|
255
|
+
hasNoHistory: hasNoHistory(totalTradeCount),
|
|
256
|
+
singleSidedBet: isSingleSidedBet(yesBetRatio),
|
|
257
|
+
largePosition: isLargePosition(maxSingleTradeUsd, totalVolume),
|
|
258
|
+
timingSensitive: isTimingSensitive(hoursBeforeEvent),
|
|
259
|
+
shortDepositWindow: hasShortDepositWindow(depositToTradeMinutes),
|
|
260
|
+
lowPriceSensitivity: hasLowPriceSensitivity(priceStandardDeviation),
|
|
261
|
+
twoPhasePattern: hasFailedTrades && successAfterFailure,
|
|
262
|
+
walletAgeDays,
|
|
263
|
+
totalTradeCount,
|
|
264
|
+
maxSingleTradeUsd,
|
|
265
|
+
yesBetRatio,
|
|
266
|
+
hoursBeforeEvent,
|
|
267
|
+
depositToTradeMinutes,
|
|
268
|
+
priceStandardDeviation,
|
|
269
|
+
hasFailedTrades,
|
|
270
|
+
successAfterFailure,
|
|
271
|
+
returnMultiple,
|
|
272
|
+
marketType,
|
|
273
|
+
};
|
|
274
|
+
// Build suspicious trades list
|
|
275
|
+
const suspiciousTrades = trades.slice(0, 20).map((t) => ({
|
|
276
|
+
timestamp: t.timestamp,
|
|
277
|
+
conditionId: t.conditionId || '',
|
|
278
|
+
marketTitle: t.marketTitle || '',
|
|
279
|
+
side: (t.side || 'BUY'),
|
|
280
|
+
outcome: t.outcome || '',
|
|
281
|
+
size: t.size || 0,
|
|
282
|
+
price: t.price || 0,
|
|
283
|
+
usdcValue: t.usdcValue || 0,
|
|
284
|
+
}));
|
|
285
|
+
return {
|
|
286
|
+
characteristics,
|
|
287
|
+
suspiciousTrades,
|
|
288
|
+
totalVolume,
|
|
289
|
+
markets,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// ============================================================================
|
|
293
|
+
// Handlers
|
|
294
|
+
// ============================================================================
|
|
295
|
+
/**
|
|
296
|
+
* Analyze a wallet for insider characteristics
|
|
297
|
+
*/
|
|
298
|
+
export async function handleAnalyzeWalletInsider(sdk, input) {
|
|
299
|
+
try {
|
|
300
|
+
// Validate address
|
|
301
|
+
if (!input.address || !input.address.startsWith('0x')) {
|
|
302
|
+
throw new McpToolError(ErrorCode.INVALID_INPUT, 'Invalid wallet address. Must start with 0x');
|
|
303
|
+
}
|
|
304
|
+
const { characteristics, suspiciousTrades, totalVolume, markets } = await calculateCharacteristics(sdk, input.address, {
|
|
305
|
+
conditionId: input.targetMarket,
|
|
306
|
+
eventTimestamp: input.eventTimestamp,
|
|
307
|
+
});
|
|
308
|
+
// Calculate score
|
|
309
|
+
const scoreResult = calculateInsiderScore({ characteristics });
|
|
310
|
+
// Build candidate
|
|
311
|
+
const now = Date.now();
|
|
312
|
+
const candidate = {
|
|
313
|
+
address: input.address.toLowerCase(),
|
|
314
|
+
insiderScore: scoreResult.score,
|
|
315
|
+
insiderLevel: scoreResult.level,
|
|
316
|
+
characteristics,
|
|
317
|
+
suspiciousTrades: suspiciousTrades.map((t) => ({
|
|
318
|
+
...t,
|
|
319
|
+
suspiciousReasons: [],
|
|
320
|
+
potentialReturn: 0,
|
|
321
|
+
returnMultiple: characteristics.returnMultiple,
|
|
322
|
+
})),
|
|
323
|
+
markets,
|
|
324
|
+
totalVolume,
|
|
325
|
+
potentialProfit: totalVolume * (characteristics.returnMultiple - 1),
|
|
326
|
+
firstSeen: now,
|
|
327
|
+
lastActivity: suspiciousTrades[0]?.timestamp || now,
|
|
328
|
+
walletAge: characteristics.walletAgeDays,
|
|
329
|
+
tags: [],
|
|
330
|
+
analyzedAt: now,
|
|
331
|
+
analyzedBy: 'agent',
|
|
332
|
+
};
|
|
333
|
+
// Save if high score and saveCandidate is not false
|
|
334
|
+
if (input.saveCandidate !== false && scoreResult.score >= INSIDER_THRESHOLDS.high) {
|
|
335
|
+
const store = await loadCandidates();
|
|
336
|
+
store.candidates[candidate.address] = candidate;
|
|
337
|
+
store.metadata.lastScanAt = now;
|
|
338
|
+
store.metadata.totalCandidates = Object.keys(store.candidates).length;
|
|
339
|
+
store.metadata.highScoreCount = Object.values(store.candidates).filter((c) => c.insiderScore >= INSIDER_THRESHOLDS.critical).length;
|
|
340
|
+
await saveCandidates(store);
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
address: candidate.address,
|
|
344
|
+
insiderScore: scoreResult.score,
|
|
345
|
+
level: scoreResult.level,
|
|
346
|
+
levelColor: getInsiderLevelColor(scoreResult.level),
|
|
347
|
+
levelDescription: getInsiderLevelDescription(scoreResult.level),
|
|
348
|
+
breakdown: {
|
|
349
|
+
baseScore: scoreResult.breakdown.baseScore,
|
|
350
|
+
bonusScore: scoreResult.breakdown.bonusScore,
|
|
351
|
+
features: scoreResult.breakdown.features.map((f) => ({
|
|
352
|
+
name: f.name,
|
|
353
|
+
weight: f.weight,
|
|
354
|
+
matched: f.matched,
|
|
355
|
+
contribution: f.contribution,
|
|
356
|
+
})),
|
|
357
|
+
bonuses: scoreResult.breakdown.bonuses.map((b) => ({
|
|
358
|
+
name: b.name,
|
|
359
|
+
value: b.value,
|
|
360
|
+
matched: b.matched,
|
|
361
|
+
})),
|
|
362
|
+
},
|
|
363
|
+
characteristics: {
|
|
364
|
+
walletAgeDays: characteristics.walletAgeDays,
|
|
365
|
+
totalTradeCount: characteristics.totalTradeCount,
|
|
366
|
+
maxSingleTradeUsd: characteristics.maxSingleTradeUsd,
|
|
367
|
+
yesBetRatio: Math.round(characteristics.yesBetRatio * 100) + '%',
|
|
368
|
+
depositToTradeMinutes: characteristics.depositToTradeMinutes,
|
|
369
|
+
priceStandardDeviation: characteristics.priceStandardDeviation
|
|
370
|
+
? Math.round(characteristics.priceStandardDeviation * 1000) / 1000
|
|
371
|
+
: undefined,
|
|
372
|
+
returnMultiple: Math.round(characteristics.returnMultiple * 100) / 100 + 'x',
|
|
373
|
+
marketType: characteristics.marketType,
|
|
374
|
+
},
|
|
375
|
+
summary: {
|
|
376
|
+
totalVolume: Math.round(totalVolume * 100) / 100,
|
|
377
|
+
potentialProfit: Math.round(candidate.potentialProfit * 100) / 100,
|
|
378
|
+
markets: markets.length,
|
|
379
|
+
recentTrades: suspiciousTrades.length,
|
|
380
|
+
},
|
|
381
|
+
saved: input.saveCandidate !== false && scoreResult.score >= INSIDER_THRESHOLDS.high,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
throw wrapError(err);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Scan market trades for insider wallets
|
|
390
|
+
*/
|
|
391
|
+
export async function handleScanInsiderWallets(sdk, input) {
|
|
392
|
+
try {
|
|
393
|
+
const minScore = input.minScore ?? INSIDER_THRESHOLDS.high;
|
|
394
|
+
const limit = input.limit ?? 100;
|
|
395
|
+
// Get market trades
|
|
396
|
+
const trades = await sdk.dataApi.getTradesByMarket(input.conditionId, limit);
|
|
397
|
+
// Get unique wallet addresses (use proxyWallet from Trade type)
|
|
398
|
+
const wallets = [
|
|
399
|
+
...new Set(trades.map((t) => t.proxyWallet).filter((w) => Boolean(w))),
|
|
400
|
+
];
|
|
401
|
+
// Analyze each wallet
|
|
402
|
+
const candidates = [];
|
|
403
|
+
for (const address of wallets.slice(0, 20)) {
|
|
404
|
+
// Limit to 20 wallets to avoid rate limiting
|
|
405
|
+
try {
|
|
406
|
+
const result = await handleAnalyzeWalletInsider(sdk, {
|
|
407
|
+
address,
|
|
408
|
+
targetMarket: input.conditionId,
|
|
409
|
+
saveCandidate: true,
|
|
410
|
+
});
|
|
411
|
+
if (result.insiderScore >= minScore) {
|
|
412
|
+
candidates.push({
|
|
413
|
+
address: result.address,
|
|
414
|
+
insiderScore: result.insiderScore,
|
|
415
|
+
level: result.level,
|
|
416
|
+
levelColor: result.levelColor,
|
|
417
|
+
summary: {
|
|
418
|
+
totalVolume: result.summary.totalVolume,
|
|
419
|
+
potentialProfit: result.summary.potentialProfit,
|
|
420
|
+
yesBetRatio: result.characteristics.yesBetRatio,
|
|
421
|
+
marketType: result.characteristics.marketType,
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// Skip wallets that fail analysis
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// Sort by score descending
|
|
432
|
+
candidates.sort((a, b) => b.insiderScore - a.insiderScore);
|
|
433
|
+
return {
|
|
434
|
+
conditionId: input.conditionId,
|
|
435
|
+
tradesAnalyzed: trades.length,
|
|
436
|
+
walletsScanned: Math.min(wallets.length, 20),
|
|
437
|
+
totalWallets: wallets.length,
|
|
438
|
+
minScoreThreshold: minScore,
|
|
439
|
+
candidates,
|
|
440
|
+
highScoreCount: candidates.filter((c) => c.insiderScore >= INSIDER_THRESHOLDS.critical).length,
|
|
441
|
+
mediumScoreCount: candidates.filter((c) => c.insiderScore >= INSIDER_THRESHOLDS.high &&
|
|
442
|
+
c.insiderScore < INSIDER_THRESHOLDS.critical).length,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
catch (err) {
|
|
446
|
+
throw wrapError(err);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Get insider candidates from storage
|
|
451
|
+
*/
|
|
452
|
+
export async function handleGetInsiderCandidates(_sdk, input) {
|
|
453
|
+
try {
|
|
454
|
+
const store = await loadCandidates();
|
|
455
|
+
let candidates = Object.values(store.candidates);
|
|
456
|
+
// Filter by score
|
|
457
|
+
const minScore = input.minScore ?? 0;
|
|
458
|
+
const maxScore = input.maxScore ?? 100;
|
|
459
|
+
candidates = candidates.filter((c) => c.insiderScore >= minScore && c.insiderScore <= maxScore);
|
|
460
|
+
// Sort
|
|
461
|
+
const sortBy = input.sortBy ?? 'score';
|
|
462
|
+
const sortOrder = input.sortOrder ?? 'desc';
|
|
463
|
+
candidates.sort((a, b) => {
|
|
464
|
+
let cmp = 0;
|
|
465
|
+
switch (sortBy) {
|
|
466
|
+
case 'score':
|
|
467
|
+
cmp = a.insiderScore - b.insiderScore;
|
|
468
|
+
break;
|
|
469
|
+
case 'analyzedAt':
|
|
470
|
+
cmp = a.analyzedAt - b.analyzedAt;
|
|
471
|
+
break;
|
|
472
|
+
case 'potentialProfit':
|
|
473
|
+
cmp = a.potentialProfit - b.potentialProfit;
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
return sortOrder === 'desc' ? -cmp : cmp;
|
|
477
|
+
});
|
|
478
|
+
// Limit
|
|
479
|
+
const limit = input.limit ?? 50;
|
|
480
|
+
candidates = candidates.slice(0, limit);
|
|
481
|
+
return {
|
|
482
|
+
candidates: candidates.map((c) => ({
|
|
483
|
+
address: c.address,
|
|
484
|
+
displayName: c.displayName,
|
|
485
|
+
insiderScore: c.insiderScore,
|
|
486
|
+
level: c.insiderLevel,
|
|
487
|
+
levelColor: getInsiderLevelColor(c.insiderLevel),
|
|
488
|
+
levelDescription: getInsiderLevelDescription(c.insiderLevel),
|
|
489
|
+
totalVolume: Math.round(c.totalVolume * 100) / 100,
|
|
490
|
+
potentialProfit: Math.round(c.potentialProfit * 100) / 100,
|
|
491
|
+
markets: c.markets.length,
|
|
492
|
+
walletAgeDays: c.walletAge,
|
|
493
|
+
analyzedAt: new Date(c.analyzedAt).toISOString(),
|
|
494
|
+
tags: c.tags,
|
|
495
|
+
})),
|
|
496
|
+
totalCount: candidates.length,
|
|
497
|
+
metadata: {
|
|
498
|
+
lastScanAt: store.metadata.lastScanAt
|
|
499
|
+
? new Date(store.metadata.lastScanAt).toISOString()
|
|
500
|
+
: null,
|
|
501
|
+
totalCandidates: store.metadata.totalCandidates,
|
|
502
|
+
highScoreCount: store.metadata.highScoreCount,
|
|
503
|
+
},
|
|
504
|
+
filters: {
|
|
505
|
+
minScore,
|
|
506
|
+
maxScore,
|
|
507
|
+
sortBy,
|
|
508
|
+
sortOrder,
|
|
509
|
+
limit,
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
throw wrapError(err);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Get political markets with insider activity
|
|
519
|
+
*/
|
|
520
|
+
export async function handleGetPoliticalMarkets(sdk, input) {
|
|
521
|
+
try {
|
|
522
|
+
const category = input.category ?? 'all';
|
|
523
|
+
const active = input.active ?? true;
|
|
524
|
+
const limit = input.limit ?? 20;
|
|
525
|
+
// Search for political markets
|
|
526
|
+
const politicalTerms = [
|
|
527
|
+
'trump',
|
|
528
|
+
'biden',
|
|
529
|
+
'election',
|
|
530
|
+
'president',
|
|
531
|
+
'congress',
|
|
532
|
+
'russia',
|
|
533
|
+
'ukraine',
|
|
534
|
+
'china',
|
|
535
|
+
'taiwan',
|
|
536
|
+
'war',
|
|
537
|
+
'sanction',
|
|
538
|
+
'policy',
|
|
539
|
+
'fed',
|
|
540
|
+
];
|
|
541
|
+
const allMarkets = [];
|
|
542
|
+
// Fetch active markets sorted by 24h volume
|
|
543
|
+
const fetchedMarkets = await sdk.gammaApi.getMarkets({
|
|
544
|
+
active,
|
|
545
|
+
closed: false,
|
|
546
|
+
order: 'volume24hr',
|
|
547
|
+
ascending: false,
|
|
548
|
+
limit: 500,
|
|
549
|
+
});
|
|
550
|
+
// Filter by political terms
|
|
551
|
+
for (const m of fetchedMarkets) {
|
|
552
|
+
const questionLower = m.question.toLowerCase();
|
|
553
|
+
const slugLower = m.slug.toLowerCase();
|
|
554
|
+
const descLower = (m.description || '').toLowerCase();
|
|
555
|
+
// Check if any political term matches
|
|
556
|
+
const matchesTerm = politicalTerms.some((term) => questionLower.includes(term) ||
|
|
557
|
+
slugLower.includes(term) ||
|
|
558
|
+
descLower.includes(term));
|
|
559
|
+
if (!matchesTerm) {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
// Check if already added
|
|
563
|
+
if (allMarkets.some((x) => x.conditionId === m.conditionId)) {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
// Categorize
|
|
567
|
+
const result = categorizePoliticalMarket(m.question, m.description);
|
|
568
|
+
if (!result.isPolitical) {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
// Filter by category if specified
|
|
572
|
+
if (category !== 'all' && result.category !== category) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
allMarkets.push({
|
|
576
|
+
conditionId: m.conditionId,
|
|
577
|
+
title: m.question,
|
|
578
|
+
slug: m.slug,
|
|
579
|
+
category: result.category,
|
|
580
|
+
matchedFigures: result.matchedFigures,
|
|
581
|
+
matchedRegions: result.matchedRegions,
|
|
582
|
+
confidence: result.confidence,
|
|
583
|
+
currentPrice: {
|
|
584
|
+
yes: m.outcomePrices?.[0] ?? 0.5,
|
|
585
|
+
no: m.outcomePrices?.[1] ?? 0.5,
|
|
586
|
+
},
|
|
587
|
+
volume24h: m.volume24hr || 0,
|
|
588
|
+
active: m.active,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
// Sort
|
|
592
|
+
const sortBy = input.sortBy ?? 'volume';
|
|
593
|
+
allMarkets.sort((a, b) => {
|
|
594
|
+
switch (sortBy) {
|
|
595
|
+
case 'volume':
|
|
596
|
+
return b.volume24h - a.volume24h;
|
|
597
|
+
case 'insiderActivity':
|
|
598
|
+
return b.confidence - a.confidence; // Proxy for now
|
|
599
|
+
case 'newest':
|
|
600
|
+
return 0; // Would need endDate
|
|
601
|
+
default:
|
|
602
|
+
return 0;
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
// Limit
|
|
606
|
+
const markets = allMarkets.slice(0, limit);
|
|
607
|
+
// Load insider candidates for activity summary
|
|
608
|
+
const store = await loadCandidates();
|
|
609
|
+
const candidateMarkets = new Set(Object.values(store.candidates).flatMap((c) => c.markets));
|
|
610
|
+
return {
|
|
611
|
+
markets: markets.map((m) => ({
|
|
612
|
+
conditionId: m.conditionId,
|
|
613
|
+
title: m.title,
|
|
614
|
+
slug: m.slug,
|
|
615
|
+
category: m.category,
|
|
616
|
+
categoryName: m.category
|
|
617
|
+
? {
|
|
618
|
+
election: '选举',
|
|
619
|
+
geopolitics: '地缘政治',
|
|
620
|
+
policy: '政策',
|
|
621
|
+
leadership: '领导人变动',
|
|
622
|
+
international: '国际关系',
|
|
623
|
+
other: '其他政治',
|
|
624
|
+
}[m.category]
|
|
625
|
+
: null,
|
|
626
|
+
matchedFigures: m.matchedFigures,
|
|
627
|
+
matchedRegions: m.matchedRegions,
|
|
628
|
+
confidence: Math.round(m.confidence * 100) + '%',
|
|
629
|
+
currentPrice: {
|
|
630
|
+
yes: Math.round(m.currentPrice.yes * 100) + '¢',
|
|
631
|
+
no: Math.round(m.currentPrice.no * 100) + '¢',
|
|
632
|
+
},
|
|
633
|
+
volume24h: Math.round(m.volume24h),
|
|
634
|
+
hasInsiderActivity: candidateMarkets.has(m.conditionId),
|
|
635
|
+
active: m.active,
|
|
636
|
+
})),
|
|
637
|
+
totalCount: markets.length,
|
|
638
|
+
filters: {
|
|
639
|
+
category,
|
|
640
|
+
active,
|
|
641
|
+
sortBy,
|
|
642
|
+
limit,
|
|
643
|
+
},
|
|
644
|
+
insiderSummary: {
|
|
645
|
+
totalCandidates: store.metadata.totalCandidates,
|
|
646
|
+
marketsWithInsiderActivity: markets.filter((m) => candidateMarkets.has(m.conditionId)).length,
|
|
647
|
+
},
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
catch (err) {
|
|
651
|
+
throw wrapError(err);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
//# sourceMappingURL=insider-detection.js.map
|