@darksol/terminal 0.10.0 → 0.12.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.
@@ -0,0 +1,465 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { parseIntent, adviseStrategy } from '../llm/intent.js';
6
+ import { getConfig, setConfig } from '../config/store.js';
7
+ import { executeSwap } from '../trading/swap.js';
8
+ import { runDCA } from '../trading/dca.js';
9
+ import { evaluateConditions, shouldTrade } from './strategy-evaluator.js';
10
+
11
+ const STRATEGIES_KEY = 'autonomous.strategies';
12
+ const AUTONOMOUS_DIR = join(homedir(), '.darksol', 'autonomous');
13
+ const runtimeTimers = new Map();
14
+ let strategySequence = 0;
15
+
16
+ export const autonomousEvents = new EventEmitter();
17
+
18
+ const autonomousDeps = {
19
+ parseIntent,
20
+ adviseStrategy,
21
+ evaluateConditions,
22
+ shouldTrade,
23
+ executeSwap,
24
+ runDCA,
25
+ now: () => Date.now(),
26
+ setInterval: global.setInterval.bind(global),
27
+ clearInterval: global.clearInterval.bind(global),
28
+ };
29
+
30
+ function riskProfile(level = 'moderate') {
31
+ const profiles = {
32
+ conservative: { stopLossPct: 5, maxErrors: 2, tradeShare: 0.1 },
33
+ moderate: { stopLossPct: 10, maxErrors: 3, tradeShare: 0.2 },
34
+ aggressive: { stopLossPct: 20, maxErrors: 5, tradeShare: 0.35 },
35
+ };
36
+ return profiles[level] || profiles.moderate;
37
+ }
38
+
39
+ function ensureDir(path) {
40
+ if (!existsSync(path)) mkdirSync(path, { recursive: true });
41
+ }
42
+
43
+ function ensureRootDir() {
44
+ ensureDir(AUTONOMOUS_DIR);
45
+ }
46
+
47
+ function strategyDir(id) {
48
+ return join(AUTONOMOUS_DIR, id);
49
+ }
50
+
51
+ function auditPath(id) {
52
+ return join(strategyDir(id), 'audit.json');
53
+ }
54
+
55
+ function loadStrategies() {
56
+ return getConfig(STRATEGIES_KEY) || [];
57
+ }
58
+
59
+ function saveStrategies(strategies) {
60
+ setConfig(STRATEGIES_KEY, strategies);
61
+ }
62
+
63
+ function findStrategy(id) {
64
+ return loadStrategies().find((item) => item.id === id || item.id.startsWith(id)) || null;
65
+ }
66
+
67
+ function persistStrategy(strategy) {
68
+ const strategies = loadStrategies();
69
+ const index = strategies.findIndex((item) => item.id === strategy.id);
70
+ if (index === -1) strategies.push(strategy);
71
+ else strategies[index] = strategy;
72
+ saveStrategies(strategies);
73
+ return strategy;
74
+ }
75
+
76
+ function safeJsonParse(raw, fallback) {
77
+ try {
78
+ return JSON.parse(raw);
79
+ } catch {
80
+ return fallback;
81
+ }
82
+ }
83
+
84
+ function appendAudit(id, entry) {
85
+ ensureRootDir();
86
+ const dir = strategyDir(id);
87
+ ensureDir(dir);
88
+ const file = auditPath(id);
89
+ const history = existsSync(file) ? safeJsonParse(readFileSync(file, 'utf8'), []) : [];
90
+ history.push(entry);
91
+ writeFileSync(file, JSON.stringify(history, null, 2));
92
+ }
93
+
94
+ function parseThreshold(goal, operator) {
95
+ const regex = operator === 'under'
96
+ ? /\b(?:under|below|<)\s*\$?([\d,.]+(?:\.\d+)?)(?:\s*[mk])?\b/i
97
+ : /\b(?:over|above|>)\s*\$?([\d,.]+(?:\.\d+)?)(?:\s*[mk])?\b/i;
98
+ const match = goal.match(regex);
99
+ if (!match) return null;
100
+ return Number(match[1].replace(/,/g, ''));
101
+ }
102
+
103
+ function parseLiquidity(goal) {
104
+ const match = goal.match(/>\s*([\d.]+)\s*([mk])?\s+liquidity/i) || goal.match(/liquidity\s*(?:above|over)\s*([\d.]+)\s*([mk])?/i);
105
+ if (!match) return null;
106
+ const base = Number(match[1]);
107
+ if (match[2]?.toLowerCase() === 'm') return base * 1_000_000;
108
+ if (match[2]?.toLowerCase() === 'k') return base * 1_000;
109
+ return base;
110
+ }
111
+
112
+ function inferPrimaryToken(goal, intent = {}) {
113
+ const candidates = [
114
+ intent.tokenOut,
115
+ intent.token,
116
+ goal.match(/\baccumulate\s+([A-Za-z0-9]+)/i)?.[1],
117
+ goal.match(/\binto\s+([A-Za-z0-9]+)/i)?.[1],
118
+ ].filter(Boolean);
119
+ return String(candidates[0] || 'ETH').toUpperCase();
120
+ }
121
+
122
+ function buildPlan(goal, intent, advisoryText, options) {
123
+ const risk = riskProfile(options.riskLevel);
124
+ const primaryToken = inferPrimaryToken(goal, intent);
125
+ const quoteToken = String(intent.tokenIn || 'USDC').toUpperCase();
126
+ const entryBelow = parseThreshold(goal, 'under');
127
+ const entryAbove = parseThreshold(goal, 'over');
128
+ const minLiquidity = parseLiquidity(goal) || (goal.match(/memecoin/i) ? 1_000_000 : 100_000);
129
+ const mode = /\bdca\b/i.test(goal) ? 'dca' : 'swing';
130
+ const takeProfitPct = options.riskLevel === 'aggressive' ? 20 : options.riskLevel === 'conservative' ? 8 : 12;
131
+
132
+ return {
133
+ summary: advisoryText || `Autonomous ${mode} strategy for ${primaryToken}`,
134
+ mode,
135
+ primaryToken,
136
+ quoteToken,
137
+ entry: {
138
+ token: primaryToken,
139
+ priceBelow: entryBelow,
140
+ priceAbove: entryAbove,
141
+ },
142
+ exit: {
143
+ takeProfitPct,
144
+ stopLossPct: risk.stopLossPct,
145
+ },
146
+ filters: {
147
+ minLiquidity,
148
+ category: goal.match(/memecoin/i) ? 'memecoins' : 'general',
149
+ },
150
+ cooldownMs: options.riskLevel === 'aggressive' ? 5 * 60 * 1000 : 15 * 60 * 1000,
151
+ };
152
+ }
153
+
154
+ function hydrateDerivedFields(strategy) {
155
+ const avgEntry = strategy.tradesExecuted > 0 && strategy.positionSize > 0
156
+ ? Number(strategy.costBasis || 0) / Number(strategy.positionSize || 1)
157
+ : null;
158
+ const takeProfitPrice = avgEntry ? avgEntry * (1 + Number(strategy.plan.exit.takeProfitPct || 0) / 100) : null;
159
+ const stopLossPrice = avgEntry ? avgEntry * (1 - Number(strategy.plan.exit.stopLossPct || 0) / 100) : null;
160
+ strategy.plan.exit.takeProfitPrice = takeProfitPrice;
161
+ strategy.plan.exit.stopLossPrice = stopLossPrice;
162
+ return strategy;
163
+ }
164
+
165
+ function computeTradeBudget(strategy, conditions) {
166
+ const profile = riskProfile(strategy.riskLevel);
167
+ const remaining = Math.max(0, Number(strategy.budget) - Number(strategy.spent));
168
+ const suggested = Math.min(Number(strategy.maxPerTrade), Number(strategy.budget) * profile.tradeShare, remaining);
169
+ return Number(conditions?.tradeAmount || suggested || remaining);
170
+ }
171
+
172
+ function updatePosition(strategy, decision, conditions, tradeAmount) {
173
+ const price = Number(conditions.price || 0);
174
+ if (!price || !tradeAmount) return strategy;
175
+
176
+ if (decision.action === 'buy') {
177
+ const units = tradeAmount / price;
178
+ strategy.spent = Number(strategy.spent) + tradeAmount;
179
+ strategy.costBasis = Number(strategy.costBasis || 0) + tradeAmount;
180
+ strategy.positionSize = Number(strategy.positionSize || 0) + units;
181
+ }
182
+
183
+ if (decision.action === 'sell') {
184
+ const currentPosition = Number(strategy.positionSize || 0);
185
+ const unitsToSell = currentPosition;
186
+ const proceeds = unitsToSell * price;
187
+ strategy.realizedPnl = Number(strategy.realizedPnl || 0) + (proceeds - Number(strategy.costBasis || 0));
188
+ strategy.positionSize = 0;
189
+ strategy.costBasis = 0;
190
+ }
191
+
192
+ strategy.pnl = Number(strategy.realizedPnl || 0);
193
+ strategy.lastPrice = price;
194
+ strategy.lastTradeAt = new Date(autonomousDeps.now()).toISOString();
195
+ strategy.tradesExecuted = Number(strategy.tradesExecuted || 0) + 1;
196
+ hydrateDerivedFields(strategy);
197
+ return strategy;
198
+ }
199
+
200
+ function finalizeStrategy(strategy, status, reason, eventName) {
201
+ strategy.status = status;
202
+ strategy.stopReason = reason;
203
+ strategy.nextCheckAt = null;
204
+ strategy.updatedAt = new Date(autonomousDeps.now()).toISOString();
205
+ persistStrategy(strategy);
206
+
207
+ const timer = runtimeTimers.get(strategy.id);
208
+ if (timer) {
209
+ autonomousDeps.clearInterval(timer);
210
+ runtimeTimers.delete(strategy.id);
211
+ }
212
+
213
+ appendAudit(strategy.id, {
214
+ timestamp: strategy.updatedAt,
215
+ type: 'stopped',
216
+ reason,
217
+ status,
218
+ });
219
+ autonomousEvents.emit(eventName || 'auto:stopped', { id: strategy.id, reason, strategy });
220
+ return strategy;
221
+ }
222
+
223
+ function scheduleStrategy(id) {
224
+ const existing = runtimeTimers.get(id);
225
+ if (existing) autonomousDeps.clearInterval(existing);
226
+ const strategy = findStrategy(id);
227
+ if (!strategy || strategy.status !== 'active') return;
228
+ const timer = autonomousDeps.setInterval(() => {
229
+ runStrategyCycle(id).catch(() => {});
230
+ }, strategy.intervalMs);
231
+ runtimeTimers.set(id, timer);
232
+ }
233
+
234
+ async function executeDecision(strategy, decision, conditions) {
235
+ const token = strategy.plan.primaryToken;
236
+ const quoteToken = strategy.plan.quoteToken;
237
+ const tradeAmount = computeTradeBudget(strategy, conditions);
238
+ const tradeEvent = {
239
+ timestamp: new Date(autonomousDeps.now()).toISOString(),
240
+ type: 'trade',
241
+ action: decision.action,
242
+ token,
243
+ amount: tradeAmount,
244
+ reason: decision.reason,
245
+ confidence: decision.confidence,
246
+ price: conditions.price,
247
+ dryRun: strategy.dryRun,
248
+ };
249
+
250
+ if (strategy.dryRun) {
251
+ updatePosition(strategy, decision, conditions, tradeAmount);
252
+ strategy.tradeHistory.push({ ...tradeEvent, result: { success: true, dryRun: true } });
253
+ appendAudit(strategy.id, { ...tradeEvent, result: { success: true, dryRun: true } });
254
+ autonomousEvents.emit('auto:trade', { id: strategy.id, trade: tradeEvent, dryRun: true });
255
+ return strategy;
256
+ }
257
+
258
+ const tradeOpts = decision.action === 'buy'
259
+ ? { tokenIn: quoteToken, tokenOut: token, amount: tradeAmount.toFixed(2), confirm: true }
260
+ : { tokenIn: token, tokenOut: quoteToken, amount: String(Number(strategy.positionSize || 0)), confirm: true };
261
+ const result = strategy.plan.mode === 'dca' && strategy.plan.useDcaExecutor
262
+ ? await autonomousDeps.runDCA({ password: strategy.password })
263
+ : await autonomousDeps.executeSwap(tradeOpts);
264
+
265
+ updatePosition(strategy, decision, conditions, tradeAmount);
266
+ strategy.tradeHistory.push({ ...tradeEvent, result: result || null });
267
+ appendAudit(strategy.id, { ...tradeEvent, result: result || null });
268
+ autonomousEvents.emit('auto:trade', { id: strategy.id, trade: tradeEvent, result });
269
+ return strategy;
270
+ }
271
+
272
+ function checkKillSwitch(strategy) {
273
+ const maxLoss = -Math.abs(Number(strategy.maxLoss || 0));
274
+ if (Number(strategy.spent) >= Number(strategy.budget)) {
275
+ finalizeStrategy(strategy, 'completed', 'budget_exhausted', 'auto:budget-hit');
276
+ return true;
277
+ }
278
+ if (Number(strategy.pnl || 0) <= maxLoss) {
279
+ finalizeStrategy(strategy, 'completed', 'max_loss_hit', 'auto:stopped');
280
+ return true;
281
+ }
282
+ if (Number(strategy.errorCount || 0) >= Number(strategy.maxErrors || 0)) {
283
+ finalizeStrategy(strategy, 'completed', 'error_threshold', 'auto:error');
284
+ return true;
285
+ }
286
+ return false;
287
+ }
288
+
289
+ export async function runStrategyCycle(id) {
290
+ const strategy = findStrategy(id);
291
+ if (!strategy || strategy.status !== 'active') return null;
292
+ if (checkKillSwitch(strategy)) return strategy;
293
+
294
+ strategy.updatedAt = new Date(autonomousDeps.now()).toISOString();
295
+
296
+ try {
297
+ const conditions = await autonomousDeps.evaluateConditions(strategy);
298
+ const decision = await autonomousDeps.shouldTrade(strategy, conditions);
299
+
300
+ appendAudit(strategy.id, {
301
+ timestamp: strategy.updatedAt,
302
+ type: 'decision',
303
+ conditions,
304
+ decision,
305
+ });
306
+
307
+ if (decision.action === 'hold') {
308
+ strategy.lastDecision = decision.reason;
309
+ strategy.nextCheckAt = new Date(autonomousDeps.now() + strategy.intervalMs).toISOString();
310
+ persistStrategy(strategy);
311
+ autonomousEvents.emit('auto:skipped', { id: strategy.id, decision, conditions });
312
+ return strategy;
313
+ }
314
+
315
+ await executeDecision(strategy, decision, conditions);
316
+ strategy.lastDecision = decision.reason;
317
+ strategy.nextCheckAt = new Date(autonomousDeps.now() + strategy.intervalMs).toISOString();
318
+ persistStrategy(strategy);
319
+
320
+ if (checkKillSwitch(strategy)) return strategy;
321
+ return strategy;
322
+ } catch (err) {
323
+ strategy.errorCount = Number(strategy.errorCount || 0) + 1;
324
+ strategy.lastError = err.message;
325
+ strategy.nextCheckAt = new Date(autonomousDeps.now() + strategy.intervalMs).toISOString();
326
+ persistStrategy(strategy);
327
+ appendAudit(strategy.id, {
328
+ timestamp: strategy.updatedAt,
329
+ type: 'error',
330
+ message: err.message,
331
+ errorCount: strategy.errorCount,
332
+ });
333
+ autonomousEvents.emit('auto:error', { id: strategy.id, error: err });
334
+ checkKillSwitch(strategy);
335
+ return strategy;
336
+ }
337
+ }
338
+
339
+ export async function startAutonomous(goal, options = {}) {
340
+ ensureRootDir();
341
+ const parsedOptions = {
342
+ budget: Number(options.budget || 0),
343
+ maxPerTrade: Number(options.maxPerTrade || options.budget || 0),
344
+ riskLevel: options.riskLevel || 'moderate',
345
+ intervalMs: Math.max(1, Number(options.interval || 5)) * 60 * 1000,
346
+ chains: Array.isArray(options.chains) && options.chains.length ? options.chains : ['base'],
347
+ dryRun: Boolean(options.dryRun),
348
+ };
349
+
350
+ const intent = await autonomousDeps.parseIntent(goal, options);
351
+ const strategyAdvice = await autonomousDeps.adviseStrategy(
352
+ intent.tokenOut || intent.token || inferPrimaryToken(goal, intent),
353
+ parsedOptions.budget || parsedOptions.maxPerTrade || 0,
354
+ `${Math.max(1, Number(options.interval || 5))} minute cadence`,
355
+ options,
356
+ ).catch(() => null);
357
+
358
+ const id = `auto_${autonomousDeps.now()}_${++strategySequence}`;
359
+ const profile = riskProfile(parsedOptions.riskLevel);
360
+ const plan = buildPlan(goal, intent, strategyAdvice?.content || strategyAdvice?.summary || '', parsedOptions);
361
+ const strategy = hydrateDerivedFields({
362
+ id,
363
+ goal,
364
+ intent,
365
+ plan,
366
+ status: 'active',
367
+ budget: parsedOptions.budget,
368
+ spent: 0,
369
+ costBasis: 0,
370
+ positionSize: 0,
371
+ tradesExecuted: 0,
372
+ tradeHistory: [],
373
+ pnl: 0,
374
+ realizedPnl: 0,
375
+ maxPerTrade: parsedOptions.maxPerTrade,
376
+ maxLoss: parsedOptions.budget * (profile.stopLossPct / 100),
377
+ maxErrors: profile.maxErrors,
378
+ errorCount: 0,
379
+ riskLevel: parsedOptions.riskLevel,
380
+ intervalMs: parsedOptions.intervalMs,
381
+ chains: parsedOptions.chains,
382
+ dryRun: parsedOptions.dryRun,
383
+ createdAt: new Date(autonomousDeps.now()).toISOString(),
384
+ startedAt: new Date(autonomousDeps.now()).toISOString(),
385
+ updatedAt: new Date(autonomousDeps.now()).toISOString(),
386
+ nextCheckAt: new Date(autonomousDeps.now() + parsedOptions.intervalMs).toISOString(),
387
+ lastDecision: '',
388
+ stopReason: '',
389
+ lastError: '',
390
+ });
391
+
392
+ persistStrategy(strategy);
393
+ appendAudit(id, {
394
+ timestamp: strategy.createdAt,
395
+ type: 'started',
396
+ goal,
397
+ options: parsedOptions,
398
+ intent,
399
+ plan,
400
+ });
401
+ autonomousEvents.emit('auto:started', { id, strategy });
402
+ scheduleStrategy(id);
403
+ return strategy;
404
+ }
405
+
406
+ export function stopAutonomous(id) {
407
+ const strategy = findStrategy(id);
408
+ if (!strategy) return null;
409
+ return finalizeStrategy(strategy, 'paused', 'manual_stop', 'auto:stopped');
410
+ }
411
+
412
+ export function getStatus(id) {
413
+ if (!id) return listStrategies();
414
+ const strategy = findStrategy(id);
415
+ if (!strategy) return null;
416
+ return {
417
+ id: strategy.id,
418
+ goal: strategy.goal,
419
+ status: strategy.status,
420
+ spent: strategy.spent,
421
+ budget: strategy.budget,
422
+ tradesExecuted: strategy.tradesExecuted,
423
+ pnl: strategy.pnl,
424
+ nextCheckAt: strategy.nextCheckAt,
425
+ riskLevel: strategy.riskLevel,
426
+ dryRun: strategy.dryRun,
427
+ lastDecision: strategy.lastDecision,
428
+ };
429
+ }
430
+
431
+ export function listStrategies() {
432
+ return loadStrategies().map((strategy) => ({
433
+ id: strategy.id,
434
+ goal: strategy.goal,
435
+ status: strategy.status,
436
+ spent: strategy.spent,
437
+ budget: strategy.budget,
438
+ tradesExecuted: strategy.tradesExecuted,
439
+ pnl: strategy.pnl,
440
+ nextCheckAt: strategy.nextCheckAt,
441
+ }));
442
+ }
443
+
444
+ export function getAuditLog(id, limit = 50) {
445
+ const file = auditPath(id);
446
+ if (!existsSync(file)) return [];
447
+ const history = safeJsonParse(readFileSync(file, 'utf8'), []);
448
+ return history.slice(-Math.max(1, Number(limit || 50)));
449
+ }
450
+
451
+ export function __setAutonomousDeps(overrides = {}) {
452
+ Object.assign(autonomousDeps, overrides);
453
+ }
454
+
455
+ export function __resetAutonomousDeps() {
456
+ autonomousDeps.parseIntent = parseIntent;
457
+ autonomousDeps.adviseStrategy = adviseStrategy;
458
+ autonomousDeps.evaluateConditions = evaluateConditions;
459
+ autonomousDeps.shouldTrade = shouldTrade;
460
+ autonomousDeps.executeSwap = executeSwap;
461
+ autonomousDeps.runDCA = runDCA;
462
+ autonomousDeps.now = () => Date.now();
463
+ autonomousDeps.setInterval = global.setInterval.bind(global);
464
+ autonomousDeps.clearInterval = global.clearInterval.bind(global);
465
+ }
@@ -0,0 +1,166 @@
1
+ import { topMovers } from '../services/market.js';
2
+ import { checkPrices } from '../services/watch.js';
3
+ import { getConfig } from '../config/store.js';
4
+ import { estimateGasCost, getProvider, quickPrice } from '../utils/helpers.js';
5
+
6
+ const DEFAULT_COOLDOWN_MS = 15 * 60 * 1000;
7
+
8
+ const evaluatorDeps = {
9
+ quickPrice,
10
+ topMovers,
11
+ checkPrices,
12
+ getProvider,
13
+ estimateGasCost,
14
+ now: () => Date.now(),
15
+ };
16
+
17
+ function riskProfile(level = 'moderate') {
18
+ const profiles = {
19
+ conservative: { gasGwei: 1.5, cooldownMs: 30 * 60 * 1000, minConfidence: 0.75 },
20
+ moderate: { gasGwei: 3, cooldownMs: 15 * 60 * 1000, minConfidence: 0.6 },
21
+ aggressive: { gasGwei: 10, cooldownMs: 5 * 60 * 1000, minConfidence: 0.45 },
22
+ };
23
+ return profiles[level] || profiles.moderate;
24
+ }
25
+
26
+ function normalizeToken(symbol) {
27
+ return typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
28
+ }
29
+
30
+ async function collectMarketData(strategy, deps = evaluatorDeps) {
31
+ const primaryToken = normalizeToken(
32
+ strategy?.plan?.primaryToken
33
+ || strategy?.plan?.entry?.token
34
+ || strategy?.intent?.tokenOut
35
+ || strategy?.intent?.token
36
+ );
37
+ const quoteToken = normalizeToken(strategy?.plan?.quoteToken || strategy?.intent?.tokenIn || 'USDC');
38
+ const currentPrice = primaryToken ? await deps.quickPrice(primaryToken) : null;
39
+
40
+ try {
41
+ if (primaryToken) {
42
+ await deps.checkPrices([primaryToken]);
43
+ }
44
+ } catch {}
45
+
46
+ return {
47
+ primaryToken,
48
+ quoteToken,
49
+ currentPrice,
50
+ price: currentPrice?.price ? Number(currentPrice.price) : null,
51
+ liquidity: currentPrice?.liquidity ? Number(currentPrice.liquidity) : 0,
52
+ change24h: currentPrice?.change24h !== undefined ? Number(currentPrice.change24h) : null,
53
+ volume24h: currentPrice?.volume24h ? Number(currentPrice.volume24h) : 0,
54
+ timestamp: new Date(deps.now()).toISOString(),
55
+ };
56
+ }
57
+
58
+ export async function evaluateConditions(strategy) {
59
+ const deps = evaluatorDeps;
60
+ const profile = riskProfile(strategy?.riskLevel);
61
+ const marketData = await collectMarketData(strategy, deps);
62
+ const provider = deps.getProvider(strategy?.chains?.[0] || getConfig('chain') || 'base');
63
+ const gas = await deps.estimateGasCost(provider, 180000n);
64
+ const gasGwei = Number(gas.gwei || 0);
65
+ const gasTooHigh = gasGwei > profile.gasGwei;
66
+
67
+ const lastTradeAt = strategy?.lastTradeAt ? new Date(strategy.lastTradeAt).getTime() : 0;
68
+ const cooldownMs = strategy?.plan?.cooldownMs || profile.cooldownMs || DEFAULT_COOLDOWN_MS;
69
+ const inCooldown = Boolean(lastTradeAt) && (deps.now() - lastTradeAt) < cooldownMs;
70
+ const minLiquidity = Number(strategy?.plan?.filters?.minLiquidity || 0);
71
+ const priceBelow = strategy?.plan?.entry?.priceBelow;
72
+ const priceAbove = strategy?.plan?.entry?.priceAbove;
73
+ const hasPrice = typeof marketData.price === 'number' && !Number.isNaN(marketData.price);
74
+
75
+ const meetsLiquidity = marketData.liquidity >= minLiquidity;
76
+ const meetsPriceBelow = !priceBelow || (hasPrice && marketData.price <= priceBelow);
77
+ const meetsPriceAbove = !priceAbove || (hasPrice && marketData.price >= priceAbove);
78
+ const hasBudget = Number(strategy?.budget || 0) > Number(strategy?.spent || 0);
79
+ const maxPerTrade = Number(strategy?.maxPerTrade || strategy?.budget || 0);
80
+ const remainingBudget = Math.max(0, Number(strategy?.budget || 0) - Number(strategy?.spent || 0));
81
+ const tradeAmount = Math.min(maxPerTrade, remainingBudget);
82
+ const withinTradeLimit = tradeAmount > 0 && tradeAmount <= maxPerTrade;
83
+
84
+ return {
85
+ ...marketData,
86
+ gasGwei,
87
+ gasTooHigh,
88
+ inCooldown,
89
+ cooldownMs,
90
+ remainingBudget,
91
+ tradeAmount,
92
+ withinTradeLimit,
93
+ meetsLiquidity,
94
+ meetsEntry: hasBudget && meetsLiquidity && meetsPriceBelow && meetsPriceAbove,
95
+ meetsExit: Boolean(
96
+ hasPrice
97
+ && (
98
+ (strategy?.plan?.exit?.takeProfitPrice && marketData.price >= strategy.plan.exit.takeProfitPrice)
99
+ || (strategy?.plan?.exit?.stopLossPrice && marketData.price <= strategy.plan.exit.stopLossPrice)
100
+ )
101
+ ),
102
+ };
103
+ }
104
+
105
+ export async function shouldTrade(strategy, marketData) {
106
+ const profile = riskProfile(strategy?.riskLevel);
107
+ const conditions = marketData || await evaluateConditions(strategy);
108
+ const amount = Number(conditions.tradeAmount || 0);
109
+
110
+ if (!conditions.currentPrice) {
111
+ return { action: 'hold', reason: 'No market data for target token', confidence: 0.1 };
112
+ }
113
+ if (conditions.gasTooHigh) {
114
+ return { action: 'hold', reason: `Gas too high (${conditions.gasGwei.toFixed(2)} gwei)`, confidence: 0.15 };
115
+ }
116
+ if (!conditions.withinTradeLimit) {
117
+ return { action: 'hold', reason: 'Per-trade or budget limit reached', confidence: 0.1 };
118
+ }
119
+ if (conditions.inCooldown) {
120
+ return { action: 'hold', reason: 'Token cooldown active', confidence: 0.2 };
121
+ }
122
+ if (conditions.meetsExit && Number(strategy?.positionSize || 0) > 0) {
123
+ return { action: 'sell', reason: 'Exit conditions met', confidence: 0.8 };
124
+ }
125
+ if (!conditions.meetsEntry) {
126
+ const reasons = [];
127
+ if (!conditions.meetsLiquidity) reasons.push('liquidity below filter');
128
+ if (strategy?.plan?.entry?.priceBelow && conditions.price > strategy.plan.entry.priceBelow) reasons.push('price above entry threshold');
129
+ if (strategy?.plan?.entry?.priceAbove && conditions.price < strategy.plan.entry.priceAbove) reasons.push('price below momentum threshold');
130
+ if (amount <= 0) reasons.push('budget exhausted');
131
+ return { action: 'hold', reason: reasons.join(', ') || 'Entry conditions not met', confidence: 0.25 };
132
+ }
133
+
134
+ let confidence = 0.55;
135
+ if (strategy?.plan?.entry?.priceBelow && conditions.price <= strategy.plan.entry.priceBelow) confidence += 0.15;
136
+ if (conditions.liquidity >= Number(strategy?.plan?.filters?.minLiquidity || 0) * 2) confidence += 0.1;
137
+ if (typeof conditions.change24h === 'number') {
138
+ if (strategy?.plan?.mode === 'dca') confidence += 0.05;
139
+ else if (conditions.change24h > 0) confidence += 0.1;
140
+ else confidence -= 0.05;
141
+ }
142
+
143
+ confidence = Math.max(0, Math.min(0.99, confidence));
144
+ if (confidence < profile.minConfidence) {
145
+ return { action: 'hold', reason: 'Signal below risk threshold', confidence };
146
+ }
147
+
148
+ return {
149
+ action: 'buy',
150
+ reason: `Entry conditions met for ${conditions.primaryToken} using ${amount.toFixed(2)} ${conditions.quoteToken}`,
151
+ confidence,
152
+ };
153
+ }
154
+
155
+ export function __setEvaluatorDeps(overrides = {}) {
156
+ Object.assign(evaluatorDeps, overrides);
157
+ }
158
+
159
+ export function __resetEvaluatorDeps() {
160
+ evaluatorDeps.quickPrice = quickPrice;
161
+ evaluatorDeps.topMovers = topMovers;
162
+ evaluatorDeps.checkPrices = checkPrices;
163
+ evaluatorDeps.getProvider = getProvider;
164
+ evaluatorDeps.estimateGasCost = estimateGasCost;
165
+ evaluatorDeps.now = () => Date.now();
166
+ }
@@ -0,0 +1,58 @@
1
+ import { sendBrowserCommand } from '../services/browser.js';
2
+
3
+ const DEFAULT_TIMEOUT = 30_000;
4
+
5
+ export async function waitForPage(target, opts = {}) {
6
+ const expression = JSON.stringify(target);
7
+ return sendBrowserCommand('eval', {
8
+ expression: `
9
+ new Promise((resolve) => {
10
+ const target = ${expression};
11
+ const timeout = ${Number(opts.timeout || DEFAULT_TIMEOUT)};
12
+ const start = Date.now();
13
+ const check = () => {
14
+ const matches = typeof target === 'string'
15
+ ? window.location.href.includes(target)
16
+ : true;
17
+ if (matches) return resolve({ ok: true, url: window.location.href });
18
+ if (Date.now() - start > timeout) return resolve({ ok: false, url: window.location.href });
19
+ setTimeout(check, 250);
20
+ };
21
+ check();
22
+ })
23
+ `,
24
+ });
25
+ }
26
+
27
+ export async function fillForm(fields = [], opts = {}) {
28
+ for (const field of fields) {
29
+ await sendBrowserCommand('type', {
30
+ selector: field.selector,
31
+ text: field.value,
32
+ timeout: opts.timeout || DEFAULT_TIMEOUT,
33
+ });
34
+ }
35
+ return true;
36
+ }
37
+
38
+ export async function runLoginFlow(flow = {}) {
39
+ if (flow.url) {
40
+ await sendBrowserCommand('navigate', {
41
+ url: flow.url,
42
+ timeout: flow.timeout || DEFAULT_TIMEOUT,
43
+ });
44
+ }
45
+ if (Array.isArray(flow.fields) && flow.fields.length) {
46
+ await fillForm(flow.fields, flow);
47
+ }
48
+ if (flow.submitSelector) {
49
+ await sendBrowserCommand('click', {
50
+ selector: flow.submitSelector,
51
+ timeout: flow.timeout || DEFAULT_TIMEOUT,
52
+ });
53
+ }
54
+ if (flow.waitFor) {
55
+ await waitForPage(flow.waitFor, flow);
56
+ }
57
+ return sendBrowserCommand('status');
58
+ }