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