@darksol/terminal 0.13.0 → 0.14.0
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 +7 -1
- package/package.json +1 -1
- package/src/cli.js +54 -0
- package/src/llm/intent.js +13 -0
- package/src/services/approvals.js +452 -0
- package/src/trading/arb-ai.js +827 -0
- package/src/trading/arb.js +57 -9
- package/src/web/commands.js +46 -2
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* arb-ai.js — AI-Powered Arbitrage Intelligence
|
|
3
|
+
*
|
|
4
|
+
* Layers AI decision-making on top of the mechanical arb scanner.
|
|
5
|
+
* Uses the configured LLM provider to:
|
|
6
|
+
* 1. Discover promising pairs to scan
|
|
7
|
+
* 2. Score opportunities beyond raw math
|
|
8
|
+
* 3. Tune thresholds dynamically based on history
|
|
9
|
+
* 4. Learn from past results (what worked, what didn't)
|
|
10
|
+
* 5. Provide natural-language strategy briefings
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { LLMEngine } from '../llm/engine.js';
|
|
14
|
+
import { getConfig } from '../config/store.js';
|
|
15
|
+
import { theme } from '../ui/theme.js';
|
|
16
|
+
import { spinner, kvDisplay, success, error, warn, info, card } from '../ui/components.js';
|
|
17
|
+
import { showSection } from '../ui/banner.js';
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
19
|
+
import { join } from 'path';
|
|
20
|
+
import { homedir } from 'os';
|
|
21
|
+
|
|
22
|
+
// ═══════════════════════════════════════════════════════════════
|
|
23
|
+
// PATHS & CONSTANTS
|
|
24
|
+
// ═══════════════════════════════════════════════════════════════
|
|
25
|
+
|
|
26
|
+
const DARKSOL_DIR = join(homedir(), '.darksol');
|
|
27
|
+
const ARB_HISTORY_PATH = join(DARKSOL_DIR, 'arb-history.json');
|
|
28
|
+
const ARB_LEARNINGS_PATH = join(DARKSOL_DIR, 'arb-learnings.json');
|
|
29
|
+
const ARB_AI_LOG_PATH = join(DARKSOL_DIR, 'arb-ai-log.json');
|
|
30
|
+
|
|
31
|
+
function ensureDir() {
|
|
32
|
+
if (!existsSync(DARKSOL_DIR)) mkdirSync(DARKSOL_DIR, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════
|
|
36
|
+
// LEARNING STORE — persistent cross-session intelligence
|
|
37
|
+
// ═══════════════════════════════════════════════════════════════
|
|
38
|
+
|
|
39
|
+
function loadLearnings() {
|
|
40
|
+
ensureDir();
|
|
41
|
+
if (!existsSync(ARB_LEARNINGS_PATH)) return getDefaultLearnings();
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(readFileSync(ARB_LEARNINGS_PATH, 'utf-8'));
|
|
44
|
+
} catch {
|
|
45
|
+
return getDefaultLearnings();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function saveLearnings(learnings) {
|
|
50
|
+
ensureDir();
|
|
51
|
+
writeFileSync(ARB_LEARNINGS_PATH, JSON.stringify(learnings, null, 2));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getDefaultLearnings() {
|
|
55
|
+
return {
|
|
56
|
+
version: 1,
|
|
57
|
+
updatedAt: new Date().toISOString(),
|
|
58
|
+
// Which pairs have historically produced profitable opportunities
|
|
59
|
+
profitablePairs: [],
|
|
60
|
+
// Which pairs consistently waste gas with no real spread
|
|
61
|
+
deadPairs: [],
|
|
62
|
+
// Best performing DEX combos (e.g. "uniswapV3→aerodrome on base")
|
|
63
|
+
bestDexCombos: [],
|
|
64
|
+
// Time-of-day patterns (hour → avg opportunity count)
|
|
65
|
+
hourlyPatterns: {},
|
|
66
|
+
// Chain performance ranking
|
|
67
|
+
chainRanking: [],
|
|
68
|
+
// Threshold recommendations from AI analysis
|
|
69
|
+
recommendedThresholds: {
|
|
70
|
+
minProfitUsd: 0.50,
|
|
71
|
+
maxTradeSize: 1.0,
|
|
72
|
+
gasCeiling: 0.01,
|
|
73
|
+
},
|
|
74
|
+
// AI-generated strategy notes (natural language)
|
|
75
|
+
strategyNotes: [],
|
|
76
|
+
// Total sessions analyzed
|
|
77
|
+
sessionsAnalyzed: 0,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function loadHistory() {
|
|
82
|
+
if (!existsSync(ARB_HISTORY_PATH)) return [];
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(readFileSync(ARB_HISTORY_PATH, 'utf-8'));
|
|
85
|
+
} catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function logAiAction(action) {
|
|
91
|
+
ensureDir();
|
|
92
|
+
let log = [];
|
|
93
|
+
if (existsSync(ARB_AI_LOG_PATH)) {
|
|
94
|
+
try { log = JSON.parse(readFileSync(ARB_AI_LOG_PATH, 'utf-8')); } catch {}
|
|
95
|
+
}
|
|
96
|
+
log.push({ ts: new Date().toISOString(), ...action });
|
|
97
|
+
if (log.length > 500) log.splice(0, log.length - 500);
|
|
98
|
+
writeFileSync(ARB_AI_LOG_PATH, JSON.stringify(log, null, 2));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ═══════════════════════════════════════════════════════════════
|
|
102
|
+
// LLM INITIALIZATION
|
|
103
|
+
// ═══════════════════════════════════════════════════════════════
|
|
104
|
+
|
|
105
|
+
async function getEngine() {
|
|
106
|
+
const engine = new LLMEngine({
|
|
107
|
+
temperature: 0.3, // low temp for analytical work
|
|
108
|
+
});
|
|
109
|
+
await engine.init();
|
|
110
|
+
return engine;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ═══════════════════════════════════════════════════════════════
|
|
114
|
+
// SYSTEM PROMPTS
|
|
115
|
+
// ═══════════════════════════════════════════════════════════════
|
|
116
|
+
|
|
117
|
+
const ARB_AI_SYSTEM = `You are DARKSOL Terminal's arbitrage intelligence engine. You analyze cross-DEX arbitrage data and provide actionable insights.
|
|
118
|
+
|
|
119
|
+
You have access to:
|
|
120
|
+
- Historical arb scan results (opportunities found, spreads, gas costs, net profit/loss)
|
|
121
|
+
- Learning data (which pairs/DEXs/times perform best)
|
|
122
|
+
- Current market conditions
|
|
123
|
+
|
|
124
|
+
Your job is to:
|
|
125
|
+
1. Identify patterns in arb performance data
|
|
126
|
+
2. Recommend which pairs to focus on and which to drop
|
|
127
|
+
3. Suggest optimal thresholds (min profit, trade size, gas ceiling)
|
|
128
|
+
4. Score individual opportunities beyond raw math (consider liquidity depth, token risk, MEV likelihood)
|
|
129
|
+
5. Provide clear, actionable strategy briefings
|
|
130
|
+
|
|
131
|
+
IMPORTANT CONSTRAINTS:
|
|
132
|
+
- Be honest about limitations — DEX arb is competitive and most simple arb is front-run
|
|
133
|
+
- Never hallucinate token addresses or contract details
|
|
134
|
+
- Base recommendations on actual data, not speculation
|
|
135
|
+
- Always factor in gas costs and MEV risk
|
|
136
|
+
- Flag honeypot tokens or suspicious liquidity patterns
|
|
137
|
+
|
|
138
|
+
RESPONSE FORMAT:
|
|
139
|
+
Always respond with valid JSON. No markdown, no prose outside the JSON structure.`;
|
|
140
|
+
|
|
141
|
+
// ═══════════════════════════════════════════════════════════════
|
|
142
|
+
// AI PAIR DISCOVERY
|
|
143
|
+
// ═══════════════════════════════════════════════════════════════
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Use AI to analyze history and suggest new pairs to scan.
|
|
147
|
+
* Also identifies dead pairs that waste gas.
|
|
148
|
+
*/
|
|
149
|
+
export async function aiDiscoverPairs(opts = {}) {
|
|
150
|
+
showSection('AI PAIR DISCOVERY');
|
|
151
|
+
|
|
152
|
+
const spin = spinner('Analyzing arb history for patterns...').start();
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const history = loadHistory();
|
|
156
|
+
const learnings = loadLearnings();
|
|
157
|
+
const chain = opts.chain || getConfig('chain') || 'base';
|
|
158
|
+
|
|
159
|
+
if (history.length < 5) {
|
|
160
|
+
spin.fail('Not enough history');
|
|
161
|
+
info('Run at least 5 arb scans first: darksol arb scan');
|
|
162
|
+
info('The AI needs data to find patterns.');
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Summarize history for the LLM (don't send raw data — too large)
|
|
167
|
+
const summary = summarizeHistory(history, chain);
|
|
168
|
+
|
|
169
|
+
const engine = await getEngine();
|
|
170
|
+
engine.setSystemPrompt(ARB_AI_SYSTEM);
|
|
171
|
+
|
|
172
|
+
const prompt = `Analyze this arb scan history summary and recommend pair strategy.
|
|
173
|
+
|
|
174
|
+
CHAIN: ${chain}
|
|
175
|
+
HISTORY SUMMARY:
|
|
176
|
+
${JSON.stringify(summary, null, 2)}
|
|
177
|
+
|
|
178
|
+
CURRENT LEARNINGS:
|
|
179
|
+
${JSON.stringify({
|
|
180
|
+
profitablePairs: learnings.profitablePairs.slice(0, 10),
|
|
181
|
+
deadPairs: learnings.deadPairs.slice(0, 10),
|
|
182
|
+
bestDexCombos: learnings.bestDexCombos.slice(0, 5),
|
|
183
|
+
}, null, 2)}
|
|
184
|
+
|
|
185
|
+
Respond with JSON:
|
|
186
|
+
{
|
|
187
|
+
"addPairs": [{"tokenA": "SYMBOL", "tokenB": "SYMBOL", "reason": "why this pair"}],
|
|
188
|
+
"removePairs": [{"tokenA": "SYMBOL", "tokenB": "SYMBOL", "reason": "why drop it"}],
|
|
189
|
+
"focusPairs": [{"tokenA": "SYMBOL", "tokenB": "SYMBOL", "priority": 1-5, "reason": "why focus"}],
|
|
190
|
+
"insights": ["insight 1", "insight 2"],
|
|
191
|
+
"confidence": 0.0-1.0
|
|
192
|
+
}`;
|
|
193
|
+
|
|
194
|
+
spin.text = 'AI analyzing patterns...';
|
|
195
|
+
const response = await engine.chat(prompt, { skipContext: true });
|
|
196
|
+
|
|
197
|
+
let analysis;
|
|
198
|
+
try {
|
|
199
|
+
// Extract JSON from response (handle markdown wrapping)
|
|
200
|
+
const jsonStr = response.replace(/```json?\n?/g, '').replace(/```/g, '').trim();
|
|
201
|
+
analysis = JSON.parse(jsonStr);
|
|
202
|
+
} catch {
|
|
203
|
+
spin.fail('AI returned invalid JSON');
|
|
204
|
+
error('Could not parse AI response. Try again.');
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
spin.succeed('AI analysis complete');
|
|
209
|
+
console.log('');
|
|
210
|
+
|
|
211
|
+
// Display results
|
|
212
|
+
if (analysis.addPairs?.length > 0) {
|
|
213
|
+
console.log(theme.gold(' 📈 Suggested New Pairs:'));
|
|
214
|
+
for (const p of analysis.addPairs) {
|
|
215
|
+
console.log(` ${theme.success('+')} ${theme.bright(p.tokenA + '/' + p.tokenB)} — ${theme.dim(p.reason)}`);
|
|
216
|
+
}
|
|
217
|
+
console.log('');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (analysis.removePairs?.length > 0) {
|
|
221
|
+
console.log(theme.gold(' 📉 Suggested Removals:'));
|
|
222
|
+
for (const p of analysis.removePairs) {
|
|
223
|
+
console.log(` ${theme.error('−')} ${theme.bright(p.tokenA + '/' + p.tokenB)} — ${theme.dim(p.reason)}`);
|
|
224
|
+
}
|
|
225
|
+
console.log('');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (analysis.focusPairs?.length > 0) {
|
|
229
|
+
console.log(theme.gold(' 🎯 Focus Priority:'));
|
|
230
|
+
for (const p of analysis.focusPairs) {
|
|
231
|
+
const stars = '★'.repeat(p.priority) + '☆'.repeat(5 - p.priority);
|
|
232
|
+
console.log(` ${theme.gold(stars)} ${theme.bright(p.tokenA + '/' + p.tokenB)} — ${theme.dim(p.reason)}`);
|
|
233
|
+
}
|
|
234
|
+
console.log('');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (analysis.insights?.length > 0) {
|
|
238
|
+
console.log(theme.gold(' 💡 Insights:'));
|
|
239
|
+
for (const insight of analysis.insights) {
|
|
240
|
+
console.log(` ${theme.dim('•')} ${insight}`);
|
|
241
|
+
}
|
|
242
|
+
console.log('');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
console.log(theme.dim(` AI confidence: ${((analysis.confidence || 0) * 100).toFixed(0)}%`));
|
|
246
|
+
console.log('');
|
|
247
|
+
|
|
248
|
+
// Update learnings
|
|
249
|
+
if (analysis.focusPairs) {
|
|
250
|
+
learnings.profitablePairs = analysis.focusPairs.map(p => `${p.tokenA}/${p.tokenB}`);
|
|
251
|
+
}
|
|
252
|
+
if (analysis.removePairs) {
|
|
253
|
+
learnings.deadPairs = [
|
|
254
|
+
...new Set([...learnings.deadPairs, ...analysis.removePairs.map(p => `${p.tokenA}/${p.tokenB}`)]),
|
|
255
|
+
].slice(0, 50);
|
|
256
|
+
}
|
|
257
|
+
learnings.updatedAt = new Date().toISOString();
|
|
258
|
+
learnings.sessionsAnalyzed++;
|
|
259
|
+
saveLearnings(learnings);
|
|
260
|
+
|
|
261
|
+
logAiAction({ type: 'discover_pairs', chain, result: analysis });
|
|
262
|
+
|
|
263
|
+
return analysis;
|
|
264
|
+
|
|
265
|
+
} catch (err) {
|
|
266
|
+
spin.fail('AI analysis failed');
|
|
267
|
+
error(err.message);
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ═══════════════════════════════════════════════════════════════
|
|
273
|
+
// AI OPPORTUNITY SCORING
|
|
274
|
+
// ═══════════════════════════════════════════════════════════════
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Score an array of raw arb opportunities using AI.
|
|
278
|
+
* Adds risk assessment, MEV likelihood, and go/no-go recommendation.
|
|
279
|
+
*/
|
|
280
|
+
export async function aiScoreOpportunities(opportunities, opts = {}) {
|
|
281
|
+
if (!opportunities || opportunities.length === 0) return [];
|
|
282
|
+
|
|
283
|
+
const engine = await getEngine();
|
|
284
|
+
engine.setSystemPrompt(ARB_AI_SYSTEM);
|
|
285
|
+
|
|
286
|
+
const learnings = loadLearnings();
|
|
287
|
+
|
|
288
|
+
// Only send top opportunities to save tokens
|
|
289
|
+
const top = opportunities
|
|
290
|
+
.sort((a, b) => b.netProfitUsd - a.netProfitUsd)
|
|
291
|
+
.slice(0, 10);
|
|
292
|
+
|
|
293
|
+
const oppData = top.map(o => ({
|
|
294
|
+
pair: o.pair,
|
|
295
|
+
buyDex: o.buyDexName,
|
|
296
|
+
sellDex: o.sellDexName,
|
|
297
|
+
spread: o.spread,
|
|
298
|
+
netProfitUsd: o.netProfitUsd,
|
|
299
|
+
gasCostUsd: o.gasCostUsd,
|
|
300
|
+
chain: o.chain,
|
|
301
|
+
amountInEth: o.amountInEth,
|
|
302
|
+
}));
|
|
303
|
+
|
|
304
|
+
const prompt = `Score these arb opportunities. Consider MEV risk, liquidity depth, token legitimacy, and historical patterns.
|
|
305
|
+
|
|
306
|
+
OPPORTUNITIES:
|
|
307
|
+
${JSON.stringify(oppData, null, 2)}
|
|
308
|
+
|
|
309
|
+
LEARNED PATTERNS:
|
|
310
|
+
- Profitable pairs: ${learnings.profitablePairs.join(', ') || 'none yet'}
|
|
311
|
+
- Dead pairs: ${learnings.deadPairs.join(', ') || 'none yet'}
|
|
312
|
+
- Best DEX combos: ${learnings.bestDexCombos.join(', ') || 'none yet'}
|
|
313
|
+
|
|
314
|
+
Respond with JSON:
|
|
315
|
+
{
|
|
316
|
+
"scored": [
|
|
317
|
+
{
|
|
318
|
+
"pair": "TOKEN/TOKEN",
|
|
319
|
+
"riskScore": 1-10,
|
|
320
|
+
"mevLikelihood": "low|medium|high",
|
|
321
|
+
"recommendation": "execute|skip|watch",
|
|
322
|
+
"reason": "why",
|
|
323
|
+
"adjustedProfitUsd": 0.00
|
|
324
|
+
}
|
|
325
|
+
],
|
|
326
|
+
"summary": "one-line overall assessment"
|
|
327
|
+
}`;
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const response = await engine.chat(prompt, { skipContext: true });
|
|
331
|
+
const jsonStr = response.replace(/```json?\n?/g, '').replace(/```/g, '').trim();
|
|
332
|
+
const scoring = JSON.parse(jsonStr);
|
|
333
|
+
|
|
334
|
+
logAiAction({ type: 'score', count: top.length, result: scoring });
|
|
335
|
+
return scoring;
|
|
336
|
+
} catch {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ═══════════════════════════════════════════════════════════════
|
|
342
|
+
// AI THRESHOLD TUNING
|
|
343
|
+
// ═══════════════════════════════════════════════════════════════
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Analyze history and recommend optimal thresholds.
|
|
347
|
+
*/
|
|
348
|
+
export async function aiTuneThresholds(opts = {}) {
|
|
349
|
+
showSection('AI THRESHOLD TUNING');
|
|
350
|
+
|
|
351
|
+
const spin = spinner('Analyzing performance data...').start();
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const history = loadHistory();
|
|
355
|
+
const learnings = loadLearnings();
|
|
356
|
+
const chain = opts.chain || getConfig('chain') || 'base';
|
|
357
|
+
|
|
358
|
+
if (history.length < 10) {
|
|
359
|
+
spin.fail('Need more data');
|
|
360
|
+
info('Run at least 10 arb scans before tuning. Current: ' + history.length);
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const summary = summarizeHistory(history, chain);
|
|
365
|
+
const engine = await getEngine();
|
|
366
|
+
engine.setSystemPrompt(ARB_AI_SYSTEM);
|
|
367
|
+
|
|
368
|
+
const prompt = `Analyze this arb performance data and recommend optimal thresholds.
|
|
369
|
+
|
|
370
|
+
CHAIN: ${chain}
|
|
371
|
+
PERFORMANCE SUMMARY:
|
|
372
|
+
${JSON.stringify(summary, null, 2)}
|
|
373
|
+
|
|
374
|
+
CURRENT THRESHOLDS:
|
|
375
|
+
${JSON.stringify(learnings.recommendedThresholds, null, 2)}
|
|
376
|
+
|
|
377
|
+
Consider:
|
|
378
|
+
- What minimum profit threshold filters noise without missing real opportunities?
|
|
379
|
+
- What trade size balances risk vs reward?
|
|
380
|
+
- What gas ceiling is appropriate for ${chain}?
|
|
381
|
+
- What cooldown prevents overtrading?
|
|
382
|
+
|
|
383
|
+
Respond with JSON:
|
|
384
|
+
{
|
|
385
|
+
"recommended": {
|
|
386
|
+
"minProfitUsd": 0.00,
|
|
387
|
+
"maxTradeSize": 0.00,
|
|
388
|
+
"gasCeiling": 0.00,
|
|
389
|
+
"cooldownMs": 0
|
|
390
|
+
},
|
|
391
|
+
"changes": [
|
|
392
|
+
{"field": "minProfitUsd", "from": 0.00, "to": 0.00, "reason": "why"}
|
|
393
|
+
],
|
|
394
|
+
"reasoning": "overall explanation",
|
|
395
|
+
"confidence": 0.0-1.0
|
|
396
|
+
}`;
|
|
397
|
+
|
|
398
|
+
spin.text = 'AI evaluating thresholds...';
|
|
399
|
+
const response = await engine.chat(prompt, { skipContext: true });
|
|
400
|
+
|
|
401
|
+
let tuning;
|
|
402
|
+
try {
|
|
403
|
+
const jsonStr = response.replace(/```json?\n?/g, '').replace(/```/g, '').trim();
|
|
404
|
+
tuning = JSON.parse(jsonStr);
|
|
405
|
+
} catch {
|
|
406
|
+
spin.fail('AI returned invalid response');
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
spin.succeed('Threshold analysis complete');
|
|
411
|
+
console.log('');
|
|
412
|
+
|
|
413
|
+
// Display recommendations
|
|
414
|
+
if (tuning.changes?.length > 0) {
|
|
415
|
+
console.log(theme.gold(' 🔧 Recommended Changes:'));
|
|
416
|
+
for (const c of tuning.changes) {
|
|
417
|
+
const arrow = c.to > c.from ? theme.success('↑') : theme.error('↓');
|
|
418
|
+
console.log(` ${arrow} ${theme.bright(c.field)}: ${c.from} → ${theme.gold(String(c.to))}`);
|
|
419
|
+
console.log(` ${theme.dim(c.reason)}`);
|
|
420
|
+
}
|
|
421
|
+
console.log('');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (tuning.reasoning) {
|
|
425
|
+
console.log(theme.gold(' 📝 Reasoning:'));
|
|
426
|
+
console.log(` ${theme.dim(tuning.reasoning)}`);
|
|
427
|
+
console.log('');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
console.log(theme.dim(` AI confidence: ${((tuning.confidence || 0) * 100).toFixed(0)}%`));
|
|
431
|
+
console.log('');
|
|
432
|
+
|
|
433
|
+
// Save recommended thresholds to learnings
|
|
434
|
+
if (tuning.recommended) {
|
|
435
|
+
learnings.recommendedThresholds = tuning.recommended;
|
|
436
|
+
learnings.updatedAt = new Date().toISOString();
|
|
437
|
+
saveLearnings(learnings);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
logAiAction({ type: 'tune_thresholds', chain, result: tuning });
|
|
441
|
+
|
|
442
|
+
return tuning;
|
|
443
|
+
|
|
444
|
+
} catch (err) {
|
|
445
|
+
spin.fail('Threshold analysis failed');
|
|
446
|
+
error(err.message);
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ═══════════════════════════════════════════════════════════════
|
|
452
|
+
// AI STRATEGY BRIEFING
|
|
453
|
+
// ═══════════════════════════════════════════════════════════════
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Generate a natural-language strategy briefing based on all available data.
|
|
457
|
+
*/
|
|
458
|
+
export async function aiStrategyBriefing(opts = {}) {
|
|
459
|
+
showSection('AI STRATEGY BRIEFING');
|
|
460
|
+
|
|
461
|
+
const spin = spinner('Generating strategy briefing...').start();
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
const history = loadHistory();
|
|
465
|
+
const learnings = loadLearnings();
|
|
466
|
+
const chain = opts.chain || getConfig('chain') || 'base';
|
|
467
|
+
const summary = summarizeHistory(history, chain);
|
|
468
|
+
|
|
469
|
+
const engine = await getEngine();
|
|
470
|
+
engine.setSystemPrompt(ARB_AI_SYSTEM);
|
|
471
|
+
|
|
472
|
+
const prompt = `Generate a strategy briefing for DEX arbitrage on ${chain}.
|
|
473
|
+
|
|
474
|
+
PERFORMANCE DATA:
|
|
475
|
+
${JSON.stringify(summary, null, 2)}
|
|
476
|
+
|
|
477
|
+
LEARNED PATTERNS:
|
|
478
|
+
${JSON.stringify({
|
|
479
|
+
profitablePairs: learnings.profitablePairs,
|
|
480
|
+
deadPairs: learnings.deadPairs,
|
|
481
|
+
bestDexCombos: learnings.bestDexCombos,
|
|
482
|
+
hourlyPatterns: learnings.hourlyPatterns,
|
|
483
|
+
chainRanking: learnings.chainRanking,
|
|
484
|
+
recommendedThresholds: learnings.recommendedThresholds,
|
|
485
|
+
sessionsAnalyzed: learnings.sessionsAnalyzed,
|
|
486
|
+
strategyNotes: learnings.strategyNotes.slice(-5),
|
|
487
|
+
}, null, 2)}
|
|
488
|
+
|
|
489
|
+
Write a concise strategy briefing. Include:
|
|
490
|
+
1. Current state assessment (how are we doing?)
|
|
491
|
+
2. Top recommendations (what should we change?)
|
|
492
|
+
3. Risk warnings (what could go wrong?)
|
|
493
|
+
4. Next actions (what should the user do right now?)
|
|
494
|
+
|
|
495
|
+
Respond with JSON:
|
|
496
|
+
{
|
|
497
|
+
"assessment": "current state in 1-2 sentences",
|
|
498
|
+
"performance": {"totalScans": 0, "profitableOpps": 0, "executedTrades": 0, "estimatedPnl": 0},
|
|
499
|
+
"recommendations": ["rec 1", "rec 2", "rec 3"],
|
|
500
|
+
"risks": ["risk 1", "risk 2"],
|
|
501
|
+
"nextActions": ["action 1", "action 2"],
|
|
502
|
+
"confidence": 0.0-1.0
|
|
503
|
+
}`;
|
|
504
|
+
|
|
505
|
+
spin.text = 'AI drafting briefing...';
|
|
506
|
+
const response = await engine.chat(prompt, { skipContext: true });
|
|
507
|
+
|
|
508
|
+
let briefing;
|
|
509
|
+
try {
|
|
510
|
+
const jsonStr = response.replace(/```json?\n?/g, '').replace(/```/g, '').trim();
|
|
511
|
+
briefing = JSON.parse(jsonStr);
|
|
512
|
+
} catch {
|
|
513
|
+
spin.fail('AI returned invalid response');
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
spin.succeed('Briefing ready');
|
|
518
|
+
console.log('');
|
|
519
|
+
|
|
520
|
+
// Display briefing
|
|
521
|
+
console.log(theme.gold(' 📋 Assessment:'));
|
|
522
|
+
console.log(` ${briefing.assessment}`);
|
|
523
|
+
console.log('');
|
|
524
|
+
|
|
525
|
+
if (briefing.performance) {
|
|
526
|
+
kvDisplay([
|
|
527
|
+
['Scans', String(briefing.performance.totalScans || 0)],
|
|
528
|
+
['Profitable Opps', String(briefing.performance.profitableOpps || 0)],
|
|
529
|
+
['Executed', String(briefing.performance.executedTrades || 0)],
|
|
530
|
+
['Est. PnL', `$${(briefing.performance.estimatedPnl || 0).toFixed(4)}`],
|
|
531
|
+
], { title: 'Performance' });
|
|
532
|
+
console.log('');
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (briefing.recommendations?.length > 0) {
|
|
536
|
+
console.log(theme.gold(' 💡 Recommendations:'));
|
|
537
|
+
briefing.recommendations.forEach((r, i) => {
|
|
538
|
+
console.log(` ${theme.gold(String(i + 1) + '.')} ${r}`);
|
|
539
|
+
});
|
|
540
|
+
console.log('');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (briefing.risks?.length > 0) {
|
|
544
|
+
console.log(theme.warning(' ⚠ Risks:'));
|
|
545
|
+
briefing.risks.forEach(r => {
|
|
546
|
+
console.log(` ${theme.error('•')} ${r}`);
|
|
547
|
+
});
|
|
548
|
+
console.log('');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (briefing.nextActions?.length > 0) {
|
|
552
|
+
console.log(theme.success(' ▶ Next Actions:'));
|
|
553
|
+
briefing.nextActions.forEach(a => {
|
|
554
|
+
console.log(` ${theme.info('→')} ${a}`);
|
|
555
|
+
});
|
|
556
|
+
console.log('');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Save briefing to learnings
|
|
560
|
+
learnings.strategyNotes.push({
|
|
561
|
+
ts: new Date().toISOString(),
|
|
562
|
+
chain,
|
|
563
|
+
assessment: briefing.assessment,
|
|
564
|
+
recommendations: briefing.recommendations,
|
|
565
|
+
});
|
|
566
|
+
// Keep last 20 briefings
|
|
567
|
+
if (learnings.strategyNotes.length > 20) {
|
|
568
|
+
learnings.strategyNotes = learnings.strategyNotes.slice(-20);
|
|
569
|
+
}
|
|
570
|
+
learnings.updatedAt = new Date().toISOString();
|
|
571
|
+
saveLearnings(learnings);
|
|
572
|
+
|
|
573
|
+
logAiAction({ type: 'briefing', chain, result: briefing });
|
|
574
|
+
|
|
575
|
+
return briefing;
|
|
576
|
+
|
|
577
|
+
} catch (err) {
|
|
578
|
+
spin.fail('Briefing failed');
|
|
579
|
+
error(err.message);
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ═══════════════════════════════════════════════════════════════
|
|
585
|
+
// AI LEARN — analyze history and update patterns
|
|
586
|
+
// ═══════════════════════════════════════════════════════════════
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Run a learning cycle — analyze recent history and update persistent learnings.
|
|
590
|
+
* Should be run periodically (after a batch of scans or daily).
|
|
591
|
+
*/
|
|
592
|
+
export async function aiLearn(opts = {}) {
|
|
593
|
+
showSection('AI LEARNING CYCLE');
|
|
594
|
+
|
|
595
|
+
const spin = spinner('Analyzing recent arb data...').start();
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
const history = loadHistory();
|
|
599
|
+
const learnings = loadLearnings();
|
|
600
|
+
const chain = opts.chain || getConfig('chain') || 'base';
|
|
601
|
+
|
|
602
|
+
if (history.length < 3) {
|
|
603
|
+
spin.fail('Not enough data to learn from');
|
|
604
|
+
info('Run more scans first.');
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Extract patterns from raw data (no LLM needed for this)
|
|
609
|
+
const chainHistory = history.filter(h => h.chain === chain);
|
|
610
|
+
|
|
611
|
+
// Hourly pattern analysis
|
|
612
|
+
const hourBuckets = {};
|
|
613
|
+
for (const h of chainHistory) {
|
|
614
|
+
const hour = new Date(h.ts).getHours();
|
|
615
|
+
if (!hourBuckets[hour]) hourBuckets[hour] = { count: 0, profitable: 0 };
|
|
616
|
+
hourBuckets[hour].count++;
|
|
617
|
+
if (h.netProfitUsd > 0) hourBuckets[hour].profitable++;
|
|
618
|
+
}
|
|
619
|
+
learnings.hourlyPatterns = hourBuckets;
|
|
620
|
+
|
|
621
|
+
// Best DEX combos
|
|
622
|
+
const comboCounts = {};
|
|
623
|
+
for (const h of chainHistory.filter(h => h.netProfitUsd > 0)) {
|
|
624
|
+
const combo = `${h.buyDex}→${h.sellDex}`;
|
|
625
|
+
comboCounts[combo] = (comboCounts[combo] || 0) + 1;
|
|
626
|
+
}
|
|
627
|
+
learnings.bestDexCombos = Object.entries(comboCounts)
|
|
628
|
+
.sort((a, b) => b[1] - a[1])
|
|
629
|
+
.slice(0, 10)
|
|
630
|
+
.map(([combo, count]) => `${combo} (${count}x)`);
|
|
631
|
+
|
|
632
|
+
// Pair profitability
|
|
633
|
+
const pairProfits = {};
|
|
634
|
+
for (const h of chainHistory) {
|
|
635
|
+
const pair = h.pair;
|
|
636
|
+
if (!pair) continue;
|
|
637
|
+
if (!pairProfits[pair]) pairProfits[pair] = { total: 0, profitable: 0, totalProfit: 0 };
|
|
638
|
+
pairProfits[pair].total++;
|
|
639
|
+
if (h.netProfitUsd > 0) {
|
|
640
|
+
pairProfits[pair].profitable++;
|
|
641
|
+
pairProfits[pair].totalProfit += h.netProfitUsd;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
learnings.profitablePairs = Object.entries(pairProfits)
|
|
646
|
+
.filter(([, data]) => data.profitable / data.total > 0.1) // >10% success rate
|
|
647
|
+
.sort((a, b) => b[1].totalProfit - a[1].totalProfit)
|
|
648
|
+
.slice(0, 20)
|
|
649
|
+
.map(([pair]) => pair);
|
|
650
|
+
|
|
651
|
+
learnings.deadPairs = Object.entries(pairProfits)
|
|
652
|
+
.filter(([, data]) => data.total >= 5 && data.profitable === 0) // 5+ scans, never profitable
|
|
653
|
+
.map(([pair]) => pair);
|
|
654
|
+
|
|
655
|
+
// Chain ranking
|
|
656
|
+
const chainCounts = {};
|
|
657
|
+
for (const h of history.filter(h => h.netProfitUsd > 0)) {
|
|
658
|
+
chainCounts[h.chain] = (chainCounts[h.chain] || 0) + 1;
|
|
659
|
+
}
|
|
660
|
+
learnings.chainRanking = Object.entries(chainCounts)
|
|
661
|
+
.sort((a, b) => b[1] - a[1])
|
|
662
|
+
.map(([c, count]) => `${c} (${count} opps)`);
|
|
663
|
+
|
|
664
|
+
learnings.updatedAt = new Date().toISOString();
|
|
665
|
+
learnings.sessionsAnalyzed++;
|
|
666
|
+
saveLearnings(learnings);
|
|
667
|
+
|
|
668
|
+
spin.succeed('Learning cycle complete');
|
|
669
|
+
console.log('');
|
|
670
|
+
|
|
671
|
+
// Display what was learned
|
|
672
|
+
kvDisplay([
|
|
673
|
+
['Data Points', chainHistory.length.toString()],
|
|
674
|
+
['Profitable Pairs', learnings.profitablePairs.length.toString()],
|
|
675
|
+
['Dead Pairs', learnings.deadPairs.length.toString()],
|
|
676
|
+
['Best DEX Combos', learnings.bestDexCombos.slice(0, 3).join(', ') || 'none yet'],
|
|
677
|
+
['Chain Ranking', learnings.chainRanking.join(', ') || 'none yet'],
|
|
678
|
+
['Sessions', learnings.sessionsAnalyzed.toString()],
|
|
679
|
+
], { title: 'Learned Patterns' });
|
|
680
|
+
console.log('');
|
|
681
|
+
|
|
682
|
+
// Show hourly heatmap
|
|
683
|
+
if (Object.keys(hourBuckets).length > 0) {
|
|
684
|
+
console.log(theme.gold(' 🕐 Hourly Opportunity Heatmap:'));
|
|
685
|
+
const maxCount = Math.max(...Object.values(hourBuckets).map(b => b.profitable));
|
|
686
|
+
for (let h = 0; h < 24; h++) {
|
|
687
|
+
const bucket = hourBuckets[h] || { count: 0, profitable: 0 };
|
|
688
|
+
const bar = maxCount > 0 ? '█'.repeat(Math.ceil((bucket.profitable / maxCount) * 20)) : '';
|
|
689
|
+
const hour = String(h).padStart(2, '0') + ':00';
|
|
690
|
+
const color = bucket.profitable > 0 ? theme.success : theme.dim;
|
|
691
|
+
console.log(` ${theme.dim(hour)} ${color(bar)} ${theme.dim(String(bucket.profitable) + '/' + String(bucket.count))}`);
|
|
692
|
+
}
|
|
693
|
+
console.log('');
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
logAiAction({ type: 'learn', chain, patternsFound: learnings.profitablePairs.length });
|
|
697
|
+
|
|
698
|
+
return learnings;
|
|
699
|
+
|
|
700
|
+
} catch (err) {
|
|
701
|
+
spin.fail('Learning cycle failed');
|
|
702
|
+
error(err.message);
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ═══════════════════════════════════════════════════════════════
|
|
708
|
+
// AI-ENHANCED MONITOR FILTER
|
|
709
|
+
// ═══════════════════════════════════════════════════════════════
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Quick AI filter for the monitor loop.
|
|
713
|
+
* Uses learnings (no LLM call) to filter opportunities in real-time.
|
|
714
|
+
* This is the fast path — no API calls, pure pattern matching.
|
|
715
|
+
*/
|
|
716
|
+
export function aiFilterOpportunity(opportunity) {
|
|
717
|
+
const learnings = loadLearnings();
|
|
718
|
+
|
|
719
|
+
let score = 50; // base score out of 100
|
|
720
|
+
|
|
721
|
+
// Boost if pair is in profitable list
|
|
722
|
+
if (learnings.profitablePairs.includes(opportunity.pair)) {
|
|
723
|
+
score += 20;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Penalize if pair is in dead list
|
|
727
|
+
if (learnings.deadPairs.includes(opportunity.pair)) {
|
|
728
|
+
score -= 40;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Boost if DEX combo is known good
|
|
732
|
+
const combo = `${opportunity.buyDex}→${opportunity.sellDex}`;
|
|
733
|
+
if (learnings.bestDexCombos.some(c => c.startsWith(combo))) {
|
|
734
|
+
score += 15;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Time-of-day boost
|
|
738
|
+
const currentHour = new Date().getHours();
|
|
739
|
+
const hourData = learnings.hourlyPatterns[currentHour];
|
|
740
|
+
if (hourData && hourData.profitable > 0) {
|
|
741
|
+
score += Math.min(10, hourData.profitable * 2);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Apply learned thresholds
|
|
745
|
+
const thresholds = learnings.recommendedThresholds;
|
|
746
|
+
if (thresholds.minProfitUsd && opportunity.netProfitUsd < thresholds.minProfitUsd) {
|
|
747
|
+
score -= 20;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Clamp score
|
|
751
|
+
score = Math.max(0, Math.min(100, score));
|
|
752
|
+
|
|
753
|
+
return {
|
|
754
|
+
score,
|
|
755
|
+
pass: score >= 40,
|
|
756
|
+
reason: score >= 70 ? 'strong pattern match'
|
|
757
|
+
: score >= 40 ? 'acceptable'
|
|
758
|
+
: 'below AI threshold',
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ═══════════════════════════════════════════════════════════════
|
|
763
|
+
// HISTORY SUMMARIZER (for LLM context)
|
|
764
|
+
// ═══════════════════════════════════════════════════════════════
|
|
765
|
+
|
|
766
|
+
function summarizeHistory(history, chain) {
|
|
767
|
+
const chainHistory = history.filter(h => h.chain === chain);
|
|
768
|
+
const last7d = chainHistory.filter(h => Date.now() - new Date(h.ts).getTime() < 7 * 86400 * 1000);
|
|
769
|
+
|
|
770
|
+
// Pair frequency
|
|
771
|
+
const pairCounts = {};
|
|
772
|
+
const pairProfits = {};
|
|
773
|
+
for (const h of last7d) {
|
|
774
|
+
const pair = h.pair || 'unknown';
|
|
775
|
+
pairCounts[pair] = (pairCounts[pair] || 0) + 1;
|
|
776
|
+
if (!pairProfits[pair]) pairProfits[pair] = { sum: 0, count: 0 };
|
|
777
|
+
pairProfits[pair].sum += (h.netProfitUsd || 0);
|
|
778
|
+
pairProfits[pair].count++;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// DEX frequency
|
|
782
|
+
const dexCounts = {};
|
|
783
|
+
for (const h of last7d) {
|
|
784
|
+
if (h.buyDex) dexCounts[h.buyDex] = (dexCounts[h.buyDex] || 0) + 1;
|
|
785
|
+
if (h.sellDex) dexCounts[h.sellDex] = (dexCounts[h.sellDex] || 0) + 1;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Spread statistics
|
|
789
|
+
const spreads = last7d.map(h => h.spread || 0).filter(s => s > 0);
|
|
790
|
+
const avgSpread = spreads.length > 0 ? spreads.reduce((a, b) => a + b, 0) / spreads.length : 0;
|
|
791
|
+
const maxSpread = Math.max(0, ...spreads);
|
|
792
|
+
|
|
793
|
+
// Profit statistics
|
|
794
|
+
const profits = last7d.map(h => h.netProfitUsd || 0);
|
|
795
|
+
const totalProfit = profits.reduce((a, b) => a + b, 0);
|
|
796
|
+
const profitableCount = profits.filter(p => p > 0).length;
|
|
797
|
+
|
|
798
|
+
// Gas statistics
|
|
799
|
+
const gasCosts = last7d.map(h => h.gasCostUsd || 0);
|
|
800
|
+
const avgGas = gasCosts.length > 0 ? gasCosts.reduce((a, b) => a + b, 0) / gasCosts.length : 0;
|
|
801
|
+
|
|
802
|
+
return {
|
|
803
|
+
chain,
|
|
804
|
+
totalEntries: chainHistory.length,
|
|
805
|
+
last7dEntries: last7d.length,
|
|
806
|
+
pairBreakdown: Object.entries(pairCounts)
|
|
807
|
+
.sort((a, b) => b[1] - a[1])
|
|
808
|
+
.slice(0, 10)
|
|
809
|
+
.map(([pair, count]) => ({
|
|
810
|
+
pair,
|
|
811
|
+
count,
|
|
812
|
+
avgProfit: pairProfits[pair] ? (pairProfits[pair].sum / pairProfits[pair].count).toFixed(4) : '0',
|
|
813
|
+
})),
|
|
814
|
+
dexUsage: dexCounts,
|
|
815
|
+
avgSpread: avgSpread.toFixed(4),
|
|
816
|
+
maxSpread: maxSpread.toFixed(4),
|
|
817
|
+
totalProfitUsd: totalProfit.toFixed(4),
|
|
818
|
+
profitableOppCount: profitableCount,
|
|
819
|
+
avgGasCostUsd: avgGas.toFixed(4),
|
|
820
|
+
types: {
|
|
821
|
+
scans: last7d.filter(h => h.type === 'scan').length,
|
|
822
|
+
executed: last7d.filter(h => h.type === 'executed').length,
|
|
823
|
+
dryRuns: last7d.filter(h => h.type === 'dry_run').length,
|
|
824
|
+
errors: last7d.filter(h => h.type === 'error').length,
|
|
825
|
+
},
|
|
826
|
+
};
|
|
827
|
+
}
|