@darksol/terminal 0.12.0 → 0.13.1

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.
@@ -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
+ }