lpsignal 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/dist/stream.js ADDED
@@ -0,0 +1,310 @@
1
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import WebSocket from 'ws';
4
+ import { LPSignalError } from './client.js';
5
+ export class MemoryLastIdStore {
6
+ id = null;
7
+ async load() { return this.id; }
8
+ async save(lastId) { this.id = lastId; }
9
+ }
10
+ /** A small JSON file, replaced atomically (temp file + rename) so a crash never leaves half a file. */
11
+ export class FileLastIdStore {
12
+ path;
13
+ constructor(path) {
14
+ this.path = path;
15
+ }
16
+ async load() {
17
+ let text;
18
+ try {
19
+ text = await readFile(this.path, 'utf8');
20
+ }
21
+ catch (e) {
22
+ if (e.code === 'ENOENT')
23
+ return null;
24
+ throw e;
25
+ }
26
+ const id = JSON.parse(text).lastId;
27
+ if (typeof id !== 'string' || !/^\d+$/.test(id))
28
+ throw new Error(`${this.path}: no valid lastId`);
29
+ return id;
30
+ }
31
+ async save(lastId) {
32
+ await mkdir(dirname(this.path), { recursive: true });
33
+ const tmp = `${this.path}.tmp`;
34
+ await writeFile(tmp, JSON.stringify({ lastId }));
35
+ await rename(tmp, this.path);
36
+ }
37
+ }
38
+ /**
39
+ * Live signals in id order, with no gaps across disconnects, outages and restarts.
40
+ *
41
+ * The server replays the last 24 hours on `?since=<id>`. Before every connection this stream first fetches everything
42
+ * newer than its position over REST, repeating until a pass finds nothing new, so an outage of any length is filled
43
+ * and the socket opens seconds after the last fetch. Anything at or below the position is dropped, so within one
44
+ * running process each signal reaches the handler once; across a crash the last one may be offered again.
45
+ */
46
+ export class SignalStream {
47
+ opts;
48
+ store;
49
+ socketFactory;
50
+ lastId = null;
51
+ queue = Promise.resolve();
52
+ running = false;
53
+ socket = null;
54
+ loopDone = null;
55
+ wake = null;
56
+ starting = null;
57
+ stopping = null;
58
+ /** the first-run starting point, chosen once and kept until it is saved (a retry must not pick a newer one) */
59
+ anchor = null;
60
+ constructor(options) {
61
+ if (!options.client.apiKey)
62
+ throw new Error('SignalStream: the client needs an API key (the stream is for paid plans)');
63
+ if (options.since !== undefined && !/^\d+$/.test(options.since))
64
+ throw new Error('SignalStream: since must be a signal id');
65
+ this.opts = { pingIntervalMs: 30_000, openTimeoutMs: 15_000, minBackoffMs: 1_000, maxBackoffMs: 30_000, ...options };
66
+ this.store = options.store ?? new MemoryLastIdStore();
67
+ this.socketFactory = options.socketFactory
68
+ ?? ((url, headers) => new WebSocket(url, {
69
+ handshakeTimeout: this.opts.openTimeoutMs,
70
+ ...options.wsOptions,
71
+ headers: { ...options.wsOptions?.headers, ...headers },
72
+ }));
73
+ }
74
+ /** Id of the last signal handed to onSignal (null before the first connection). */
75
+ get position() { return this.lastId; }
76
+ /**
77
+ * Loads the saved position and starts in the background; call stop() to end. Calling it while running is a no-op;
78
+ * during a stop() it starts once the stop has finished. After a `fatal` event, call stop() before starting again.
79
+ */
80
+ start() {
81
+ if (this.stopping)
82
+ return this.stopping.then(() => this.start());
83
+ // claimed synchronously: concurrent calls share one start, and a stop() during load() cancels it
84
+ this.starting ??= (async () => {
85
+ this.running = true;
86
+ try {
87
+ const saved = await this.store.load();
88
+ if (!this.running)
89
+ return;
90
+ this.lastId = saved ?? this.opts.since ?? null;
91
+ this.loopDone = this.loop();
92
+ }
93
+ catch (e) {
94
+ this.running = false;
95
+ this.starting = null;
96
+ throw e;
97
+ }
98
+ })();
99
+ return this.starting;
100
+ }
101
+ /**
102
+ * Closes the socket and waits for the signal being handled (if any). The stream can be started again.
103
+ * Don't await it from inside onSignal: it would wait for itself.
104
+ */
105
+ stop() {
106
+ // every caller waits for the same, complete shutdown
107
+ this.stopping ??= (async () => {
108
+ this.running = false;
109
+ this.socket?.close(1000);
110
+ this.wake?.();
111
+ await this.starting?.catch(() => undefined);
112
+ await this.loopDone;
113
+ await this.queue;
114
+ this.starting = null;
115
+ this.loopDone = null;
116
+ })().finally(() => { this.stopping = null; });
117
+ return this.stopping;
118
+ }
119
+ emit(e) {
120
+ try {
121
+ this.opts.onEvent?.(e);
122
+ }
123
+ catch { /* a listener must not break the stream */ }
124
+ }
125
+ fatal(error) {
126
+ this.running = false;
127
+ this.emit({ type: 'fatal', error });
128
+ }
129
+ async loop() {
130
+ let backoff = this.opts.minBackoffMs;
131
+ while (this.running) {
132
+ let healthy = false;
133
+ try {
134
+ await this.catchUp();
135
+ if (this.running)
136
+ healthy = await this.connectOnce();
137
+ }
138
+ catch (e) {
139
+ const err = e instanceof Error ? e : new Error(String(e));
140
+ if (err instanceof LPSignalError && err.status === 401)
141
+ this.fatal(err);
142
+ else
143
+ this.emit({ type: 'error', error: err });
144
+ }
145
+ if (!this.running)
146
+ break;
147
+ backoff = healthy ? this.opts.minBackoffMs : Math.min(backoff * 2, this.opts.maxBackoffMs);
148
+ await new Promise((r) => { const t = setTimeout(r, backoff); this.wake = () => { clearTimeout(t); r(); }; });
149
+ this.wake = null;
150
+ }
151
+ }
152
+ /** Establish a position (first run) or fetch everything after it over REST. */
153
+ async catchUp() {
154
+ if (this.lastId === null) {
155
+ // nothing is consumed until the starting point is saved: a restart must resume from this same point
156
+ this.anchor ??= (await this.opts.client.signals({ limit: 1 })).signals[0]?.id ?? '0';
157
+ await this.store.save(this.anchor);
158
+ this.lastId = this.anchor;
159
+ this.emit({ type: 'anchored', lastId: this.lastId });
160
+ }
161
+ // repeat until a pass finds nothing: a long backlog can take longer than the server's 24h replay window, and
162
+ // whatever was created meanwhile must come from REST too, or the socket would skip it
163
+ let delivered = 0;
164
+ for (;;) {
165
+ const batch = await this.opts.client.signalsAfter(this.lastId);
166
+ if (!batch.length || !this.running)
167
+ break;
168
+ for (const s of batch) {
169
+ if (!this.running)
170
+ return;
171
+ if (await this.deliver(s, 'rest'))
172
+ delivered++;
173
+ }
174
+ }
175
+ if (delivered)
176
+ this.emit({ type: 'caught_up', delivered });
177
+ }
178
+ /** One connection's life. Resolves when it closes; true when it went live. */
179
+ connectOnce() {
180
+ return new Promise((resolve) => {
181
+ const url = `${this.opts.client.streamUrl}?since=${this.lastId}`;
182
+ this.emit({ type: 'connecting', url });
183
+ let ws;
184
+ try {
185
+ ws = this.socketFactory(url, { authorization: `Bearer ${this.opts.client.apiKey}` });
186
+ }
187
+ catch (e) {
188
+ this.emit({ type: 'error', error: e });
189
+ resolve(false);
190
+ return;
191
+ }
192
+ this.socket = ws;
193
+ const conn = { failed: false, live: false, opened: false, done: false };
194
+ let alive = true;
195
+ let pinger = null;
196
+ const finish = () => {
197
+ if (conn.done)
198
+ return;
199
+ conn.done = true;
200
+ clearTimeout(opener);
201
+ if (pinger)
202
+ clearInterval(pinger);
203
+ if (this.socket === ws)
204
+ this.socket = null;
205
+ // let this connection's queued frames finish before the next connection starts
206
+ void this.queue.then(() => resolve(conn.live && !conn.failed));
207
+ };
208
+ // a peer that accepts TCP but never answers the upgrade must not stall the stream
209
+ const opener = setTimeout(() => {
210
+ if (conn.opened)
211
+ return;
212
+ this.emit({ type: 'error', error: new Error(`stream did not open within ${this.opts.openTimeoutMs} ms`) });
213
+ try {
214
+ ws.terminate();
215
+ }
216
+ catch { /* already gone */ }
217
+ finish();
218
+ }, this.opts.openTimeoutMs);
219
+ ws.on('unexpected-response', (_req, res) => {
220
+ const code = res?.statusCode ?? 0;
221
+ if (code === 401)
222
+ this.fatal(new Error('stream refused (401): the API key is invalid or was replaced'));
223
+ else if (code === 402)
224
+ this.fatal(new Error('stream refused (402): the plan does not include the stream'));
225
+ else
226
+ this.emit({ type: 'error', error: new Error(`stream refused (${code})${code === 429 ? ': too many connections for this key' : ''}`) });
227
+ try {
228
+ ws.terminate();
229
+ }
230
+ catch { /* already gone */ }
231
+ });
232
+ ws.on('open', () => {
233
+ conn.opened = true;
234
+ clearTimeout(opener);
235
+ if (!this.running) {
236
+ ws.close(1000);
237
+ return;
238
+ }
239
+ this.emit({ type: 'connected' });
240
+ pinger = setInterval(() => {
241
+ // a half-open TCP connection is only found by asking
242
+ if (!alive) {
243
+ ws.terminate();
244
+ return;
245
+ }
246
+ alive = false;
247
+ try {
248
+ ws.ping();
249
+ }
250
+ catch { /* close follows */ }
251
+ }, this.opts.pingIntervalMs);
252
+ });
253
+ ws.on('pong', () => { alive = true; });
254
+ ws.on('message', (raw) => {
255
+ alive = true;
256
+ let frame;
257
+ try {
258
+ frame = JSON.parse(raw.toString());
259
+ }
260
+ catch {
261
+ return;
262
+ }
263
+ this.queue = this.queue
264
+ .then(async () => {
265
+ if (conn.failed)
266
+ return;
267
+ if (frame.type === 'signal' && frame.signal)
268
+ await this.deliver(frame.signal, frame.replay ? 'replay' : 'live');
269
+ else if (frame.type === 'ready') {
270
+ conn.live = true;
271
+ this.emit({ type: 'live' });
272
+ }
273
+ // replay_truncated / replay_failed: the server closes with 4409 next; the reconnect resumes from lastId
274
+ })
275
+ .catch((err) => {
276
+ // keep the position and reconnect: the signal is offered again
277
+ conn.failed = true;
278
+ this.emit({ type: 'error', error: err instanceof Error ? err : new Error(String(err)) });
279
+ try {
280
+ ws.terminate();
281
+ }
282
+ catch { /* already gone */ }
283
+ });
284
+ });
285
+ ws.on('error', (err) => this.emit({ type: 'error', error: err }));
286
+ ws.on('close', (code, reason) => {
287
+ if (conn.done)
288
+ return;
289
+ this.emit({ type: 'disconnected', code, reason: reason?.toString() ?? '' });
290
+ if (code === 4402)
291
+ this.fatal(new Error('stream closed (4402): the plan no longer includes the stream'));
292
+ finish();
293
+ });
294
+ });
295
+ }
296
+ /** true when the signal was new and handed to onSignal */
297
+ async deliver(s, source) {
298
+ // after stop(), nothing new reaches the handler (frames already queued are dropped, not delivered)
299
+ if (!this.running)
300
+ return false;
301
+ if (this.lastId !== null && BigInt(s.id) <= BigInt(this.lastId))
302
+ return false;
303
+ await this.opts.onSignal(s, { source });
304
+ // the in-memory position follows the saved one: a failed save offers the signal again
305
+ await this.store.save(s.id);
306
+ this.lastId = s.id;
307
+ return true;
308
+ }
309
+ }
310
+ //# sourceMappingURL=stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Response shapes of the LPSignal API (https://api.lpsignal.app/v1).
3
+ *
4
+ * Conventions:
5
+ * - APRs and ratios are fractions: 0.345 = 34.5%.
6
+ * - `ilApr` / `il7d` are the loss versus holding the two tokens, so they are ≤ 0, and `netApr = feeApr + ilApr`.
7
+ * - `fee` is in hundredths of a basis point: 500 = the 0.05% fee tier.
8
+ * - `rangeBp` is the range half-width in basis points of price: 500 = ±5%, 0 = full range.
9
+ * - Ids and 256-bit values are strings; timestamps are ISO 8601 strings in UTC.
10
+ */
11
+ /** Chains scanned today. The API may add more, so any string is accepted. */
12
+ export type Chain = 'ethereum' | 'bsc' | 'base' | 'arbitrum' | 'optimism' | 'polygon' | (string & {});
13
+ export type PairClass = 'stable' | 'correlated' | 'volatile';
14
+ export type WindowHours = 24 | 168 | 720;
15
+ export type Tier = 'free' | 'basic' | 'pro';
16
+ export type SignalKind = 'net_apr' | 'tvl_outflow' | 'depeg' | 'smart_lp';
17
+ export interface Health {
18
+ ok: boolean;
19
+ }
20
+ export interface ChainStatus {
21
+ chain: Chain;
22
+ /** last block scanned */
23
+ block: string;
24
+ blockTs: string;
25
+ /** the first block this deployment scanned: history before it is not available */
26
+ firstBlockTs: string;
27
+ /** seconds between now and the last block scanned; chains more than 2h behind fire no signals */
28
+ lagSec: number;
29
+ activePools: number;
30
+ }
31
+ export interface BestRange {
32
+ windowHours: WindowHours;
33
+ rangeBp: number;
34
+ tickLower: number;
35
+ tickUpper: number;
36
+ /** fees measured from on-chain fee growth (only exact metrics fire signals) */
37
+ exact: boolean;
38
+ feeApr: number;
39
+ ilApr: number;
40
+ netApr: number;
41
+ inRangeRatio: number;
42
+ /** Slipstream: gauge emissions if staked instead (fees forgone, not backtested); 0 = not applicable */
43
+ stakedEmissionApr: number;
44
+ asOf: string;
45
+ }
46
+ export interface RankedPool {
47
+ chain: Chain;
48
+ /** 20-byte pool address, or the 32-byte pool id for Uniswap v4 */
49
+ address: string;
50
+ dex: string;
51
+ /** "SYMBOL0/SYMBOL1" */
52
+ pair: string;
53
+ fee: number;
54
+ pairClass: PairClass;
55
+ /** v4 pools: liquidity within ±2% of price, since v4 has no per-pool balances */
56
+ tvlUsd: number;
57
+ tvlAt: string | null;
58
+ best: BestRange;
59
+ }
60
+ export interface PoolsPage {
61
+ pools: RankedPool[];
62
+ limit: number;
63
+ offset: number;
64
+ }
65
+ export interface Pool {
66
+ id: string;
67
+ chain: Chain;
68
+ address: string;
69
+ status: string;
70
+ reason: string;
71
+ dex: string;
72
+ token0: string;
73
+ token1: string;
74
+ symbol0: string;
75
+ symbol1: string;
76
+ decimals0: number;
77
+ decimals1: number;
78
+ fee: number;
79
+ tickSpacing: number;
80
+ lpShare0: number;
81
+ lpShare1: number;
82
+ pairClass: PairClass;
83
+ gauge: string;
84
+ tvlUsd: number;
85
+ tvlAt: string | null;
86
+ paramsAt: string | null;
87
+ createdAt: string;
88
+ updatedAt: string;
89
+ deletedAt: string | null;
90
+ }
91
+ export interface PoolMetric {
92
+ poolId: string;
93
+ windowHours: WindowHours;
94
+ rangeBp: number;
95
+ feeApr: number;
96
+ ilApr: number;
97
+ netApr: number;
98
+ inRangeRatio: number;
99
+ emissionApr: number;
100
+ tickLower: number;
101
+ tickUpper: number;
102
+ exact: boolean;
103
+ asOf: string;
104
+ createdAt: string;
105
+ updatedAt: string;
106
+ }
107
+ export interface PoolDetail {
108
+ pool: Pool;
109
+ metrics: PoolMetric[];
110
+ }
111
+ export interface PoolHour {
112
+ /** start of the hour */
113
+ hour: string;
114
+ swaps: number;
115
+ /** raw token units (not decimal-adjusted) */
116
+ volume0: number;
117
+ volume1: number;
118
+ /** fees earned per unit of in-range liquidity during the hour, raw token units */
119
+ fee0PerL: number;
120
+ fee1PerL: number;
121
+ openTick: number;
122
+ closeTick: number;
123
+ minTick: number;
124
+ maxTick: number;
125
+ /** uint128 liquidity, as a decimal string */
126
+ closeLiquidity: string;
127
+ /** fees come from on-chain fee growth rather than a Swap-log estimate */
128
+ feeExact: boolean;
129
+ }
130
+ export interface Backtest {
131
+ chain: Chain;
132
+ address: string;
133
+ rangeBp: number;
134
+ days: number;
135
+ asOf: string;
136
+ feeApr: number;
137
+ ilApr: number;
138
+ netApr: number;
139
+ inRangeRatio: number;
140
+ /** fees of hours that straddled a range edge: the true fee APR lies in [feeApr, feeApr + uncertainFeeApr] */
141
+ uncertainFeeApr: number;
142
+ startTick: number;
143
+ endTick: number;
144
+ tickLower: number;
145
+ tickUpper: number;
146
+ feesExact: boolean;
147
+ exact: boolean;
148
+ }
149
+ export interface SignalOutcome {
150
+ /** done = exact 7-day result; inexact = fees could not be attributed precisely (not a result) */
151
+ status: 'done' | 'inexact';
152
+ netApr: number | null;
153
+ feeApr: number | null;
154
+ ilApr: number | null;
155
+ evaluatedAt: string;
156
+ }
157
+ interface SignalBase {
158
+ /** increasing; resume streams and paging from it */
159
+ id: string;
160
+ firedAt: string;
161
+ chain: Chain;
162
+ dex: string;
163
+ pool: string;
164
+ pair: string;
165
+ fee: number;
166
+ pairClass: PairClass;
167
+ rangeBp: number;
168
+ entryTick: number;
169
+ tickLower: number;
170
+ tickUpper: number;
171
+ /** only opportunities are scored; null until 7 days have passed (and always null for other kinds) */
172
+ outcome: SignalOutcome | null;
173
+ }
174
+ /** An opportunity: the recommended position is `tickLower..tickUpper`. */
175
+ export interface NetAprSignal extends SignalBase {
176
+ kind: 'net_apr';
177
+ tvlUsd: number;
178
+ net24h: number;
179
+ net7d: number;
180
+ /** null until the pool has 30 days of history */
181
+ net30d: number | null;
182
+ fee7d: number;
183
+ il7d: number;
184
+ inRange7d: number;
185
+ stakedEmissionApr?: number;
186
+ }
187
+ /** Liquidity fell by `drop` within `windowHours`, valued at current prices. */
188
+ export interface TvlOutflowSignal extends SignalBase {
189
+ kind: 'tvl_outflow';
190
+ tvlBeforeUsd: number;
191
+ tvlNowUsd: number;
192
+ drop: number;
193
+ windowHours: number;
194
+ }
195
+ /** token0 trades `deviation` away from its 7-day median against token1. */
196
+ export interface DepegSignal extends SignalBase {
197
+ kind: 'depeg';
198
+ deviation: number;
199
+ severe: boolean;
200
+ medianTick: number;
201
+ tick: number;
202
+ }
203
+ /** A top-ranked or followed wallet opened a position (Pro). */
204
+ export interface SmartLpSignal extends SignalBase {
205
+ kind: 'smart_lp';
206
+ owner: string;
207
+ tokenId: string;
208
+ entryUsd: number;
209
+ top: boolean;
210
+ rank: number | null;
211
+ wallet30d: {
212
+ positions: number;
213
+ pnlUsd: number;
214
+ returnPct: number;
215
+ } | null;
216
+ }
217
+ export type Signal = NetAprSignal | TvlOutflowSignal | DepegSignal | SmartLpSignal;
218
+ export interface SignalsPage {
219
+ signals: Signal[];
220
+ /** pass as `before` for the next (older) page; null on the last page */
221
+ next: string | null;
222
+ }
223
+ export interface LeaderboardWallet {
224
+ rank: number;
225
+ /** masked (0x1234…abcd) unless the caller is on Pro */
226
+ owner: string;
227
+ positions: number;
228
+ wins: number;
229
+ capitalUsd: number;
230
+ /** LP result minus simply holding the deposited tokens */
231
+ pnlUsd: number;
232
+ returnPct: number;
233
+ chains: Chain[];
234
+ }
235
+ export interface Leaderboard {
236
+ windowDays: 30 | 90;
237
+ /** false = top 10 with masked addresses (not Pro) */
238
+ full: boolean;
239
+ wallets: LeaderboardWallet[];
240
+ }
241
+ interface PositionPool {
242
+ chain: Chain;
243
+ pool: string;
244
+ pair: string;
245
+ dex: string;
246
+ fee: number;
247
+ }
248
+ export interface OpenPosition extends PositionPool {
249
+ tokenId: string;
250
+ tickLower: number;
251
+ tickUpper: number;
252
+ openedAt: string | null;
253
+ staked: boolean;
254
+ entryUsd: number | null;
255
+ }
256
+ export interface ClosedPosition extends PositionPool {
257
+ tokenId: string;
258
+ lifecycle: number;
259
+ tickLower: number;
260
+ tickUpper: number;
261
+ openedAt: string | null;
262
+ closedAt: string;
263
+ /** false = funded by several holders, staked, or first seen mid-life: not counted in rankings */
264
+ scoreable: boolean;
265
+ capitalUsd: number | null;
266
+ pnlUsd: number | null;
267
+ }
268
+ export interface WalletPositions {
269
+ owner: string;
270
+ open: OpenPosition[];
271
+ closed: ClosedPosition[];
272
+ }
273
+ export interface Me {
274
+ id: string;
275
+ tier: Tier;
276
+ paid: boolean;
277
+ telegramLinked: boolean;
278
+ webhookUrl: string | null;
279
+ /** the wallet the account signs in with on lpsignal.app (null for API-key-only accounts) */
280
+ walletAddress: string | null;
281
+ hasApiKey: boolean;
282
+ }
283
+ /** Track record of opportunity signals with a known 7-day outcome, over the last `days`. */
284
+ export interface SignalStats {
285
+ days: number;
286
+ /** exact outcomes (the only ones in the numbers below) */
287
+ scored: number;
288
+ /** evaluated without exact fees: never counted as a result */
289
+ inexact: number;
290
+ positive: number;
291
+ medianRealizedNetApr: number | null;
292
+ medianSignalNetApr: number | null;
293
+ worst: {
294
+ id: string;
295
+ pair: string;
296
+ chain: Chain;
297
+ netApr: number;
298
+ } | null;
299
+ /** at-signal vs realized net APR of the most recent (up to 500) scored signals */
300
+ points: {
301
+ id: string;
302
+ signalNetApr: number;
303
+ realizedNetApr: number;
304
+ }[];
305
+ }
306
+ export interface WebhookRegistration {
307
+ url: string;
308
+ /** shown only here: store it to verify deliveries */
309
+ secret: string;
310
+ }
311
+ export interface TelegramLink {
312
+ code: string;
313
+ instruction: string;
314
+ }
315
+ export interface Follow {
316
+ owner: string;
317
+ since: string;
318
+ }
319
+ export interface BillingStatus {
320
+ tier: Tier;
321
+ manual: {
322
+ tier: string;
323
+ until: string | null;
324
+ };
325
+ stripe: {
326
+ tier: string | null;
327
+ until: string | null;
328
+ status: string | null;
329
+ invoiceUnpaidSince: string | null;
330
+ syncedAt: string | null;
331
+ };
332
+ onSale: {
333
+ basic: boolean;
334
+ pro: boolean;
335
+ };
336
+ }
337
+ /** The body of a webhook delivery. */
338
+ export interface WebhookEvent {
339
+ type: 'signal';
340
+ /** unique per delivery: deliveries are at-least-once, so skip ids already handled */
341
+ deliveryId: string;
342
+ signal: Signal;
343
+ }
344
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Response shapes of the LPSignal API (https://api.lpsignal.app/v1).
3
+ *
4
+ * Conventions:
5
+ * - APRs and ratios are fractions: 0.345 = 34.5%.
6
+ * - `ilApr` / `il7d` are the loss versus holding the two tokens, so they are ≤ 0, and `netApr = feeApr + ilApr`.
7
+ * - `fee` is in hundredths of a basis point: 500 = the 0.05% fee tier.
8
+ * - `rangeBp` is the range half-width in basis points of price: 500 = ±5%, 0 = full range.
9
+ * - Ids and 256-bit values are strings; timestamps are ISO 8601 strings in UTC.
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}