openbroker 1.0.33
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/CHANGELOG.md +94 -0
- package/README.md +160 -0
- package/SKILL.md +296 -0
- package/bin/cli.ts +170 -0
- package/bin/openbroker.js +24 -0
- package/config/example.env +48 -0
- package/package.json +79 -0
- package/scripts/core/client.ts +844 -0
- package/scripts/core/config.ts +92 -0
- package/scripts/core/types.ts +192 -0
- package/scripts/core/utils.ts +156 -0
- package/scripts/info/account.ts +117 -0
- package/scripts/info/all-markets.ts +223 -0
- package/scripts/info/funding.ts +133 -0
- package/scripts/info/markets.ts +151 -0
- package/scripts/info/positions.ts +88 -0
- package/scripts/info/search-markets.ts +230 -0
- package/scripts/info/spot.ts +192 -0
- package/scripts/operations/bracket.ts +285 -0
- package/scripts/operations/cancel.ts +124 -0
- package/scripts/operations/chase.ts +236 -0
- package/scripts/operations/limit-order.ts +160 -0
- package/scripts/operations/market-order.ts +167 -0
- package/scripts/operations/scale.ts +263 -0
- package/scripts/operations/set-tpsl.ts +302 -0
- package/scripts/operations/trigger-order.ts +201 -0
- package/scripts/operations/twap.ts +222 -0
- package/scripts/setup/approve-builder.ts +178 -0
- package/scripts/setup/onboard.ts +242 -0
- package/scripts/strategies/dca.ts +292 -0
- package/scripts/strategies/funding-arb.ts +319 -0
- package/scripts/strategies/grid.ts +397 -0
- package/scripts/strategies/mm-maker.ts +411 -0
- package/scripts/strategies/mm-spread.ts +402 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
// Simple Market Making - Place bid/ask quotes around mid price with inventory management
|
|
3
|
+
|
|
4
|
+
import { getClient } from '../core/client.js';
|
|
5
|
+
import { formatUsd, parseArgs, sleep } from '../core/utils.js';
|
|
6
|
+
|
|
7
|
+
function printUsage() {
|
|
8
|
+
console.log(`
|
|
9
|
+
Open Broker - Market Making Spread
|
|
10
|
+
==================================
|
|
11
|
+
|
|
12
|
+
Place bid and ask orders around the mid price, earning the spread when both sides fill.
|
|
13
|
+
Includes inventory management to stay neutral and avoid directional drift.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
npx tsx scripts/strategies/mm-spread.ts --coin <COIN> --size <SIZE> --spread <BPS>
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--coin Asset to market make (e.g., ETH, BTC)
|
|
20
|
+
--size Order size on each side (in base asset)
|
|
21
|
+
--spread Spread in bps from mid (e.g., 10 = 0.1% each side)
|
|
22
|
+
--skew-factor How aggressively to skew for inventory (default: 2.0)
|
|
23
|
+
--refresh Refresh interval in milliseconds (default: 2000)
|
|
24
|
+
--max-position Maximum net position before stopping that side (default: 3x size)
|
|
25
|
+
--cooldown Cooldown after fill before same-side quote (ms, default: 5000)
|
|
26
|
+
--duration How long to run in minutes (default: runs until stopped)
|
|
27
|
+
--dry Dry run - show strategy parameters without trading
|
|
28
|
+
|
|
29
|
+
Inventory Management:
|
|
30
|
+
The strategy automatically skews quotes based on inventory to stay neutral:
|
|
31
|
+
- If LONG: Bid is wider (less aggressive), Ask is tighter (more aggressive)
|
|
32
|
+
- If SHORT: Bid is tighter (more aggressive), Ask is wider (less aggressive)
|
|
33
|
+
- At max position: Stops quoting that side entirely
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
# Market make ETH with 0.1 size, 10bps spread
|
|
37
|
+
npx tsx scripts/strategies/mm-spread.ts --coin ETH --size 0.1 --spread 10
|
|
38
|
+
|
|
39
|
+
# Tighter position limit and faster cooldown
|
|
40
|
+
npx tsx scripts/strategies/mm-spread.ts --coin BTC --size 0.01 --spread 5 --max-position 0.03 --cooldown 3000
|
|
41
|
+
|
|
42
|
+
# Preview setup
|
|
43
|
+
npx tsx scripts/strategies/mm-spread.ts --coin SOL --size 10 --spread 15 --dry
|
|
44
|
+
|
|
45
|
+
Risks:
|
|
46
|
+
- Adverse selection: getting picked off by informed traders
|
|
47
|
+
- Inventory risk: accumulating position during directional moves
|
|
48
|
+
- Use smaller --max-position for volatile assets
|
|
49
|
+
`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface Quote {
|
|
53
|
+
side: 'bid' | 'ask';
|
|
54
|
+
price: number;
|
|
55
|
+
size: number;
|
|
56
|
+
oid?: number;
|
|
57
|
+
placedAt: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface MmState {
|
|
61
|
+
bid: Quote | null;
|
|
62
|
+
ask: Quote | null;
|
|
63
|
+
lastBidFill: number;
|
|
64
|
+
lastAskFill: number;
|
|
65
|
+
totalBought: number;
|
|
66
|
+
totalSold: number;
|
|
67
|
+
totalBuyCost: number;
|
|
68
|
+
totalSellRevenue: number;
|
|
69
|
+
roundTrips: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function main() {
|
|
73
|
+
const args = parseArgs(process.argv.slice(2));
|
|
74
|
+
|
|
75
|
+
const coin = args.coin as string;
|
|
76
|
+
const size = parseFloat(args.size as string);
|
|
77
|
+
const spreadBps = parseFloat(args.spread as string);
|
|
78
|
+
const skewFactor = args['skew-factor'] ? parseFloat(args['skew-factor'] as string) : 2.0;
|
|
79
|
+
const refreshMs = args.refresh ? parseInt(args.refresh as string) : 2000;
|
|
80
|
+
const maxPosition = args['max-position'] ? parseFloat(args['max-position'] as string) : size * 3;
|
|
81
|
+
const cooldownMs = args.cooldown ? parseInt(args.cooldown as string) : 5000;
|
|
82
|
+
const durationMins = args.duration ? parseFloat(args.duration as string) : Infinity;
|
|
83
|
+
const dryRun = args.dry as boolean;
|
|
84
|
+
|
|
85
|
+
if (!coin || isNaN(size) || isNaN(spreadBps)) {
|
|
86
|
+
printUsage();
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (spreadBps < 1) {
|
|
91
|
+
console.error('Error: --spread must be at least 1 bps');
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const client = getClient();
|
|
96
|
+
|
|
97
|
+
if (args.verbose) {
|
|
98
|
+
client.verbose = true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log('Open Broker - Market Making');
|
|
102
|
+
console.log('===========================\n');
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// Get fresh market data
|
|
106
|
+
const mids = await client.getAllMids();
|
|
107
|
+
const midPrice = parseFloat(mids[coin]);
|
|
108
|
+
if (!midPrice) {
|
|
109
|
+
console.error(`Error: No market data for ${coin}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const baseSpread = spreadBps / 10000;
|
|
114
|
+
const halfSpread = baseSpread / 2;
|
|
115
|
+
const bidPrice = midPrice * (1 - halfSpread);
|
|
116
|
+
const askPrice = midPrice * (1 + halfSpread);
|
|
117
|
+
const notionalPerSide = size * midPrice;
|
|
118
|
+
const profitPerRoundTrip = (askPrice - bidPrice) * size;
|
|
119
|
+
|
|
120
|
+
console.log('Strategy Configuration');
|
|
121
|
+
console.log('----------------------');
|
|
122
|
+
console.log(`Coin: ${coin}`);
|
|
123
|
+
console.log(`Current Mid: ${formatUsd(midPrice)}`);
|
|
124
|
+
console.log(`Order Size: ${size} ${coin}`);
|
|
125
|
+
console.log(`Notional/Side: ${formatUsd(notionalPerSide)}`);
|
|
126
|
+
console.log(`Spread: ${spreadBps} bps (${(spreadBps / 100).toFixed(2)}%)`);
|
|
127
|
+
console.log(`Max Position: ±${maxPosition} ${coin}`);
|
|
128
|
+
console.log(`Skew Factor: ${skewFactor}x`);
|
|
129
|
+
console.log(`Cooldown: ${cooldownMs}ms`);
|
|
130
|
+
console.log(`Refresh: ${refreshMs}ms`);
|
|
131
|
+
console.log(`\nQuote Prices (at neutral):`);
|
|
132
|
+
console.log(` Bid: ${formatUsd(bidPrice)} (-${(halfSpread * 100).toFixed(3)}%)`);
|
|
133
|
+
console.log(` Ask: ${formatUsd(askPrice)} (+${(halfSpread * 100).toFixed(3)}%)`);
|
|
134
|
+
console.log(`\nProfit/Round Trip: ${formatUsd(profitPerRoundTrip)}`);
|
|
135
|
+
|
|
136
|
+
console.log(`\nInventory Skewing:`);
|
|
137
|
+
console.log(` When LONG: Bid wider, Ask tighter (encourage selling)`);
|
|
138
|
+
console.log(` When SHORT: Bid tighter, Ask wider (encourage buying)`);
|
|
139
|
+
|
|
140
|
+
if (dryRun) {
|
|
141
|
+
console.log('\n--- Dry run complete ---');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log('\nStarting market making...\n');
|
|
146
|
+
|
|
147
|
+
const state: MmState = {
|
|
148
|
+
bid: null,
|
|
149
|
+
ask: null,
|
|
150
|
+
lastBidFill: 0,
|
|
151
|
+
lastAskFill: 0,
|
|
152
|
+
totalBought: 0,
|
|
153
|
+
totalSold: 0,
|
|
154
|
+
totalBuyCost: 0,
|
|
155
|
+
totalSellRevenue: 0,
|
|
156
|
+
roundTrips: 0,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const endTime = durationMins === Infinity ? Infinity : Date.now() + durationMins * 60 * 1000;
|
|
160
|
+
|
|
161
|
+
// Handle graceful shutdown
|
|
162
|
+
let shuttingDown = false;
|
|
163
|
+
process.on('SIGINT', async () => {
|
|
164
|
+
if (shuttingDown) return;
|
|
165
|
+
shuttingDown = true;
|
|
166
|
+
console.log('\n\nShutting down - cancelling quotes...');
|
|
167
|
+
await cancelQuotes(client, coin, state);
|
|
168
|
+
await printSummary(client, coin, state);
|
|
169
|
+
process.exit(0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
let lastLogTime = 0;
|
|
173
|
+
let iteration = 0;
|
|
174
|
+
|
|
175
|
+
while (Date.now() < endTime && !shuttingDown) {
|
|
176
|
+
iteration++;
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
|
|
179
|
+
// Always get fresh mid price
|
|
180
|
+
const freshMids = await client.getAllMids();
|
|
181
|
+
const currentMid = parseFloat(freshMids[coin]);
|
|
182
|
+
|
|
183
|
+
// Get actual position from exchange for ground truth
|
|
184
|
+
const userState = await client.getUserState();
|
|
185
|
+
const position = userState.assetPositions.find(p => p.position.coin === coin);
|
|
186
|
+
const actualPosition = position ? parseFloat(position.position.szi) : 0;
|
|
187
|
+
|
|
188
|
+
// Get our open orders
|
|
189
|
+
const openOrders = await client.getOpenOrders();
|
|
190
|
+
const ourOrders = openOrders.filter(o => o.coin === coin);
|
|
191
|
+
const openOids = new Set(ourOrders.map(o => o.oid));
|
|
192
|
+
|
|
193
|
+
// Check for bid fill
|
|
194
|
+
if (state.bid && state.bid.oid && !openOids.has(state.bid.oid)) {
|
|
195
|
+
const fillPrice = state.bid.price;
|
|
196
|
+
state.totalBought += state.bid.size;
|
|
197
|
+
state.totalBuyCost += fillPrice * state.bid.size;
|
|
198
|
+
state.lastBidFill = now;
|
|
199
|
+
console.log(`[${new Date().toLocaleTimeString()}] BID FILLED @ ${formatUsd(fillPrice)} | Position: ${actualPosition.toFixed(4)}`);
|
|
200
|
+
state.bid = null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Check for ask fill
|
|
204
|
+
if (state.ask && state.ask.oid && !openOids.has(state.ask.oid)) {
|
|
205
|
+
const fillPrice = state.ask.price;
|
|
206
|
+
state.totalSold += state.ask.size;
|
|
207
|
+
state.totalSellRevenue += fillPrice * state.ask.size;
|
|
208
|
+
state.lastAskFill = now;
|
|
209
|
+
state.roundTrips += 0.5;
|
|
210
|
+
console.log(`[${new Date().toLocaleTimeString()}] ASK FILLED @ ${formatUsd(fillPrice)} | Position: ${actualPosition.toFixed(4)}`);
|
|
211
|
+
state.ask = null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Calculate inventory-based skew
|
|
215
|
+
// Skew ranges from -1 (max short) to +1 (max long)
|
|
216
|
+
const inventoryRatio = Math.max(-1, Math.min(1, actualPosition / maxPosition));
|
|
217
|
+
|
|
218
|
+
// Skew the spread: positive inventory = wider bid, tighter ask
|
|
219
|
+
const bidSkew = halfSpread * (1 + inventoryRatio * skewFactor);
|
|
220
|
+
const askSkew = halfSpread * (1 - inventoryRatio * skewFactor);
|
|
221
|
+
|
|
222
|
+
// Calculate skewed prices
|
|
223
|
+
const skewedBidPrice = currentMid * (1 - Math.max(0.0001, bidSkew));
|
|
224
|
+
const skewedAskPrice = currentMid * (1 + Math.max(0.0001, askSkew));
|
|
225
|
+
|
|
226
|
+
// Determine if we should quote each side
|
|
227
|
+
const bidCooldownOk = (now - state.lastBidFill) > cooldownMs;
|
|
228
|
+
const askCooldownOk = (now - state.lastAskFill) > cooldownMs;
|
|
229
|
+
const shouldBid = actualPosition < maxPosition && bidCooldownOk;
|
|
230
|
+
const shouldAsk = actualPosition > -maxPosition && askCooldownOk;
|
|
231
|
+
|
|
232
|
+
// Cancel stale quotes that have drifted too far from target
|
|
233
|
+
if (state.bid && state.bid.oid) {
|
|
234
|
+
const bidDrift = Math.abs(state.bid.price - skewedBidPrice) / currentMid;
|
|
235
|
+
if (bidDrift > 0.001 || !shouldBid) { // 0.1% drift threshold
|
|
236
|
+
try {
|
|
237
|
+
await client.cancel(coin, state.bid.oid);
|
|
238
|
+
state.bid = null;
|
|
239
|
+
} catch {
|
|
240
|
+
// Order may have been filled
|
|
241
|
+
state.bid = null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (state.ask && state.ask.oid) {
|
|
247
|
+
const askDrift = Math.abs(state.ask.price - skewedAskPrice) / currentMid;
|
|
248
|
+
if (askDrift > 0.001 || !shouldAsk) { // 0.1% drift threshold
|
|
249
|
+
try {
|
|
250
|
+
await client.cancel(coin, state.ask.oid);
|
|
251
|
+
state.ask = null;
|
|
252
|
+
} catch {
|
|
253
|
+
// Order may have been filled
|
|
254
|
+
state.ask = null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Place new bid if needed
|
|
260
|
+
if (shouldBid && !state.bid) {
|
|
261
|
+
const response = await client.limitOrder(coin, true, size, skewedBidPrice, 'Gtc', false);
|
|
262
|
+
|
|
263
|
+
if (response.status === 'ok' && response.response && typeof response.response === 'object') {
|
|
264
|
+
const status = response.response.data.statuses[0];
|
|
265
|
+
if (status?.resting) {
|
|
266
|
+
state.bid = {
|
|
267
|
+
side: 'bid',
|
|
268
|
+
price: skewedBidPrice,
|
|
269
|
+
size,
|
|
270
|
+
oid: status.resting.oid,
|
|
271
|
+
placedAt: now,
|
|
272
|
+
};
|
|
273
|
+
} else if (status?.filled) {
|
|
274
|
+
// Filled immediately - unusual for a bid below mid
|
|
275
|
+
const fillPrice = parseFloat(status.filled.avgPx);
|
|
276
|
+
state.totalBought += size;
|
|
277
|
+
state.totalBuyCost += fillPrice * size;
|
|
278
|
+
state.lastBidFill = now;
|
|
279
|
+
console.log(`[${new Date().toLocaleTimeString()}] BID FILLED IMMEDIATELY @ ${formatUsd(fillPrice)}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Place new ask if needed
|
|
285
|
+
if (shouldAsk && !state.ask) {
|
|
286
|
+
const response = await client.limitOrder(coin, false, size, skewedAskPrice, 'Gtc', false);
|
|
287
|
+
|
|
288
|
+
if (response.status === 'ok' && response.response && typeof response.response === 'object') {
|
|
289
|
+
const status = response.response.data.statuses[0];
|
|
290
|
+
if (status?.resting) {
|
|
291
|
+
state.ask = {
|
|
292
|
+
side: 'ask',
|
|
293
|
+
price: skewedAskPrice,
|
|
294
|
+
size,
|
|
295
|
+
oid: status.resting.oid,
|
|
296
|
+
placedAt: now,
|
|
297
|
+
};
|
|
298
|
+
} else if (status?.filled) {
|
|
299
|
+
// Filled immediately - unusual for an ask above mid
|
|
300
|
+
const fillPrice = parseFloat(status.filled.avgPx);
|
|
301
|
+
state.totalSold += size;
|
|
302
|
+
state.totalSellRevenue += fillPrice * size;
|
|
303
|
+
state.lastAskFill = now;
|
|
304
|
+
state.roundTrips += 0.5;
|
|
305
|
+
console.log(`[${new Date().toLocaleTimeString()}] ASK FILLED IMMEDIATELY @ ${formatUsd(fillPrice)}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Periodic status log
|
|
311
|
+
if (now - lastLogTime > 30000) {
|
|
312
|
+
const bidStatus = state.bid ? formatUsd(state.bid.price) : (shouldBid ? 'placing...' : 'paused');
|
|
313
|
+
const askStatus = state.ask ? formatUsd(state.ask.price) : (shouldAsk ? 'placing...' : 'paused');
|
|
314
|
+
const realizedPnl = state.totalSellRevenue - state.totalBuyCost;
|
|
315
|
+
const skewPct = (inventoryRatio * 100).toFixed(1);
|
|
316
|
+
|
|
317
|
+
console.log(`[${new Date().toLocaleTimeString()}] Mid: ${formatUsd(currentMid)} | Bid: ${bidStatus} | Ask: ${askStatus} | Pos: ${actualPosition.toFixed(4)} (skew: ${skewPct}%) | PnL: ${formatUsd(realizedPnl)}`);
|
|
318
|
+
lastLogTime = now;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
await sleep(refreshMs);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// End of duration
|
|
325
|
+
if (!shuttingDown) {
|
|
326
|
+
console.log('\nDuration ended. Cancelling quotes...');
|
|
327
|
+
await cancelQuotes(client, coin, state);
|
|
328
|
+
await printSummary(client, coin, state);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.error('Error:', error);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function cancelQuotes(
|
|
338
|
+
client: ReturnType<typeof getClient>,
|
|
339
|
+
coin: string,
|
|
340
|
+
state: MmState
|
|
341
|
+
): Promise<void> {
|
|
342
|
+
if (state.bid && state.bid.oid) {
|
|
343
|
+
try {
|
|
344
|
+
await client.cancel(coin, state.bid.oid);
|
|
345
|
+
console.log(` Cancelled bid @ ${formatUsd(state.bid.price)}`);
|
|
346
|
+
} catch {
|
|
347
|
+
// Ignore
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (state.ask && state.ask.oid) {
|
|
351
|
+
try {
|
|
352
|
+
await client.cancel(coin, state.ask.oid);
|
|
353
|
+
console.log(` Cancelled ask @ ${formatUsd(state.ask.price)}`);
|
|
354
|
+
} catch {
|
|
355
|
+
// Ignore
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function printSummary(
|
|
361
|
+
client: ReturnType<typeof getClient>,
|
|
362
|
+
coin: string,
|
|
363
|
+
state: MmState
|
|
364
|
+
): Promise<void> {
|
|
365
|
+
// Get final position and price
|
|
366
|
+
const userState = await client.getUserState();
|
|
367
|
+
const position = userState.assetPositions.find(p => p.position.coin === coin);
|
|
368
|
+
const finalPosition = position ? parseFloat(position.position.szi) : 0;
|
|
369
|
+
|
|
370
|
+
const mids = await client.getAllMids();
|
|
371
|
+
const currentMid = parseFloat(mids[coin]);
|
|
372
|
+
|
|
373
|
+
const inventoryValue = finalPosition * currentMid;
|
|
374
|
+
const realizedPnl = state.totalSellRevenue - state.totalBuyCost;
|
|
375
|
+
|
|
376
|
+
// Calculate inventory PnL
|
|
377
|
+
let inventoryPnl = 0;
|
|
378
|
+
if (finalPosition > 0 && state.totalBought > 0) {
|
|
379
|
+
const avgBuyPrice = state.totalBuyCost / state.totalBought;
|
|
380
|
+
inventoryPnl = (currentMid - avgBuyPrice) * finalPosition;
|
|
381
|
+
} else if (finalPosition < 0 && state.totalSold > 0) {
|
|
382
|
+
const avgSellPrice = state.totalSellRevenue / state.totalSold;
|
|
383
|
+
inventoryPnl = (avgSellPrice - currentMid) * Math.abs(finalPosition);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
console.log('\n========== MM Summary ==========');
|
|
387
|
+
console.log(`Total Bought: ${state.totalBought.toFixed(6)} ${coin}`);
|
|
388
|
+
console.log(`Total Sold: ${state.totalSold.toFixed(6)} ${coin}`);
|
|
389
|
+
console.log(`Final Position: ${finalPosition.toFixed(6)} ${coin}`);
|
|
390
|
+
console.log(`Inventory Value: ${formatUsd(inventoryValue)}`);
|
|
391
|
+
console.log(`Round Trips: ${state.roundTrips.toFixed(1)}`);
|
|
392
|
+
console.log(`Realized PnL: ${formatUsd(realizedPnl)}`);
|
|
393
|
+
console.log(`Inventory PnL: ${formatUsd(inventoryPnl)}`);
|
|
394
|
+
console.log(`Total PnL: ${formatUsd(realizedPnl + inventoryPnl)}`);
|
|
395
|
+
|
|
396
|
+
if (Math.abs(finalPosition) > 0.0001) {
|
|
397
|
+
console.log(`\n⚠️ You have an open position of ${finalPosition.toFixed(6)} ${coin}`);
|
|
398
|
+
console.log(` Close it manually if desired.`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
main();
|