@exagent/agent 0.3.5 → 0.3.7
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/chunk-7UGLJO6W.js +6392 -0
- package/dist/chunk-EHAOPCTJ.js +6406 -0
- package/dist/chunk-FGMXTW5I.js +6540 -0
- package/dist/chunk-IVA2SCSN.js +6756 -0
- package/dist/chunk-JHXCSGPC.js +6352 -0
- package/dist/chunk-V6O4UXVN.js +6345 -0
- package/dist/chunk-ZRAOPQQW.js +6406 -0
- package/dist/cli.js +40 -98
- package/dist/index.d.ts +24 -2
- package/dist/index.js +1 -1
- package/package.json +17 -14
- package/.turbo/turbo-build.log +0 -17
- package/src/bridge/across.ts +0 -240
- package/src/bridge/bridge-manager.ts +0 -87
- package/src/bridge/index.ts +0 -9
- package/src/bridge/types.ts +0 -77
- package/src/chains.ts +0 -105
- package/src/cli.ts +0 -244
- package/src/config.ts +0 -499
- package/src/diagnostics.ts +0 -335
- package/src/index.ts +0 -98
- package/src/llm/anthropic.ts +0 -63
- package/src/llm/base.ts +0 -264
- package/src/llm/deepseek.ts +0 -48
- package/src/llm/google.ts +0 -63
- package/src/llm/groq.ts +0 -48
- package/src/llm/index.ts +0 -42
- package/src/llm/mistral.ts +0 -48
- package/src/llm/ollama.ts +0 -52
- package/src/llm/openai.ts +0 -51
- package/src/llm/together.ts +0 -48
- package/src/llm-providers.ts +0 -100
- package/src/logger.ts +0 -137
- package/src/paper/executor.ts +0 -201
- package/src/paper/index.ts +0 -1
- package/src/perp/client.ts +0 -200
- package/src/perp/index.ts +0 -12
- package/src/perp/msgpack.ts +0 -272
- package/src/perp/orders.ts +0 -234
- package/src/perp/positions.ts +0 -126
- package/src/perp/signer.ts +0 -277
- package/src/perp/types.ts +0 -192
- package/src/perp/websocket.ts +0 -274
- package/src/position-tracker.ts +0 -243
- package/src/prediction/client.ts +0 -281
- package/src/prediction/index.ts +0 -3
- package/src/prediction/order-manager.ts +0 -297
- package/src/prediction/types.ts +0 -151
- package/src/relay.ts +0 -254
- package/src/runtime.ts +0 -1755
- package/src/scrub-secrets.ts +0 -39
- package/src/setup.ts +0 -384
- package/src/signal.ts +0 -212
- package/src/spot/aerodrome.ts +0 -158
- package/src/spot/client.ts +0 -138
- package/src/spot/index.ts +0 -11
- package/src/spot/swap-manager.ts +0 -219
- package/src/spot/types.ts +0 -203
- package/src/spot/uniswap.ts +0 -150
- package/src/store.ts +0 -50
- package/src/strategy/index.ts +0 -2
- package/src/strategy/loader.ts +0 -191
- package/src/strategy/templates.ts +0 -125
- package/src/trading/index.ts +0 -2
- package/src/trading/market.ts +0 -120
- package/src/trading/risk.ts +0 -107
- package/src/ui.ts +0 -75
- package/test-bridge-arb-to-base.mjs +0 -223
- package/test-funded-check.mjs +0 -79
- package/test-funded-phase19.mjs +0 -933
- package/test-hl-deposit-recover.mjs +0 -281
- package/test-hl-withdraw.mjs +0 -372
- package/test-live-signing.mjs +0 -374
- package/test-phase7.mjs +0 -416
- package/test-recover-arb.mjs +0 -206
- package/test-spot-bridge.mjs +0 -248
- package/test-wallet-setup.mjs +0 -126
- package/tsconfig.json +0 -8
package/src/perp/websocket.ts
DELETED
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hyperliquid WebSocket Client
|
|
3
|
-
*
|
|
4
|
-
* Real-time fill and funding event subscription.
|
|
5
|
-
* Auto-reconnects with exponential backoff.
|
|
6
|
-
* Checkpoints last processed fill time for REST backfill on reconnect.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import WebSocket from 'ws';
|
|
10
|
-
import type { PerpConfig, PerpFill } from './types.js';
|
|
11
|
-
import { HyperliquidClient } from './client.js';
|
|
12
|
-
|
|
13
|
-
export type FillCallback = (fill: PerpFill) => void;
|
|
14
|
-
export type FundingCallback = (funding: FundingPayment) => void;
|
|
15
|
-
export type LiquidationCallback = (instrument: string, size: number) => void;
|
|
16
|
-
|
|
17
|
-
export interface FundingPayment {
|
|
18
|
-
time: number;
|
|
19
|
-
coin: string;
|
|
20
|
-
usdc: string;
|
|
21
|
-
szi: string;
|
|
22
|
-
fundingRate: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export class HyperliquidWebSocket {
|
|
26
|
-
private readonly wsUrl: string;
|
|
27
|
-
private readonly userAddress: string;
|
|
28
|
-
private readonly client: HyperliquidClient;
|
|
29
|
-
|
|
30
|
-
private ws: WebSocket | null = null;
|
|
31
|
-
private reconnectAttempts: number = 0;
|
|
32
|
-
private readonly maxReconnectAttempts: number = 20;
|
|
33
|
-
private readonly baseReconnectMs: number = 1_000;
|
|
34
|
-
private readonly maxReconnectMs: number = 60_000;
|
|
35
|
-
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
36
|
-
private pingTimer: ReturnType<typeof setInterval> | null = null;
|
|
37
|
-
private isConnecting: boolean = false;
|
|
38
|
-
private shouldReconnect: boolean = true;
|
|
39
|
-
|
|
40
|
-
private lastProcessedFillTime: number = 0;
|
|
41
|
-
|
|
42
|
-
private onFill: FillCallback | null = null;
|
|
43
|
-
private onFunding: FundingCallback | null = null;
|
|
44
|
-
private onLiquidation: LiquidationCallback | null = null;
|
|
45
|
-
|
|
46
|
-
constructor(config: PerpConfig, userAddress: string, client: HyperliquidClient) {
|
|
47
|
-
this.wsUrl = config.wsUrl;
|
|
48
|
-
this.userAddress = userAddress;
|
|
49
|
-
this.client = client;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ── CONNECTION ─────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
async connect(): Promise<void> {
|
|
55
|
-
if (this.ws?.readyState === WebSocket.OPEN || this.isConnecting) return;
|
|
56
|
-
|
|
57
|
-
this.isConnecting = true;
|
|
58
|
-
this.shouldReconnect = true;
|
|
59
|
-
|
|
60
|
-
return new Promise<void>((resolve, reject) => {
|
|
61
|
-
try {
|
|
62
|
-
this.ws = new WebSocket(this.wsUrl);
|
|
63
|
-
|
|
64
|
-
this.ws.on('open', () => {
|
|
65
|
-
this.isConnecting = false;
|
|
66
|
-
this.reconnectAttempts = 0;
|
|
67
|
-
console.log('[perp-ws] Connected');
|
|
68
|
-
|
|
69
|
-
this.subscribe({
|
|
70
|
-
type: 'subscribe',
|
|
71
|
-
subscription: { type: 'userFills', user: this.userAddress },
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
this.subscribe({
|
|
75
|
-
type: 'subscribe',
|
|
76
|
-
subscription: { type: 'userFundings', user: this.userAddress },
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
this.startPing();
|
|
80
|
-
|
|
81
|
-
this.backfillMissedFills().catch((err) => {
|
|
82
|
-
console.warn('[perp-ws] Backfill failed:', err instanceof Error ? err.message : err);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
resolve();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
this.ws.on('message', (data: WebSocket.Data) => {
|
|
89
|
-
this.handleMessage(data);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
this.ws.on('close', (code: number, reason: Buffer) => {
|
|
93
|
-
this.isConnecting = false;
|
|
94
|
-
console.log(`[perp-ws] Closed: ${code} ${reason.toString()}`);
|
|
95
|
-
this.stopPing();
|
|
96
|
-
this.scheduleReconnect();
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
this.ws.on('error', (error: Error) => {
|
|
100
|
-
this.isConnecting = false;
|
|
101
|
-
console.error('[perp-ws] Error:', error.message);
|
|
102
|
-
if (this.reconnectAttempts === 0) reject(error);
|
|
103
|
-
});
|
|
104
|
-
} catch (error) {
|
|
105
|
-
this.isConnecting = false;
|
|
106
|
-
reject(error);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
disconnect(): void {
|
|
112
|
-
this.shouldReconnect = false;
|
|
113
|
-
if (this.reconnectTimer) {
|
|
114
|
-
clearTimeout(this.reconnectTimer);
|
|
115
|
-
this.reconnectTimer = null;
|
|
116
|
-
}
|
|
117
|
-
this.stopPing();
|
|
118
|
-
if (this.ws) {
|
|
119
|
-
this.ws.removeAllListeners();
|
|
120
|
-
if (this.ws.readyState === WebSocket.OPEN) {
|
|
121
|
-
this.ws.close(1000, 'Client disconnect');
|
|
122
|
-
}
|
|
123
|
-
this.ws = null;
|
|
124
|
-
}
|
|
125
|
-
console.log('[perp-ws] Disconnected');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
get isConnected(): boolean {
|
|
129
|
-
return this.ws?.readyState === WebSocket.OPEN;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// ── EVENT HANDLERS ─────────────────────────────────────────
|
|
133
|
-
|
|
134
|
-
onFillReceived(callback: FillCallback): void {
|
|
135
|
-
this.onFill = callback;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
onFundingReceived(callback: FundingCallback): void {
|
|
139
|
-
this.onFunding = callback;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
onLiquidationDetected(callback: LiquidationCallback): void {
|
|
143
|
-
this.onLiquidation = callback;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
getLastProcessedFillTime(): number {
|
|
147
|
-
return this.lastProcessedFillTime;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ── MESSAGE HANDLING ───────────────────────────────────────
|
|
151
|
-
|
|
152
|
-
private handleMessage(data: WebSocket.Data): void {
|
|
153
|
-
try {
|
|
154
|
-
const msg = JSON.parse(data.toString());
|
|
155
|
-
if (msg.channel === 'userFills') {
|
|
156
|
-
this.handleFillMessage(msg.data);
|
|
157
|
-
} else if (msg.channel === 'userFundings') {
|
|
158
|
-
this.handleFundingMessage(msg.data);
|
|
159
|
-
}
|
|
160
|
-
} catch {
|
|
161
|
-
// Ignore parse errors (pong frames, etc.)
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
private handleFillMessage(fills: any[]): void {
|
|
166
|
-
if (!Array.isArray(fills) || !this.onFill) return;
|
|
167
|
-
|
|
168
|
-
for (const rawFill of fills) {
|
|
169
|
-
const fill = this.client.parseFill(rawFill);
|
|
170
|
-
|
|
171
|
-
if (fill.time > this.lastProcessedFillTime) {
|
|
172
|
-
this.lastProcessedFillTime = fill.time;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (fill.liquidation && this.onLiquidation) {
|
|
176
|
-
this.onLiquidation(fill.coin, parseFloat(fill.sz));
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
this.onFill(fill);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
private handleFundingMessage(fundings: any[]): void {
|
|
184
|
-
if (!Array.isArray(fundings) || !this.onFunding) return;
|
|
185
|
-
|
|
186
|
-
for (const funding of fundings) {
|
|
187
|
-
this.onFunding({
|
|
188
|
-
time: funding.time,
|
|
189
|
-
coin: funding.coin,
|
|
190
|
-
usdc: funding.usdc,
|
|
191
|
-
szi: funding.szi,
|
|
192
|
-
fundingRate: funding.fundingRate,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// ── BACKFILL ───────────────────────────────────────────────
|
|
198
|
-
|
|
199
|
-
private async backfillMissedFills(): Promise<void> {
|
|
200
|
-
if (this.lastProcessedFillTime === 0 || !this.onFill) return;
|
|
201
|
-
|
|
202
|
-
console.log(`[perp-ws] Backfilling fills since ${new Date(this.lastProcessedFillTime).toISOString()}`);
|
|
203
|
-
|
|
204
|
-
const fills = await this.client.getUserFillsByTime(
|
|
205
|
-
this.userAddress,
|
|
206
|
-
this.lastProcessedFillTime + 1,
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
if (fills.length > 0) {
|
|
210
|
-
console.log(`[perp-ws] Backfilled ${fills.length} missed fills`);
|
|
211
|
-
for (const rawFill of fills) {
|
|
212
|
-
const fill = this.client.parseFill(rawFill);
|
|
213
|
-
if (fill.time > this.lastProcessedFillTime) {
|
|
214
|
-
this.lastProcessedFillTime = fill.time;
|
|
215
|
-
}
|
|
216
|
-
if (fill.liquidation && this.onLiquidation) {
|
|
217
|
-
this.onLiquidation(fill.coin, parseFloat(fill.sz));
|
|
218
|
-
}
|
|
219
|
-
this.onFill(fill);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// ── RECONNECTION ───────────────────────────────────────────
|
|
225
|
-
|
|
226
|
-
private scheduleReconnect(): void {
|
|
227
|
-
if (!this.shouldReconnect || this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
228
|
-
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
229
|
-
console.error(`[perp-ws] Max reconnect attempts (${this.maxReconnectAttempts}) reached`);
|
|
230
|
-
}
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const delay = Math.min(
|
|
235
|
-
this.baseReconnectMs * Math.pow(2, this.reconnectAttempts),
|
|
236
|
-
this.maxReconnectMs,
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
this.reconnectAttempts++;
|
|
240
|
-
console.log(`[perp-ws] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
241
|
-
|
|
242
|
-
this.reconnectTimer = setTimeout(() => {
|
|
243
|
-
this.connect().catch((err) => {
|
|
244
|
-
console.error('[perp-ws] Reconnect failed:', err instanceof Error ? err.message : err);
|
|
245
|
-
});
|
|
246
|
-
}, delay);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// ── KEEPALIVE ──────────────────────────────────────────────
|
|
250
|
-
|
|
251
|
-
private startPing(): void {
|
|
252
|
-
this.stopPing();
|
|
253
|
-
this.pingTimer = setInterval(() => {
|
|
254
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
255
|
-
this.ws.send(JSON.stringify({ method: 'ping' }));
|
|
256
|
-
}
|
|
257
|
-
}, 25_000);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
private stopPing(): void {
|
|
261
|
-
if (this.pingTimer) {
|
|
262
|
-
clearInterval(this.pingTimer);
|
|
263
|
-
this.pingTimer = null;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// ── HELPERS ────────────────────────────────────────────────
|
|
268
|
-
|
|
269
|
-
private subscribe(msg: Record<string, unknown>): void {
|
|
270
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
271
|
-
this.ws.send(JSON.stringify(msg));
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
package/src/position-tracker.ts
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
import type { TrackedPosition, TradeRecord, PositionSummary, StrategyStore } from '@exagent/sdk';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Stablecoins and quote assets that should NOT be tracked as positions
|
|
5
|
-
* on spot DEX venues (you don't want a "USDC position" when swapping).
|
|
6
|
-
* On perp venues, ALL instruments are tracked (ETH, BTC, etc.).
|
|
7
|
-
*/
|
|
8
|
-
const QUOTE_ASSETS = new Set(['USDC', 'USDbC', 'DAI', 'USDT', 'EURC']);
|
|
9
|
-
|
|
10
|
-
/** Perp venues where positions can be short (negative quantity) */
|
|
11
|
-
const PERP_VENUES = new Set(['hyperliquid_perp']);
|
|
12
|
-
|
|
13
|
-
/** Prediction market venues */
|
|
14
|
-
const PREDICTION_VENUES = new Set(['polymarket']);
|
|
15
|
-
|
|
16
|
-
export class PositionTracker {
|
|
17
|
-
private positions: Map<string, TrackedPosition> = new Map();
|
|
18
|
-
private trades: TradeRecord[] = [];
|
|
19
|
-
private realizedPnL = 0;
|
|
20
|
-
private store: StrategyStore;
|
|
21
|
-
|
|
22
|
-
constructor(store: StrategyStore) {
|
|
23
|
-
this.store = store;
|
|
24
|
-
this.load();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Build a unique position key: venue-scoped when venue is provided */
|
|
28
|
-
private positionKey(token: string, venue?: string): string {
|
|
29
|
-
return venue ? `${venue}:${token}` : token;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
private load(): void {
|
|
33
|
-
const saved = this.store.get<{
|
|
34
|
-
positions: [string, TrackedPosition][];
|
|
35
|
-
trades: TradeRecord[];
|
|
36
|
-
realizedPnL: number;
|
|
37
|
-
}>('position_tracker');
|
|
38
|
-
|
|
39
|
-
if (saved) {
|
|
40
|
-
this.positions = new Map(saved.positions);
|
|
41
|
-
this.trades = saved.trades;
|
|
42
|
-
this.realizedPnL = saved.realizedPnL;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
private save(): void {
|
|
47
|
-
this.store.set('position_tracker', {
|
|
48
|
-
positions: Array.from(this.positions.entries()),
|
|
49
|
-
trades: this.trades.slice(-1000),
|
|
50
|
-
realizedPnL: this.realizedPnL,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Returns realized PnL from this trade (non-zero when closing/reducing a position) */
|
|
55
|
-
recordBuy(
|
|
56
|
-
token: string,
|
|
57
|
-
quantity: number,
|
|
58
|
-
price: number,
|
|
59
|
-
fee: number,
|
|
60
|
-
venue?: string,
|
|
61
|
-
chain?: string,
|
|
62
|
-
venueFillId?: string,
|
|
63
|
-
): number {
|
|
64
|
-
// Skip quote assets on spot venues (no "USDC position" from swaps).
|
|
65
|
-
// Perp and prediction venues track all instruments.
|
|
66
|
-
const isPerp = venue ? PERP_VENUES.has(venue) : false;
|
|
67
|
-
const isPrediction = venue ? PREDICTION_VENUES.has(venue) : false;
|
|
68
|
-
if (!isPerp && !isPrediction && QUOTE_ASSETS.has(token)) return 0;
|
|
69
|
-
|
|
70
|
-
const key = this.positionKey(token, venue);
|
|
71
|
-
const existing = this.positions.get(key);
|
|
72
|
-
let tradePnL = 0;
|
|
73
|
-
|
|
74
|
-
if (existing) {
|
|
75
|
-
if (isPerp && existing.quantity < 0) {
|
|
76
|
-
// Buying to close/reduce a short position
|
|
77
|
-
const closedQty = Math.min(quantity, Math.abs(existing.quantity));
|
|
78
|
-
tradePnL = (existing.costBasisPerUnit - price) * closedQty - fee;
|
|
79
|
-
this.realizedPnL += tradePnL;
|
|
80
|
-
|
|
81
|
-
existing.quantity += quantity;
|
|
82
|
-
if (Math.abs(existing.quantity) <= 0.000001) {
|
|
83
|
-
this.positions.delete(key);
|
|
84
|
-
} else if (existing.quantity > 0) {
|
|
85
|
-
// Flipped from short to long — reset cost basis
|
|
86
|
-
existing.costBasisPerUnit = price;
|
|
87
|
-
}
|
|
88
|
-
} else {
|
|
89
|
-
// Adding to a long position (or adding to perp long)
|
|
90
|
-
const totalQty = existing.quantity + quantity;
|
|
91
|
-
const totalCost = existing.costBasisPerUnit * existing.quantity + price * quantity;
|
|
92
|
-
existing.costBasisPerUnit = totalCost / totalQty;
|
|
93
|
-
existing.quantity = totalQty;
|
|
94
|
-
}
|
|
95
|
-
} else {
|
|
96
|
-
this.positions.set(key, {
|
|
97
|
-
token,
|
|
98
|
-
quantity,
|
|
99
|
-
costBasisPerUnit: price,
|
|
100
|
-
entryTimestamp: Date.now(),
|
|
101
|
-
venue,
|
|
102
|
-
chain,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
this.trades.push({
|
|
107
|
-
token,
|
|
108
|
-
action: 'buy',
|
|
109
|
-
quantity,
|
|
110
|
-
price,
|
|
111
|
-
fee,
|
|
112
|
-
timestamp: Date.now(),
|
|
113
|
-
venue,
|
|
114
|
-
chain,
|
|
115
|
-
venueFillId,
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
this.save();
|
|
119
|
-
return tradePnL;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Returns realized PnL from this trade (non-zero when closing/reducing a position) */
|
|
123
|
-
recordSell(
|
|
124
|
-
token: string,
|
|
125
|
-
quantity: number,
|
|
126
|
-
price: number,
|
|
127
|
-
fee: number,
|
|
128
|
-
venue?: string,
|
|
129
|
-
chain?: string,
|
|
130
|
-
venueFillId?: string,
|
|
131
|
-
): number {
|
|
132
|
-
// Skip quote assets on spot venues (no "USDC position" from swaps).
|
|
133
|
-
// Perp and prediction venues track all instruments.
|
|
134
|
-
const isPerp = venue ? PERP_VENUES.has(venue) : false;
|
|
135
|
-
const isPrediction = venue ? PREDICTION_VENUES.has(venue) : false;
|
|
136
|
-
if (!isPerp && !isPrediction && QUOTE_ASSETS.has(token)) return 0;
|
|
137
|
-
|
|
138
|
-
const key = this.positionKey(token, venue);
|
|
139
|
-
const existing = this.positions.get(key);
|
|
140
|
-
let tradePnL = 0;
|
|
141
|
-
|
|
142
|
-
if (existing) {
|
|
143
|
-
if (isPerp && existing.quantity > 0) {
|
|
144
|
-
// Selling to close/reduce a long position
|
|
145
|
-
const closedQty = Math.min(quantity, existing.quantity);
|
|
146
|
-
tradePnL = (price - existing.costBasisPerUnit) * closedQty - fee;
|
|
147
|
-
this.realizedPnL += tradePnL;
|
|
148
|
-
|
|
149
|
-
existing.quantity -= quantity;
|
|
150
|
-
if (Math.abs(existing.quantity) <= 0.000001) {
|
|
151
|
-
this.positions.delete(key);
|
|
152
|
-
} else if (existing.quantity < 0) {
|
|
153
|
-
// Flipped from long to short — reset cost basis
|
|
154
|
-
existing.costBasisPerUnit = price;
|
|
155
|
-
}
|
|
156
|
-
} else if (isPerp && existing.quantity <= 0) {
|
|
157
|
-
// Adding to a short position
|
|
158
|
-
const totalQty = Math.abs(existing.quantity) + quantity;
|
|
159
|
-
const totalCost = existing.costBasisPerUnit * Math.abs(existing.quantity) + price * quantity;
|
|
160
|
-
existing.costBasisPerUnit = totalCost / totalQty;
|
|
161
|
-
existing.quantity -= quantity;
|
|
162
|
-
} else {
|
|
163
|
-
// Spot sell — standard close logic
|
|
164
|
-
tradePnL = (price - existing.costBasisPerUnit) * quantity - fee;
|
|
165
|
-
this.realizedPnL += tradePnL;
|
|
166
|
-
|
|
167
|
-
existing.quantity -= quantity;
|
|
168
|
-
if (existing.quantity <= 0.000001) {
|
|
169
|
-
this.positions.delete(key);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
} else if (isPerp) {
|
|
173
|
-
// Opening a new short position (perps only)
|
|
174
|
-
this.positions.set(key, {
|
|
175
|
-
token,
|
|
176
|
-
quantity: -quantity,
|
|
177
|
-
costBasisPerUnit: price,
|
|
178
|
-
entryTimestamp: Date.now(),
|
|
179
|
-
venue,
|
|
180
|
-
chain,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
this.trades.push({
|
|
185
|
-
token,
|
|
186
|
-
action: 'sell',
|
|
187
|
-
quantity,
|
|
188
|
-
price,
|
|
189
|
-
fee,
|
|
190
|
-
timestamp: Date.now(),
|
|
191
|
-
venue,
|
|
192
|
-
chain,
|
|
193
|
-
venueFillId,
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
this.save();
|
|
197
|
-
return tradePnL;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
getSummary(prices: Record<string, number>): PositionSummary {
|
|
201
|
-
let totalUnrealizedPnL = 0;
|
|
202
|
-
|
|
203
|
-
const openPositions = Array.from(this.positions.values());
|
|
204
|
-
for (const pos of openPositions) {
|
|
205
|
-
const currentPrice = prices[pos.token];
|
|
206
|
-
if (currentPrice) {
|
|
207
|
-
if (pos.quantity >= 0) {
|
|
208
|
-
// Long: profit when price goes up
|
|
209
|
-
totalUnrealizedPnL += (currentPrice - pos.costBasisPerUnit) * pos.quantity;
|
|
210
|
-
} else {
|
|
211
|
-
// Short: profit when price goes down
|
|
212
|
-
totalUnrealizedPnL += (pos.costBasisPerUnit - currentPrice) * Math.abs(pos.quantity);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return {
|
|
218
|
-
openPositions,
|
|
219
|
-
totalUnrealizedPnL,
|
|
220
|
-
totalRealizedPnL: this.realizedPnL,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
getPositions(): TrackedPosition[] {
|
|
225
|
-
return Array.from(this.positions.values());
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
getTrades(limit?: number): TradeRecord[] {
|
|
229
|
-
if (limit) return this.trades.slice(-limit);
|
|
230
|
-
return [...this.trades];
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
getRealizedPnL(): number {
|
|
234
|
-
return this.realizedPnL;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
reset(): void {
|
|
238
|
-
this.positions.clear();
|
|
239
|
-
this.trades = [];
|
|
240
|
-
this.realizedPnL = 0;
|
|
241
|
-
this.save();
|
|
242
|
-
}
|
|
243
|
-
}
|