hood-traders 0.1.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/LICENSE +201 -0
- package/README.md +214 -0
- package/dashboard/app.js +215 -0
- package/dashboard/index.html +44 -0
- package/dashboard/style.css +309 -0
- package/dist/chunk-NXL2F7EP.js +1371 -0
- package/dist/chunk-NXL2F7EP.js.map +1 -0
- package/dist/index.d.ts +697 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/main.js +59 -0
- package/dist/main.js.map +1 -0
- package/package.json +75 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
import { Address, Hash, Account } from 'viem';
|
|
2
|
+
import * as hoodchain from 'hoodchain';
|
|
3
|
+
import { HoodNetwork, HoodClient, SwapQuote, StockQuote } from 'hoodchain';
|
|
4
|
+
import { Server } from 'node:http';
|
|
5
|
+
|
|
6
|
+
/** Trading mode. `paper` simulates fills against live liquidity; `live` signs real swaps. */
|
|
7
|
+
type Mode = 'paper' | 'live';
|
|
8
|
+
/** A single actionable instruction produced by a strategy's `decide` step. */
|
|
9
|
+
interface Intent {
|
|
10
|
+
/** `buy` = spend the quote token to acquire `token`; `sell` = the reverse. */
|
|
11
|
+
side: 'buy' | 'sell';
|
|
12
|
+
/** The non-quote token being acquired (buy) or disposed (sell). */
|
|
13
|
+
token: Address;
|
|
14
|
+
/** Human label for the token, for journal/dashboard readability. */
|
|
15
|
+
tokenSymbol: string;
|
|
16
|
+
/**
|
|
17
|
+
* Amount of the INPUT token, in its smallest unit. On a `buy` the input is the
|
|
18
|
+
* quote token (spend N USDG/WETH → receive `token`); on a `sell` the input is
|
|
19
|
+
* `token` itself (sell N token units → receive the quote token).
|
|
20
|
+
*/
|
|
21
|
+
amountIn: bigint;
|
|
22
|
+
/** Which token `amountIn` is denominated in (what leaves the wallet on a buy). */
|
|
23
|
+
quoteToken: Address;
|
|
24
|
+
/** Quote token symbol, e.g. `USDG` or `WETH`. */
|
|
25
|
+
quoteSymbol: string;
|
|
26
|
+
/** Free-text reason the strategy fired — surfaced verbatim in the decision journal. */
|
|
27
|
+
reason: string;
|
|
28
|
+
/** Optional per-intent slippage override (bps). Falls back to the agent's cap. */
|
|
29
|
+
maxSlippageBps?: number;
|
|
30
|
+
/** Opaque strategy metadata persisted with the decision (take-profit level, curve %, premium bps…). */
|
|
31
|
+
meta?: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
/** An alert a strategy raises without trading (e.g. premium-watch in alerts-only mode). */
|
|
34
|
+
interface Alert {
|
|
35
|
+
level: 'info' | 'warn';
|
|
36
|
+
message: string;
|
|
37
|
+
meta?: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
/** What a strategy returns each tick. */
|
|
40
|
+
interface Decision {
|
|
41
|
+
intents: Intent[];
|
|
42
|
+
alerts: Alert[];
|
|
43
|
+
}
|
|
44
|
+
/** A position the agent is currently holding (paper or live). */
|
|
45
|
+
interface Position {
|
|
46
|
+
token: Address;
|
|
47
|
+
tokenSymbol: string;
|
|
48
|
+
/** Token units held (smallest unit). */
|
|
49
|
+
amount: bigint;
|
|
50
|
+
/** Quote token spent to open (net of sells), smallest unit. */
|
|
51
|
+
costBasis: bigint;
|
|
52
|
+
/** USD cost basis at time of each buy (net of proportional sells) — the number the position cap bounds. */
|
|
53
|
+
investedUsd: number;
|
|
54
|
+
quoteToken: Address;
|
|
55
|
+
quoteSymbol: string;
|
|
56
|
+
/** ms epoch the position opened. */
|
|
57
|
+
openedAt: number;
|
|
58
|
+
/** Marked exit value in USD at last tick (null until first mark). */
|
|
59
|
+
markUsd: number | null;
|
|
60
|
+
/** Strategy metadata carried from the opening intent. */
|
|
61
|
+
meta: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
/** Reason an intent was refused before execution. */
|
|
64
|
+
type RefusalReason = 'position_cap' | 'daily_cap' | 'fleet_daily_cap' | 'slippage_bound' | 'cooldown' | 'kill_switch' | 'no_route' | 'eligibility_gate' | 'insufficient_balance' | 'zero_amount';
|
|
65
|
+
/** A journaled trade (executed or simulated). */
|
|
66
|
+
interface TradeRecord {
|
|
67
|
+
id?: number;
|
|
68
|
+
agentId: string;
|
|
69
|
+
mode: Mode;
|
|
70
|
+
ts: number;
|
|
71
|
+
side: 'buy' | 'sell';
|
|
72
|
+
token: Address;
|
|
73
|
+
tokenSymbol: string;
|
|
74
|
+
quoteToken: Address;
|
|
75
|
+
quoteSymbol: string;
|
|
76
|
+
amountIn: bigint;
|
|
77
|
+
amountOut: bigint;
|
|
78
|
+
/** null in paper mode; the swap tx hash in live mode. */
|
|
79
|
+
txHash: Hash | null;
|
|
80
|
+
reason: string;
|
|
81
|
+
slippageBps: number;
|
|
82
|
+
gasEstimate: bigint;
|
|
83
|
+
meta: Record<string, unknown>;
|
|
84
|
+
}
|
|
85
|
+
/** A journaled decision that did NOT result in a trade (refused / held / alerted). */
|
|
86
|
+
interface DecisionRecord {
|
|
87
|
+
id?: number;
|
|
88
|
+
agentId: string;
|
|
89
|
+
ts: number;
|
|
90
|
+
kind: 'refused' | 'alert' | 'observe';
|
|
91
|
+
detail: string;
|
|
92
|
+
meta: Record<string, unknown>;
|
|
93
|
+
}
|
|
94
|
+
/** A point on an agent's equity curve. */
|
|
95
|
+
interface EquityPoint {
|
|
96
|
+
agentId: string;
|
|
97
|
+
ts: number;
|
|
98
|
+
/** Realized PnL to date (quote units, USDG cents-style as float USD). */
|
|
99
|
+
realizedUsd: number;
|
|
100
|
+
/** Marked value of open positions (USD). */
|
|
101
|
+
openValueUsd: number;
|
|
102
|
+
/** realized + open. */
|
|
103
|
+
equityUsd: number;
|
|
104
|
+
}
|
|
105
|
+
/** Per-agent risk limits. */
|
|
106
|
+
interface RiskLimits {
|
|
107
|
+
maxPositionUsdg: number;
|
|
108
|
+
maxDailySpendUsdg: number;
|
|
109
|
+
maxSlippageBps: number;
|
|
110
|
+
cooldownSeconds: number;
|
|
111
|
+
}
|
|
112
|
+
/** Live snapshot of an agent for the dashboard. */
|
|
113
|
+
interface AgentStatus {
|
|
114
|
+
id: string;
|
|
115
|
+
strategy: string;
|
|
116
|
+
mode: Mode;
|
|
117
|
+
running: boolean;
|
|
118
|
+
killed: boolean;
|
|
119
|
+
limits: RiskLimits;
|
|
120
|
+
spentTodayUsd: number;
|
|
121
|
+
realizedUsd: number;
|
|
122
|
+
openValueUsd: number;
|
|
123
|
+
equityUsd: number;
|
|
124
|
+
positions: Position[];
|
|
125
|
+
lastTickAt: number | null;
|
|
126
|
+
lastError: string | null;
|
|
127
|
+
ticks: number;
|
|
128
|
+
trades: number;
|
|
129
|
+
refusals: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The decision journal — the agent's black box recorder. Every observe, every
|
|
134
|
+
* refusal, every trade (paper or live), and every equity mark lands here so the
|
|
135
|
+
* dashboard can answer "why did this trade fire?" and the whole run is auditable
|
|
136
|
+
* after the fact.
|
|
137
|
+
*
|
|
138
|
+
* SQLite via better-sqlite3 (synchronous, zero-config, embedded). bigints are
|
|
139
|
+
* stored as decimal TEXT — SQLite integers are 64-bit signed and token amounts
|
|
140
|
+
* routinely exceed that, so TEXT is the only lossless option.
|
|
141
|
+
*/
|
|
142
|
+
declare class Journal {
|
|
143
|
+
private readonly db;
|
|
144
|
+
constructor(dbPath: string);
|
|
145
|
+
private migrate;
|
|
146
|
+
recordTrade(t: TradeRecord): number;
|
|
147
|
+
recordDecision(d: DecisionRecord): number;
|
|
148
|
+
recordEquity(e: EquityPoint): void;
|
|
149
|
+
/** Sum of USDG-denominated spend by an agent since a UTC-day boundary (ms). */
|
|
150
|
+
spentSince(agentId: string, sinceMs: number, quoteSymbol?: string): number;
|
|
151
|
+
recentTrades(agentId: string, limit?: number): TradeRecord[];
|
|
152
|
+
recentDecisions(agentId: string, limit?: number): DecisionRecord[];
|
|
153
|
+
equityCurve(agentId: string, limit?: number): EquityPoint[];
|
|
154
|
+
/** All trades across every agent, newest first — for the fleet-wide feed. */
|
|
155
|
+
allRecentTrades(limit?: number): TradeRecord[];
|
|
156
|
+
close(): void;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Fleet-wide configuration resolved from the environment. */
|
|
160
|
+
interface FleetConfig {
|
|
161
|
+
network: HoodNetwork;
|
|
162
|
+
rpcUrl: string | undefined;
|
|
163
|
+
mode: Mode;
|
|
164
|
+
/** Set true only when HOOD_TRADERS_LIVE=1 AND a key is present. */
|
|
165
|
+
hasWallet: boolean;
|
|
166
|
+
privateKey: `0x${string}` | undefined;
|
|
167
|
+
stockTokenEligible: boolean;
|
|
168
|
+
fleetMaxDailySpendUsdg: number;
|
|
169
|
+
dashboardPort: number;
|
|
170
|
+
killFile: string;
|
|
171
|
+
/** Path to the SQLite journal. */
|
|
172
|
+
dbPath: string;
|
|
173
|
+
defaultLimits: RiskLimits;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Resolve fleet configuration from the environment.
|
|
177
|
+
*
|
|
178
|
+
* Live mode is deliberately hard to enable by accident: it requires BOTH
|
|
179
|
+
* `HOOD_TRADERS_LIVE=1` and a `ROBINHOOD_CHAIN_PRIVATE_KEY`. Missing either one
|
|
180
|
+
* falls back to paper mode rather than erroring, so a mis-set flag can never
|
|
181
|
+
* silently spend real funds.
|
|
182
|
+
*/
|
|
183
|
+
declare function loadFleetConfig(env?: NodeJS.ProcessEnv): FleetConfig;
|
|
184
|
+
|
|
185
|
+
/** A memecoin/token spot price sourced from live Uniswap v3 liquidity. */
|
|
186
|
+
interface SpotPrice {
|
|
187
|
+
token: Address;
|
|
188
|
+
/** USD value of one whole token, derived from a probe quote against USDG (direct or via WETH). */
|
|
189
|
+
priceUsd: number;
|
|
190
|
+
/** Quote token the probe routed through. */
|
|
191
|
+
via: 'usdg' | 'weth';
|
|
192
|
+
ts: number;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The market adapter. Everything the observe/simulate steps need, sourced from
|
|
196
|
+
* live Robinhood Chain state through the `hoodchain` SDK — Chainlink stock
|
|
197
|
+
* feeds, Uniswap v3 quotes (which ARE `eth_call` simulations against real
|
|
198
|
+
* pools), and an ETH/USD reference derived from the on-chain USDG/WETH pool.
|
|
199
|
+
*
|
|
200
|
+
* No price here is fabricated: a token with no liquidity resolves to `null`, and
|
|
201
|
+
* strategies must handle that rather than trade a made-up number.
|
|
202
|
+
*/
|
|
203
|
+
declare class Market {
|
|
204
|
+
readonly client: HoodClient;
|
|
205
|
+
readonly usdg: Address;
|
|
206
|
+
readonly weth: Address;
|
|
207
|
+
readonly usdgDecimals = 6;
|
|
208
|
+
private ethUsdCache;
|
|
209
|
+
constructor(config: FleetConfig, account?: Account);
|
|
210
|
+
/** Latest block number — a cheap liveness/observe heartbeat. */
|
|
211
|
+
blockNumber(): Promise<bigint>;
|
|
212
|
+
/**
|
|
213
|
+
* ETH price in USD from the on-chain USDG/WETH pool (quote 0.01 WETH → USDG).
|
|
214
|
+
* Cached for 30s — it moves slowly relative to a trading tick and every call
|
|
215
|
+
* is a real RPC round trip. Returns `null` if no USDG/WETH pool answers.
|
|
216
|
+
*/
|
|
217
|
+
ethUsd(maxAgeMs?: number, now?: number): Promise<number | null>;
|
|
218
|
+
/**
|
|
219
|
+
* Spot USD price of one whole `token` from live liquidity. Probes token→USDG
|
|
220
|
+
* directly, then token→WETH→USDG, using a 1-token probe. Returns `null` when
|
|
221
|
+
* neither route has liquidity.
|
|
222
|
+
*/
|
|
223
|
+
spotPrice(token: Address, tokenDecimals?: number, now?: number): Promise<SpotPrice | null>;
|
|
224
|
+
/**
|
|
225
|
+
* Quote a buy: how much `token` `amountIn` of `quoteToken` acquires, at live
|
|
226
|
+
* liquidity. This is the simulate step — a QuoterV2 `eth_call`, no state
|
|
227
|
+
* change. Returns `null` when no route fills.
|
|
228
|
+
*/
|
|
229
|
+
quoteBuy(quoteToken: Address, token: Address, amountIn: bigint): Promise<SwapQuote | null>;
|
|
230
|
+
/** Quote a sell: proceeds in `quoteToken` from selling `amount` of `token`. */
|
|
231
|
+
quoteSell(token: Address, quoteToken: Address, amount: bigint): Promise<SwapQuote | null>;
|
|
232
|
+
/** Chainlink price for a Stock Token (already multiplier-adjusted). */
|
|
233
|
+
stockChainlinkPrice(symbol: string): Promise<StockQuote | null>;
|
|
234
|
+
/**
|
|
235
|
+
* DEX price of a Stock Token in USD from the USDG pool — the number the
|
|
236
|
+
* premium-watch strategy compares against the Chainlink oracle. Probes with a
|
|
237
|
+
* flat 10 USDG order (small enough to keep impact low across the priced
|
|
238
|
+
* registry) and backs out the implied per-token price from the fill.
|
|
239
|
+
*/
|
|
240
|
+
stockDexPrice(tokenAddress: Address, referenceUsd: number): Promise<number | null>;
|
|
241
|
+
/** Priced Stock Tokens from the registry (those with a Chainlink feed). */
|
|
242
|
+
pricedStockTokens(): hoodchain.StockToken[];
|
|
243
|
+
/** Router/quoter/weth/usdg set for the active network. */
|
|
244
|
+
addresses(): {
|
|
245
|
+
quoterV2: Address;
|
|
246
|
+
router: Address;
|
|
247
|
+
routerKind: "swapRouter02" | "swapRouter";
|
|
248
|
+
weth: Address;
|
|
249
|
+
usdg: Address;
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Global kill switch. Once tripped it is irreversible for the process lifetime:
|
|
255
|
+
* every agent checks `isKilled()` before each decide/execute step and refuses to
|
|
256
|
+
* open new positions. Three independent triggers, per the spec:
|
|
257
|
+
*
|
|
258
|
+
* 1. SIGINT / SIGTERM (Ctrl-C, container stop)
|
|
259
|
+
* 2. the presence of a `KILL` file on disk (drop-a-file panic button)
|
|
260
|
+
* 3. an HTTP `POST /kill` on the dashboard server (wired in server/)
|
|
261
|
+
*
|
|
262
|
+
* The switch never sells or unwinds on its own — it HALTS new risk. Unwinding is
|
|
263
|
+
* an explicit operator action, because a forced market-sell into thin liquidity
|
|
264
|
+
* during whatever caused the panic is usually worse than holding.
|
|
265
|
+
*/
|
|
266
|
+
declare class KillSwitch {
|
|
267
|
+
private readonly killFilePath;
|
|
268
|
+
private killed;
|
|
269
|
+
private reason;
|
|
270
|
+
private readonly listeners;
|
|
271
|
+
private fileTimer;
|
|
272
|
+
constructor(killFilePath: string);
|
|
273
|
+
/** Install SIGINT/SIGTERM handlers and begin polling the kill file. */
|
|
274
|
+
arm(): void;
|
|
275
|
+
/** Trip the switch. Idempotent. */
|
|
276
|
+
trip(reason: string): void;
|
|
277
|
+
isKilled(): boolean;
|
|
278
|
+
killReason(): string | null;
|
|
279
|
+
/** Subscribe to the trip event. Returns an unsubscribe fn. */
|
|
280
|
+
onKill(listener: (reason: string) => void): () => void;
|
|
281
|
+
/** Stop the file poller (used in tests/teardown). */
|
|
282
|
+
dispose(): void;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Which token a strategy denominates its orders in. */
|
|
286
|
+
type QuoteKind = 'usdg' | 'weth';
|
|
287
|
+
/** Documentation a strategy must publish about itself — surfaced in docs + dashboard. */
|
|
288
|
+
interface StrategyMeta {
|
|
289
|
+
/** One-paragraph honest statement of why this could have an edge. */
|
|
290
|
+
edge: string;
|
|
291
|
+
/** The ways this strategy loses money — named plainly, no hand-waving. */
|
|
292
|
+
failureModes: string[];
|
|
293
|
+
/** Tunable parameters and their live values. */
|
|
294
|
+
params: Record<string, unknown>;
|
|
295
|
+
}
|
|
296
|
+
/** Context handed to a strategy on each decision tick. */
|
|
297
|
+
interface StrategyTickContext {
|
|
298
|
+
market: Market;
|
|
299
|
+
/** This agent's currently open positions. */
|
|
300
|
+
positions: Position[];
|
|
301
|
+
/** Injected clock (ms) — deterministic in tests. */
|
|
302
|
+
now: number;
|
|
303
|
+
/** Resolved quote token address for this strategy's {@link QuoteKind}. */
|
|
304
|
+
quoteToken: Address;
|
|
305
|
+
quoteSymbol: string;
|
|
306
|
+
quoteDecimals: number;
|
|
307
|
+
/** Structured log line into the decision journal (kind = observe). */
|
|
308
|
+
log: (message: string, meta?: Record<string, unknown>) => void;
|
|
309
|
+
}
|
|
310
|
+
/** Context handed once at strategy startup (for stream subscriptions). */
|
|
311
|
+
interface StrategyStartContext {
|
|
312
|
+
market: Market;
|
|
313
|
+
log: (message: string, meta?: Record<string, unknown>) => void;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* A trading strategy: pure observation → decision. It never touches wallets,
|
|
317
|
+
* risk caps, or the journal — the {@link Agent} owns simulate/execute/journal
|
|
318
|
+
* and enforces every risk rail around whatever the strategy proposes. A
|
|
319
|
+
* strategy that proposes a wild order simply gets refused; it cannot bypass the
|
|
320
|
+
* risk engine.
|
|
321
|
+
*/
|
|
322
|
+
interface Strategy {
|
|
323
|
+
readonly id: string;
|
|
324
|
+
readonly title: string;
|
|
325
|
+
readonly quote: QuoteKind;
|
|
326
|
+
readonly meta: StrategyMeta;
|
|
327
|
+
/** Optional: wire real-time subscriptions (e.g. launch stream). */
|
|
328
|
+
start?(ctx: StrategyStartContext): Promise<void> | void;
|
|
329
|
+
/** Optional teardown for subscriptions. */
|
|
330
|
+
stop?(): void;
|
|
331
|
+
/** Produce intents + alerts for this tick. */
|
|
332
|
+
tick(ctx: StrategyTickContext): Promise<Decision>;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Everything an {@link Agent} is constructed with. */
|
|
336
|
+
interface AgentOptions {
|
|
337
|
+
id: string;
|
|
338
|
+
strategy: Strategy;
|
|
339
|
+
market: Market;
|
|
340
|
+
limits: RiskLimits;
|
|
341
|
+
journal: Journal;
|
|
342
|
+
kill: KillSwitch;
|
|
343
|
+
mode: Mode;
|
|
344
|
+
/** Account for live execution; null in paper mode. */
|
|
345
|
+
account: Account | null;
|
|
346
|
+
fleetMaxDailySpendUsdg: number;
|
|
347
|
+
/** Fleet-wide spend accessor + reporter, so the agent respects the global cap. */
|
|
348
|
+
fleetSpentTodayUsd: () => number;
|
|
349
|
+
reportFleetSpend: (usd: number) => void;
|
|
350
|
+
/** Milliseconds between decision ticks. */
|
|
351
|
+
tickIntervalMs: number;
|
|
352
|
+
/** Injected clock, for tests. Defaults to `Date.now`. */
|
|
353
|
+
clock?: () => number;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* An autonomous trading agent = strategy + wallet + risk budget + journal.
|
|
357
|
+
*
|
|
358
|
+
* Each tick runs the full pipeline: observe (the strategy reads the {@link
|
|
359
|
+
* Market}) → decide (the strategy returns intents) → simulate (a real QuoterV2
|
|
360
|
+
* `eth_call`) → risk-check (the {@link RiskEngine}, fail-closed) → execute
|
|
361
|
+
* (paper: record the simulated fill; live: sign the swap) → journal (every
|
|
362
|
+
* decision, refusal, trade, and equity mark). The strategy proposes; the agent
|
|
363
|
+
* disposes, and never lets an intent skip the risk gate.
|
|
364
|
+
*/
|
|
365
|
+
declare class Agent {
|
|
366
|
+
readonly id: string;
|
|
367
|
+
readonly strategy: Strategy;
|
|
368
|
+
readonly mode: Mode;
|
|
369
|
+
private readonly market;
|
|
370
|
+
private readonly risk;
|
|
371
|
+
private readonly journal;
|
|
372
|
+
private readonly kill;
|
|
373
|
+
private readonly account;
|
|
374
|
+
private readonly opts;
|
|
375
|
+
private readonly clock;
|
|
376
|
+
private readonly positions;
|
|
377
|
+
private lastTradeAt;
|
|
378
|
+
private realizedUsd;
|
|
379
|
+
private spentTodayUsd;
|
|
380
|
+
private spentDay;
|
|
381
|
+
private ticks;
|
|
382
|
+
private trades;
|
|
383
|
+
private refusals;
|
|
384
|
+
private lastTickAt;
|
|
385
|
+
private lastError;
|
|
386
|
+
private timer;
|
|
387
|
+
private running;
|
|
388
|
+
private ticking;
|
|
389
|
+
constructor(opts: AgentOptions);
|
|
390
|
+
/** Begin the tick loop and wire the strategy's stream subscriptions. */
|
|
391
|
+
start(): Promise<void>;
|
|
392
|
+
/** Stop the loop and the strategy's subscriptions. */
|
|
393
|
+
stop(): void;
|
|
394
|
+
private scheduleTick;
|
|
395
|
+
/** Run one full pipeline pass. Safe to call directly (used by tests). */
|
|
396
|
+
tick(): Promise<void>;
|
|
397
|
+
private decide;
|
|
398
|
+
private quoteInfo;
|
|
399
|
+
private processIntent;
|
|
400
|
+
private executeLive;
|
|
401
|
+
private applyFill;
|
|
402
|
+
/** Mark every open position to its live exit value (a real sell-side quote). */
|
|
403
|
+
private markPositions;
|
|
404
|
+
private openValueUsd;
|
|
405
|
+
private recordEquity;
|
|
406
|
+
private rolloverDay;
|
|
407
|
+
/** Live status snapshot for the dashboard API. */
|
|
408
|
+
status(): AgentStatus;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Definition of one agent within a fleet. */
|
|
412
|
+
interface AgentSpec {
|
|
413
|
+
id: string;
|
|
414
|
+
strategy: Strategy;
|
|
415
|
+
/** Overrides merged over the fleet default limits. */
|
|
416
|
+
limits?: Partial<RiskLimits>;
|
|
417
|
+
tickIntervalMs?: number;
|
|
418
|
+
}
|
|
419
|
+
/** Aggregate fleet numbers for the dashboard header. */
|
|
420
|
+
interface FleetSummary {
|
|
421
|
+
network: string;
|
|
422
|
+
mode: Mode;
|
|
423
|
+
killed: boolean;
|
|
424
|
+
killReason: string | null;
|
|
425
|
+
fleetSpentTodayUsd: number;
|
|
426
|
+
fleetMaxDailySpendUsdg: number;
|
|
427
|
+
realizedUsd: number;
|
|
428
|
+
openValueUsd: number;
|
|
429
|
+
equityUsd: number;
|
|
430
|
+
agents: number;
|
|
431
|
+
startedAt: number;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* The fleet: owns the shared market client, journal, and kill switch, then runs
|
|
435
|
+
* a set of agents against them and tracks the global daily-spend budget. It is
|
|
436
|
+
* the process-level object the dashboard server reads and the kill switch acts
|
|
437
|
+
* on.
|
|
438
|
+
*/
|
|
439
|
+
declare class Fleet {
|
|
440
|
+
readonly config: FleetConfig;
|
|
441
|
+
readonly journal: Journal;
|
|
442
|
+
readonly kill: KillSwitch;
|
|
443
|
+
readonly market: Market;
|
|
444
|
+
private readonly account;
|
|
445
|
+
private readonly agents;
|
|
446
|
+
private fleetSpentTodayUsd;
|
|
447
|
+
private spentDay;
|
|
448
|
+
private startedAt;
|
|
449
|
+
constructor(config: FleetConfig);
|
|
450
|
+
/** Build agents from specs. */
|
|
451
|
+
addAgents(specs: AgentSpec[]): void;
|
|
452
|
+
private currentFleetSpend;
|
|
453
|
+
private recordFleetSpend;
|
|
454
|
+
/** Arm the kill switch and start every agent's loop. */
|
|
455
|
+
start(): Promise<void>;
|
|
456
|
+
/** Stop all agents (kill switch stays tripped if it was). */
|
|
457
|
+
stop(): void;
|
|
458
|
+
/** Run exactly one tick per agent, in parallel, without arming the interval scheduler. Used by E2E tests. */
|
|
459
|
+
tickAllOnce(): Promise<void>;
|
|
460
|
+
/** Trip the kill switch — halts new orders across the fleet. */
|
|
461
|
+
tripKill(reason: string): void;
|
|
462
|
+
agentStatuses(): AgentStatus[];
|
|
463
|
+
summary(): FleetSummary;
|
|
464
|
+
close(): void;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Everything the risk engine needs to rule on a single intent. */
|
|
468
|
+
interface RiskContext {
|
|
469
|
+
/** `buy` grows exposure and hits spend/position caps; `sell` reduces it and is exempt from those. */
|
|
470
|
+
side: 'buy' | 'sell';
|
|
471
|
+
/** USD notional of this order (agent computes it from live prices; USDG≈USD, WETH×ethUsd). */
|
|
472
|
+
notionalUsd: number;
|
|
473
|
+
/** USD value the token position would reach AFTER a buy fills. */
|
|
474
|
+
positionUsdAfter: number;
|
|
475
|
+
/** USD this agent has already spent this UTC day. */
|
|
476
|
+
spentTodayUsd: number;
|
|
477
|
+
/** USD the whole fleet has spent this UTC day. */
|
|
478
|
+
fleetSpentTodayUsd: number;
|
|
479
|
+
/** ms epoch of this agent's last executed trade, or null. */
|
|
480
|
+
lastTradeAt: number | null;
|
|
481
|
+
/** Slippage bound (bps) the order would execute with. */
|
|
482
|
+
slippageBps: number;
|
|
483
|
+
/** Whether the global kill switch is tripped. */
|
|
484
|
+
killed: boolean;
|
|
485
|
+
/** Current time (ms) — injected for deterministic tests. */
|
|
486
|
+
now: number;
|
|
487
|
+
/** Fleet-wide daily spend ceiling (USD). */
|
|
488
|
+
fleetMaxDailySpendUsdg: number;
|
|
489
|
+
}
|
|
490
|
+
/** Result of a risk check. */
|
|
491
|
+
interface RiskVerdict {
|
|
492
|
+
ok: boolean;
|
|
493
|
+
reason?: RefusalReason;
|
|
494
|
+
/** Human explanation, safe to journal and surface on the dashboard. */
|
|
495
|
+
detail: string;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* The risk engine. It is the last gate before any order — paper or live —
|
|
499
|
+
* reaches execution, and it fails CLOSED: any check it cannot satisfy refuses
|
|
500
|
+
* the trade rather than letting it through. Refusals are ordered from
|
|
501
|
+
* cheapest/most-fatal (kill switch) to most-specific (caps) so the journaled
|
|
502
|
+
* reason is the most meaningful one.
|
|
503
|
+
*
|
|
504
|
+
* Sells are intentionally exempt from spend and position caps: those caps exist
|
|
505
|
+
* to bound how much risk you take ON, and refusing a de-risking sell because of
|
|
506
|
+
* a spend cap would trap an agent in a losing position. Sells still honor the
|
|
507
|
+
* kill switch, cooldown, and slippage bound.
|
|
508
|
+
*/
|
|
509
|
+
declare class RiskEngine {
|
|
510
|
+
private readonly limits;
|
|
511
|
+
constructor(limits: RiskLimits);
|
|
512
|
+
get riskLimits(): RiskLimits;
|
|
513
|
+
check(ctx: RiskContext): RiskVerdict;
|
|
514
|
+
}
|
|
515
|
+
/** UTC-midnight ms boundary for `now` — the daily-cap accounting window. */
|
|
516
|
+
declare function utcDayStart(now: number): number;
|
|
517
|
+
|
|
518
|
+
/** Tunables for {@link LaunchSniper}. */
|
|
519
|
+
interface LaunchSniperParams {
|
|
520
|
+
/** WETH spent per entry. */
|
|
521
|
+
entryWeth: number;
|
|
522
|
+
/** Take profit as a fraction (0.5 = +50%). */
|
|
523
|
+
takeProfitPct: number;
|
|
524
|
+
/** Stop loss as a fraction (0.3 = -30%). */
|
|
525
|
+
stopLossPct: number;
|
|
526
|
+
/** Force-exit a position after this many seconds regardless of PnL. */
|
|
527
|
+
maxHoldSeconds: number;
|
|
528
|
+
/** Reject a launch if the deployer still holds more than this fraction of supply. */
|
|
529
|
+
maxDeployerPct: number;
|
|
530
|
+
/** Reject if an immediate buy→sell round trip would lose more than this fraction (honeypot/thin-pool guard). */
|
|
531
|
+
maxRoundTripLossPct: number;
|
|
532
|
+
/** Only consider launches at most this many seconds old when first seen. */
|
|
533
|
+
maxLaunchAgeSeconds: number;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* launch-sniper — enter brand-new launchpad coins that clear objective safety
|
|
537
|
+
* filters, then exit on take-profit, stop, or a hard time limit.
|
|
538
|
+
*
|
|
539
|
+
* EDGE HYPOTHESIS: the first minutes after a NOXA instant-listing are the most
|
|
540
|
+
* information-rich and most volatile window a memecoin ever has. A disciplined
|
|
541
|
+
* buyer who (a) only touches tokens that are actually round-trippable (not
|
|
542
|
+
* honeypots) and whose deployer is not sitting on the supply, and (b) exits
|
|
543
|
+
* mechanically instead of falling in love, harvests a slice of that opening
|
|
544
|
+
* volatility. The edge is speed + discipline, not prediction.
|
|
545
|
+
*
|
|
546
|
+
* FAILURE MODES: most new launches go to zero — the stop loss WILL fire often
|
|
547
|
+
* and the strategy is negative-carry unless the winners pay for the losers.
|
|
548
|
+
* Honeypots evolve (sell-tax toggled AFTER you buy); the round-trip check only
|
|
549
|
+
* sees the state at entry. Odyssey tokens on the bonding curve have no Uniswap
|
|
550
|
+
* pool yet, so they are skipped until graduation — this strategy is really a
|
|
551
|
+
* NOXA/graduated-pool sniper. Paper fills assume the QuoterV2 mid; a real
|
|
552
|
+
* sniper competes with faster bots and eats worse fills.
|
|
553
|
+
*/
|
|
554
|
+
declare class LaunchSniper implements Strategy {
|
|
555
|
+
readonly id = "launch-sniper";
|
|
556
|
+
readonly title = "Launch Sniper";
|
|
557
|
+
readonly quote: "weth";
|
|
558
|
+
private readonly p;
|
|
559
|
+
private readonly queue;
|
|
560
|
+
private readonly seen;
|
|
561
|
+
private unwatch;
|
|
562
|
+
constructor(params?: Partial<LaunchSniperParams>);
|
|
563
|
+
get meta(): StrategyMeta;
|
|
564
|
+
start(ctx: StrategyStartContext): void;
|
|
565
|
+
stop(): void;
|
|
566
|
+
tick(ctx: StrategyTickContext): Promise<Decision>;
|
|
567
|
+
private evaluate;
|
|
568
|
+
private deployerConcentration;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** Tunables for {@link Momentum}. */
|
|
572
|
+
interface MomentumParams {
|
|
573
|
+
/** USDG spent per entry. */
|
|
574
|
+
entryUsdg: number;
|
|
575
|
+
/** Minimum price gain over the lookback window to count as a breakout. */
|
|
576
|
+
breakoutPct: number;
|
|
577
|
+
/** How many price samples (ticks) form the lookback window. */
|
|
578
|
+
lookbackSamples: number;
|
|
579
|
+
/** Trailing stop: exit if price falls this fraction off its post-entry peak. */
|
|
580
|
+
trailingStopPct: number;
|
|
581
|
+
/** Hard time exit regardless of PnL. */
|
|
582
|
+
maxHoldSeconds: number;
|
|
583
|
+
/** How many graduated tokens to track price history for (most-recently-graduated first). */
|
|
584
|
+
maxTracked: number;
|
|
585
|
+
/** Blocks to look back for discovering graduated tokens. */
|
|
586
|
+
discoveryLookbackBlocks: bigint;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* momentum — volume+price breakout entries on already-graduated (liquid)
|
|
590
|
+
* tokens, trailing-stop exits.
|
|
591
|
+
*
|
|
592
|
+
* EDGE HYPOTHESIS: a token that has already graduated to a locked Uniswap v3
|
|
593
|
+
* pool has survived the highest-mortality phase (the bonding curve / first
|
|
594
|
+
* minutes). A subsequent breakout — price up sharply over a short lookback,
|
|
595
|
+
* confirmed by the pool actually being tradable at size — reflects real
|
|
596
|
+
* incoming demand rather than launch-day noise, and trend-following that with
|
|
597
|
+
* a trailing stop captures the middle of the move while giving back only the
|
|
598
|
+
* tail.
|
|
599
|
+
*
|
|
600
|
+
* FAILURE MODES: breakouts on illiquid tokens are trivially fakeable by a
|
|
601
|
+
* single wallet round-tripping the pool; the strategy only looks at price, not
|
|
602
|
+
* depth, so it can chase a move that reverses the instant it stops buying.
|
|
603
|
+
* Trailing stops whipsaw in choppy markets — expect a string of small losses
|
|
604
|
+
* between real trends. Discovery only looks at NOXA (instant-listed, so
|
|
605
|
+
* "graduated" from day one) and Odyssey `PoolMigrated` history within the
|
|
606
|
+
* lookback window; a real breakout minutes after that window closes is missed
|
|
607
|
+
* until the next discovery pass.
|
|
608
|
+
*/
|
|
609
|
+
declare class Momentum implements Strategy {
|
|
610
|
+
readonly id = "momentum";
|
|
611
|
+
readonly title = "Momentum";
|
|
612
|
+
readonly quote: "usdg";
|
|
613
|
+
private readonly p;
|
|
614
|
+
private readonly history;
|
|
615
|
+
private readonly peakSinceEntry;
|
|
616
|
+
private lastDiscoveryAt;
|
|
617
|
+
constructor(params?: Partial<MomentumParams>);
|
|
618
|
+
get meta(): StrategyMeta;
|
|
619
|
+
tick(ctx: StrategyTickContext): Promise<Decision>;
|
|
620
|
+
private exitIntent;
|
|
621
|
+
private discover;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** Tunables for {@link PremiumWatch}. */
|
|
625
|
+
interface PremiumWatchParams {
|
|
626
|
+
/** Symbols to watch. Defaults to a handful of the most liquid priced Stock Tokens. */
|
|
627
|
+
symbols: string[];
|
|
628
|
+
/** Premium/discount (bps, |dex - oracle| / oracle) that triggers an alert. */
|
|
629
|
+
alertThresholdBps: number;
|
|
630
|
+
/** Premium/discount (bps) that triggers a convergence TRADE — only reachable in trade-eligible config. */
|
|
631
|
+
tradeThresholdBps: number;
|
|
632
|
+
/** USDG spent per convergence trade, when trading is enabled. */
|
|
633
|
+
tradeSizeUsdg: number;
|
|
634
|
+
/** Exit once the premium reverts to within this many bps of fair. */
|
|
635
|
+
exitThresholdBps: number;
|
|
636
|
+
/** Hard time exit for a convergence position regardless of premium. */
|
|
637
|
+
maxHoldSeconds: number;
|
|
638
|
+
/**
|
|
639
|
+
* Convergence TRADING is opt-in and requires BOTH this flag AND
|
|
640
|
+
* `market.client.acknowledgeStockTokenEligibility === true` (the operator's
|
|
641
|
+
* non-US-person affirmation, per `_shared.md`). Missing either keeps the
|
|
642
|
+
* strategy strictly alerts-only, which is the default and the recommended mode.
|
|
643
|
+
*/
|
|
644
|
+
enableTrading: boolean;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* premium-watch — tracks the spread between each Stock Token's Chainlink
|
|
648
|
+
* oracle price and its live Uniswap v3 DEX price, and alerts on premium/discount.
|
|
649
|
+
* Trades the convergence ONLY when explicitly enabled AND the operator has
|
|
650
|
+
* affirmed Stock Token eligibility — otherwise it is alerts-only, which is the
|
|
651
|
+
* default and the recommended mode for anyone who hasn't cleared the legal gate
|
|
652
|
+
* in `_shared.md`.
|
|
653
|
+
*
|
|
654
|
+
* EDGE HYPOTHESIS: Stock Token pools are much thinner than the underlying
|
|
655
|
+
* equity market, so DEX price can drift from the Chainlink oracle (itself fed
|
|
656
|
+
* by the real market) when flow is one-sided. If the drift is a liquidity
|
|
657
|
+
* artifact rather than new information, buying the discount / shorting the
|
|
658
|
+
* premium (here: buying the discount and later selling back at fair, since the
|
|
659
|
+
* SDK has no short primitive) captures the reversion as the pool re-equilibrates
|
|
660
|
+
* via arbitrage or the next informed trade.
|
|
661
|
+
*
|
|
662
|
+
* FAILURE MODES: a premium can be RIGHT — the DEX price can be reacting to
|
|
663
|
+
* information the Chainlink heartbeat (up to 24h) hasn't caught up to yet, in
|
|
664
|
+
* which case "fading" it loses money on purpose. Stock Token pools are thin;
|
|
665
|
+
* the $10 probe used to read `stockDexPrice` may not reflect the price your
|
|
666
|
+
* actual trade size gets, and the trade itself moves the price you're trying to
|
|
667
|
+
* arbitrage. Only long convergence is possible (no shorting), so premiums
|
|
668
|
+
* (DEX rich) are alert-only even when trading is enabled — asymmetric coverage
|
|
669
|
+
* by construction. Chainlink staleness (weekends, feed outages) can make a
|
|
670
|
+
* quote look like a discount when it is just old.
|
|
671
|
+
*/
|
|
672
|
+
declare class PremiumWatch implements Strategy {
|
|
673
|
+
readonly id = "premium-watch";
|
|
674
|
+
readonly title = "Premium Watch";
|
|
675
|
+
readonly quote: "usdg";
|
|
676
|
+
private readonly p;
|
|
677
|
+
constructor(params?: Partial<PremiumWatchParams>);
|
|
678
|
+
get meta(): StrategyMeta;
|
|
679
|
+
tick(ctx: StrategyTickContext): Promise<Decision>;
|
|
680
|
+
private readSpread;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* The fleet dashboard: a tiny read API over {@link Fleet} plus a `POST /kill`
|
|
685
|
+
* panic button, serving the static `dashboard/` UI. No framework — this is a
|
|
686
|
+
* small, auditable surface that is also the E2E harness talks to.
|
|
687
|
+
*
|
|
688
|
+
* `staticRoot` is resolved by the caller rather than guessed from this
|
|
689
|
+
* module's own location: tsup flattens `src/**/*.ts` into a single-level
|
|
690
|
+
* `dist/`, so a path relative to *this* file's directory would differ between
|
|
691
|
+
* `tsx` (unbundled, nested under `src/server/`) and the built bundle (flat).
|
|
692
|
+
* `main.ts` sits at a stable one-level depth in both trees, so it computes
|
|
693
|
+
* the path once and passes it in.
|
|
694
|
+
*/
|
|
695
|
+
declare function createDashboardServer(fleet: Fleet, staticRoot: string): Server;
|
|
696
|
+
|
|
697
|
+
export { Agent, type AgentOptions, type AgentSpec, type AgentStatus, type Alert, type Decision, type DecisionRecord, type EquityPoint, Fleet, type FleetConfig, type FleetSummary, type Intent, Journal, KillSwitch, LaunchSniper, type LaunchSniperParams, Market, type Mode, Momentum, type MomentumParams, type Position, PremiumWatch, type PremiumWatchParams, type QuoteKind, type RefusalReason, type RiskContext, RiskEngine, type RiskLimits, type RiskVerdict, type SpotPrice, type Strategy, type StrategyMeta, type StrategyStartContext, type StrategyTickContext, type TradeRecord, createDashboardServer, loadFleetConfig, utcDayStart };
|