@darksol/terminal 0.11.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.
- package/README.md +96 -1
- package/package.json +1 -1
- package/src/agent/autonomous.js +465 -0
- package/src/agent/strategy-evaluator.js +166 -0
- package/src/cli.js +218 -0
- package/src/config/keys.js +16 -0
- package/src/config/store.js +6 -0
- package/src/services/gas.js +35 -42
- package/src/services/watch.js +67 -61
- package/src/services/whale-monitor.js +388 -0
- package/src/services/whale.js +421 -0
- package/src/ui/dashboard.js +596 -0
- package/src/wallet/history.js +47 -46
- package/src/wallet/portfolio.js +75 -87
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ A unified CLI for market intel, trading, AI-powered analysis, on-chain oracle, c
|
|
|
15
15
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
16
16
|
[](https://nodejs.org/)
|
|
17
17
|
|
|
18
|
-
- Current release: **0.
|
|
18
|
+
- Current release: **0.12.0**
|
|
19
19
|
- Changelog: `CHANGELOG.md`
|
|
20
20
|
|
|
21
21
|
## Install
|
|
@@ -150,6 +150,9 @@ ai <prompt> # chat with trading assistant
|
|
|
150
150
|
| `dca` | Dollar-cost averaging engine | Gas only |
|
|
151
151
|
| `soul` | Agent identity & personality configuration | Free |
|
|
152
152
|
| `memory` | Persistent cross-session memory store | Free |
|
|
153
|
+
| `whale` | Whale Radar — track wallets, copy-trade, live feed | Free |
|
|
154
|
+
| `dash` | Live TUI dashboard — portfolio, prices, gas, whale feed | Free |
|
|
155
|
+
| `auto` | Autonomous Trader — goal-based automated execution | Provider dependent |
|
|
153
156
|
| `agent task` | Autonomous ReAct agent loop with tool use | Provider dependent |
|
|
154
157
|
| `ai` | LLM-powered trading assistant & intent execution | Provider dependent |
|
|
155
158
|
| `agent` | Secure agent signer (PK-isolated proxy) | Free |
|
|
@@ -176,6 +179,98 @@ ai <prompt> # chat with trading assistant
|
|
|
176
179
|
|
|
177
180
|
---
|
|
178
181
|
|
|
182
|
+
## 🐋 Whale Radar
|
|
183
|
+
|
|
184
|
+
Track any wallet across 5 chains. Get alerts on swaps, transfers, new tokens. Enable copy-trading to mirror a whale's moves automatically.
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# Track a wallet
|
|
188
|
+
darksol whale track 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --label "vitalik" --chain ethereum
|
|
189
|
+
|
|
190
|
+
# List all tracked wallets
|
|
191
|
+
darksol whale list
|
|
192
|
+
|
|
193
|
+
# View recent activity
|
|
194
|
+
darksol whale activity 0xd8dA... --limit 20
|
|
195
|
+
|
|
196
|
+
# Enable copy-trading (mirrors swaps with your own limits)
|
|
197
|
+
darksol whale mirror 0xd8dA... --max 50 --slippage 2 --dry-run
|
|
198
|
+
|
|
199
|
+
# Open the live feed (blessed TUI)
|
|
200
|
+
darksol whale feed
|
|
201
|
+
|
|
202
|
+
# Stop tracking
|
|
203
|
+
darksol whale stop 0xd8dA...
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
- **5-chain support:** Base, Ethereum, Arbitrum, Polygon, Optimism
|
|
207
|
+
- **Swap decoding:** Uniswap V2 + V3 router signatures automatically parsed
|
|
208
|
+
- **Copy-trading:** Mirror whale swaps with budget caps, slippage limits, dry-run mode
|
|
209
|
+
- **Live feed:** Real-time blessed terminal UI with whale events streaming
|
|
210
|
+
- **Daemon integration:** Runs as a background service, feeds alerts to Telegram bot
|
|
211
|
+
- **Event system:** Subscribe to `whale:swap`, `whale:transfer`, `whale:newtoken`, `whale:mirror-executed`
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## 📊 Live Dashboard
|
|
216
|
+
|
|
217
|
+
Full-screen terminal dashboard. Portfolio, prices, gas, transactions, whale alerts — all updating in real-time.
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
# Launch the dashboard
|
|
221
|
+
darksol dash
|
|
222
|
+
|
|
223
|
+
# Custom refresh interval
|
|
224
|
+
darksol dash --refresh 15
|
|
225
|
+
|
|
226
|
+
# Compact mode (portfolio + prices only)
|
|
227
|
+
darksol dash --compact
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- **Portfolio summary** — total value, token balances, chain breakdown
|
|
231
|
+
- **Price ticker** — sparkline micro-charts for tracked tokens
|
|
232
|
+
- **Gas gauge** — current gas prices across all 5 chains
|
|
233
|
+
- **Recent transactions** — last 10 txs from wallet history
|
|
234
|
+
- **Whale feed** — live alerts when whale monitor is running
|
|
235
|
+
- **Keyboard shortcuts:** `q` quit, `r` refresh, `tab` cycle focus, `w` toggle whales, `1-5` switch chains
|
|
236
|
+
- **DARKSOL gold/dark theme** throughout
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## 🤖 Autonomous Trader
|
|
241
|
+
|
|
242
|
+
Set a goal in plain English. The AI builds a strategy, monitors the market, and executes trades within your budget and risk limits. Full audit trail on every decision.
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
# Start an autonomous strategy
|
|
246
|
+
darksol auto start "accumulate ETH under 2400" --budget 500 --max-per-trade 50 --risk moderate
|
|
247
|
+
|
|
248
|
+
# DCA into memecoins
|
|
249
|
+
darksol auto start "DCA into BASE memecoins with >1M liquidity" --budget 200 --interval 15 --dry-run
|
|
250
|
+
|
|
251
|
+
# Check status
|
|
252
|
+
darksol auto status
|
|
253
|
+
darksol auto status auto_1741...
|
|
254
|
+
|
|
255
|
+
# View audit trail
|
|
256
|
+
darksol auto log auto_1741... --limit 20
|
|
257
|
+
|
|
258
|
+
# Stop a strategy
|
|
259
|
+
darksol auto stop auto_1741...
|
|
260
|
+
|
|
261
|
+
# List all strategies
|
|
262
|
+
darksol auto list
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
- **Natural language goals** — parsed by LLM intent system into executable strategies
|
|
266
|
+
- **Three risk levels:** conservative (5% stop-loss), moderate (10%), aggressive (20%)
|
|
267
|
+
- **Kill switches:** budget exhaustion, max loss, error threshold — auto-stops immediately
|
|
268
|
+
- **Dry-run mode** — test strategies without executing real trades
|
|
269
|
+
- **Full audit log** — every decision, trade, and skip logged to `~/.darksol/autonomous/<id>/audit.json`
|
|
270
|
+
- **Event system:** `auto:started`, `auto:trade`, `auto:skipped`, `auto:stopped`, `auto:budget-hit`, `auto:error`
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
179
274
|
## 📱 Telegram Bot
|
|
180
275
|
|
|
181
276
|
Turn your terminal into a Telegram AI agent. Same brain (LLM + soul + memory), different mouth.
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|