perp-cli 0.7.1 → 0.7.3

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.
Files changed (53) hide show
  1. package/dist/arb/sizing.d.ts +1 -0
  2. package/dist/arb/sizing.js +25 -6
  3. package/dist/arb/utils.js +1 -0
  4. package/dist/bot/conditions.js +1 -2
  5. package/dist/bot/config.d.ts +3 -1
  6. package/dist/bot/config.js +4 -2
  7. package/dist/bot/engine.js +63 -18
  8. package/dist/bot/index.d.ts +1 -1
  9. package/dist/bot/index.js +1 -1
  10. package/dist/bot/presets.js +1 -0
  11. package/dist/bot/strategies/dca-strategy.js +8 -4
  12. package/dist/bot/strategies/funding-arb-strategy.d.ts +1 -1
  13. package/dist/bot/strategies/funding-arb-strategy.js +126 -14
  14. package/dist/bot/strategies/grid-strategy.js +56 -51
  15. package/dist/bot/tui/BotDashboard.d.ts +7 -0
  16. package/dist/bot/tui/BotDashboard.js +3 -2
  17. package/dist/bot/tui/index.d.ts +1 -1
  18. package/dist/cli-spec.js +1 -1
  19. package/dist/commands/account.js +1 -1
  20. package/dist/commands/arb/index.js +12 -4
  21. package/dist/commands/arb-auto.d.ts +2 -0
  22. package/dist/commands/arb-auto.js +41 -18
  23. package/dist/commands/arb.d.ts +1 -0
  24. package/dist/commands/arb.js +26 -5
  25. package/dist/commands/bot.js +47 -10
  26. package/dist/commands/bridge.js +1 -1
  27. package/dist/commands/dashboard.js +1 -1
  28. package/dist/commands/funds.js +1 -1
  29. package/dist/commands/history.js +6 -2
  30. package/dist/commands/init.d.ts +2 -2
  31. package/dist/commands/init.js +5 -0
  32. package/dist/commands/market.js +20 -11
  33. package/dist/commands/rebalance.js +3 -3
  34. package/dist/commands/risk.js +1 -1
  35. package/dist/commands/settings.js +1 -1
  36. package/dist/commands/trade.js +3 -3
  37. package/dist/commands/wallet.js +2 -2
  38. package/dist/config.d.ts +1 -1
  39. package/dist/config.js +1 -0
  40. package/dist/constants.js +1 -0
  41. package/dist/exchanges/aster.d.ts +4 -0
  42. package/dist/exchanges/aster.js +39 -28
  43. package/dist/exchanges/edgex.d.ts +62 -0
  44. package/dist/exchanges/edgex.js +552 -0
  45. package/dist/exchanges/index.d.ts +1 -0
  46. package/dist/exchanges/index.js +1 -0
  47. package/dist/exchanges/registry.js +5 -0
  48. package/dist/funding/normalize.js +1 -0
  49. package/dist/funding/rates.d.ts +1 -1
  50. package/dist/funding/rates.js +36 -3
  51. package/dist/index.js +19 -6
  52. package/dist/strategies/funding-arb.js +28 -5
  53. package/package.json +1 -1
@@ -25,6 +25,8 @@ export class GridStrategy {
25
25
  async init(ctx, snapshot) {
26
26
  this._config = ctx.config;
27
27
  const params = this.params;
28
+ if (params.grids < 2)
29
+ throw new Error("Grid strategy requires at least 2 grid lines");
28
30
  // Determine price range
29
31
  if (params.range_mode === "auto") {
30
32
  const pct = params.range_pct ?? 3;
@@ -40,12 +42,12 @@ export class GridStrategy {
40
42
  ctx.state.set("gridUpper", params.upper);
41
43
  ctx.state.set("gridLower", params.lower);
42
44
  }
43
- ctx.state.set("gridOrders", new Map());
45
+ ctx.state.set("gridLevels", []);
46
+ ctx.state.set("gridInitialized", false);
44
47
  }
45
48
  async onTick(ctx, snapshot) {
46
49
  const params = this.params;
47
50
  const actions = [];
48
- const gridOrders = ctx.state.get("gridOrders");
49
51
  const gridUpper = ctx.state.get("gridUpper");
50
52
  const gridLower = ctx.state.get("gridLower");
51
53
  // First tick: set leverage + place initial grid
@@ -53,59 +55,62 @@ export class GridStrategy {
53
55
  if (params.leverage) {
54
56
  actions.push({ type: "set_leverage", leverage: params.leverage });
55
57
  }
56
- return [...actions, ...this.buildGridOrders(ctx, snapshot.price)];
58
+ const gridActions = this.buildGridOrders(ctx, snapshot.price);
59
+ ctx.state.set("gridInitialized", true);
60
+ return [...actions, ...gridActions];
61
+ }
62
+ // Wait one tick after init/rebalance for orders to settle on exchange
63
+ if (!ctx.state.get("gridInitialized")) {
64
+ ctx.state.set("gridInitialized", true);
65
+ return [];
57
66
  }
58
67
  if (params.grids < 2)
59
68
  throw new Error("Grid requires at least 2 grid lines");
60
69
  const step = (gridUpper - gridLower) / (params.grids - 1);
61
70
  const sizePerGrid = params.size / params.grids;
62
- // Check for fills
71
+ // Price tolerance for matching: half a grid step
72
+ const tolerance = step * 0.5;
73
+ const gridLevels = ctx.state.get("gridLevels");
74
+ // Check for fills by comparing expected grid levels against actual open orders
63
75
  try {
64
76
  const openOrders = await ctx.adapter.getOpenOrders();
65
- const openIds = new Set(openOrders.filter(o => o.symbol.toUpperCase() === ctx.symbol.toUpperCase()).map(o => o.orderId));
66
- // Build filled order IDs from order history for accurate fill detection
67
- let filledIds = null;
68
- try {
69
- const history = await ctx.adapter.getOrderHistory(100);
70
- filledIds = new Set(history.filter(o => o.status === "filled").map(o => o.orderId));
71
- }
72
- catch { /* non-critical -- fall back to assuming fills */ }
73
- let newFills = 0;
74
- for (const [idx, orderId] of gridOrders.entries()) {
75
- if (!openIds.has(orderId)) {
76
- // Verify the order was actually filled (not just cancelled)
77
- if (filledIds && !filledIds.has(orderId)) {
78
- ctx.log(` [GRID] Order ${orderId} missing -- likely cancelled, skipping`);
79
- gridOrders.delete(idx);
80
- continue;
81
- }
82
- // Order filled -- place opposite order
83
- newFills++;
84
- // Determine new side and price
85
- const oldPrice = gridLower + step * idx;
86
- const wasBuy = oldPrice < snapshot.price;
87
- const newSide = wasBuy ? "sell" : "buy";
88
- const newPrice = wasBuy ? oldPrice + step : oldPrice - step;
89
- if (newPrice >= gridLower && newPrice <= gridUpper) {
90
- actions.push({
91
- type: "place_order",
92
- side: newSide,
93
- price: newPrice.toFixed(2),
94
- size: String(sizePerGrid),
95
- orderType: "limit",
96
- });
97
- // We'll update gridOrders when the action is executed
98
- // For now mark this index as pending -- use a sentinel
99
- gridOrders.set(idx, `pending-${idx}`);
100
- }
101
- else {
102
- gridOrders.delete(idx);
103
- }
77
+ const symbolOrders = openOrders.filter(o => o.symbol.toUpperCase() === ctx.symbol.toUpperCase());
78
+ // For each grid level, check if a matching open order exists (by price proximity)
79
+ const unmatchedLevels = [];
80
+ const matchedLevels = [];
81
+ for (const level of gridLevels) {
82
+ const hasMatch = symbolOrders.some(o => {
83
+ const orderPrice = parseFloat(o.price);
84
+ return Math.abs(orderPrice - level.price) < tolerance && o.side === level.side;
85
+ });
86
+ if (hasMatch) {
87
+ matchedLevels.push(level);
104
88
  }
89
+ else {
90
+ unmatchedLevels.push(level);
91
+ }
92
+ }
93
+ if (unmatchedLevels.length > 0) {
94
+ ctx.log(` [GRID] ${unmatchedLevels.length} fill(s) detected`);
105
95
  }
106
- if (newFills > 0) {
107
- ctx.log(` [GRID] ${newFills} fill(s) detected`);
96
+ // For each filled level, place the opposite (counter) order
97
+ const newLevels = [...matchedLevels];
98
+ for (const filled of unmatchedLevels) {
99
+ const wasBuy = filled.side === "buy";
100
+ const newSide = wasBuy ? "sell" : "buy";
101
+ const newPrice = wasBuy ? filled.price + step : filled.price - step;
102
+ if (newPrice >= gridLower && newPrice <= gridUpper) {
103
+ actions.push({
104
+ type: "place_order",
105
+ side: newSide,
106
+ price: newPrice.toFixed(2),
107
+ size: String(sizePerGrid),
108
+ orderType: "limit",
109
+ });
110
+ newLevels.push({ price: newPrice, side: newSide });
111
+ }
108
112
  }
113
+ ctx.state.set("gridLevels", newLevels);
109
114
  }
110
115
  catch { /* retry next loop */ }
111
116
  // Auto-rebalance if price exits range
@@ -120,12 +125,12 @@ export class GridStrategy {
120
125
  ctx.state.set("gridUpper", newUpper);
121
126
  ctx.state.set("gridLower", newLower);
122
127
  ctx.state.set("lastRebalance", Date.now());
128
+ ctx.state.set("gridInitialized", false);
123
129
  const rebalanceCount = (ctx.state.get("rebalanceCount") ?? 0) + 1;
124
130
  ctx.state.set("rebalanceCount", rebalanceCount);
125
131
  ctx.log(` [GRID] Rebalance #${rebalanceCount}: price $${snapshot.price.toFixed(2)} outside range -> new $${newLower.toFixed(2)} - $${newUpper.toFixed(2)}`);
126
132
  // Cancel all and rebuild
127
133
  actions.push({ type: "cancel_all" });
128
- gridOrders.clear();
129
134
  return [...actions, ...this.buildGridOrders(ctx, snapshot.price)];
130
135
  }
131
136
  }
@@ -144,11 +149,11 @@ export class GridStrategy {
144
149
  const step = (gridUpper - gridLower) / (params.grids - 1);
145
150
  const sizePerGrid = params.size / params.grids;
146
151
  const actions = [];
152
+ const gridLevels = [];
147
153
  // Cancel existing orders first
148
- const gridOrders = ctx.state.get("gridOrders");
149
- if (gridOrders.size > 0) {
154
+ const existingLevels = ctx.state.get("gridLevels");
155
+ if (existingLevels && existingLevels.length > 0) {
150
156
  actions.push({ type: "cancel_all" });
151
- gridOrders.clear();
152
157
  }
153
158
  for (let i = 0; i < params.grids; i++) {
154
159
  const price = gridLower + step * i;
@@ -166,9 +171,9 @@ export class GridStrategy {
166
171
  size: String(sizePerGrid),
167
172
  orderType: "limit",
168
173
  });
169
- // Mark grid index with pending sentinel -- engine updates after execution
170
- gridOrders.set(i, `pending-${i}`);
174
+ gridLevels.push({ price, side });
171
175
  }
176
+ ctx.state.set("gridLevels", gridLevels);
172
177
  ctx.log(` [GRID] Placing ${params.grids} orders (step: $${step.toFixed(2)}, size: ${sizePerGrid.toFixed(6)})`);
173
178
  return actions;
174
179
  }
@@ -1,3 +1,8 @@
1
+ export interface ExchangeBalance {
2
+ exchange: string;
3
+ equity: string;
4
+ available: string;
5
+ }
1
6
  export interface BotTuiState {
2
7
  phase: string;
3
8
  equity: number;
@@ -18,6 +23,7 @@ export interface BotTuiState {
18
23
  openOrders: OpenOrder[];
19
24
  strategyState: Record<string, unknown>;
20
25
  tick: number;
26
+ exchangeBalances?: ExchangeBalance[];
21
27
  }
22
28
  export interface Position {
23
29
  symbol: string;
@@ -26,6 +32,7 @@ export interface Position {
26
32
  entryPrice: string;
27
33
  markPrice: string;
28
34
  unrealizedPnl: string;
35
+ exchange?: string;
29
36
  }
30
37
  export interface OpenOrder {
31
38
  orderId: string;
@@ -68,9 +68,10 @@ export function BotDashboard({ initialState, onQuit, onPause, subscribe }) {
68
68
  const drawdownPct = state.peakEquity > 0 ? (drawdown / state.peakEquity) * 100 : 0;
69
69
  const bidOrders = state.openOrders.filter((o) => o.side.toLowerCase() === "buy");
70
70
  const askOrders = state.openOrders.filter((o) => o.side.toLowerCase() === "sell");
71
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.strategy, " | ", state.symbol, " | ", state.exchange] }), _jsxs(Text, { color: "gray", children: ["Runtime: ", formatDuration(state.runtime), " | Tick: ", state.tick] })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: phaseColor(state.phase), children: phaseIndicator(state.phase) }) })] }), _jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Price: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.price.toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["Vol24h: ", _jsxs(Text, { color: "white", children: [state.volatility24h.toFixed(1), "%"] })] }), _jsxs(Text, { color: "gray", children: ["FR: ", _jsxs(Text, { color: state.fundingRate >= 0 ? "green" : "red", children: [(state.fundingRate * 100).toFixed(4), "%"] })] }), _jsxs(Text, { color: "gray", children: ["OI: ", _jsx(Text, { color: "white", children: state.openInterest })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Account" }), _jsxs(Text, { children: ["Equity: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.equity.toFixed(2)] }), _jsxs(Text, { color: "gray", children: [" (peak: $", state.peakEquity.toFixed(2), ")"] })] }), _jsxs(Text, { children: ["Daily P&L: ", _jsxs(Text, { bold: true, color: pnlColor(state.dailyPnl), children: ["$", state.dailyPnl.toFixed(2)] })] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: ["Drawdown: ", _jsxs(Text, { bold: true, color: drawdown > 0 ? "red" : "green", children: ["$", drawdown.toFixed(2), " (", drawdownPct.toFixed(1), "%)"] })] }), _jsxs(Text, { children: ["Fills: ", _jsx(Text, { bold: true, color: "white", children: state.fills }), _jsx(Text, { color: "gray", children: " | Total P&L: " }), _jsxs(Text, { bold: true, color: pnlColor(state.totalPnl), children: ["$", state.totalPnl.toFixed(2)] })] })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Positions" }), state.positions.length === 0 ? (_jsx(Text, { color: "gray", children: " No open positions" })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "gray", children: pad("Symbol", 10) }), _jsx(Text, { color: "gray", children: pad("Side", 6) }), _jsx(Text, { color: "gray", children: pad("Size", 12) }), _jsx(Text, { color: "gray", children: pad("Entry", 12) }), _jsx(Text, { color: "gray", children: pad("Mark", 12) }), _jsx(Text, { color: "gray", children: "uPnL" })] }), state.positions.map((p, i) => {
71
+ const hasMultiExchange = (state.exchangeBalances?.length ?? 0) > 1;
72
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.strategy, " | ", state.symbol, " | ", state.exchange] }), _jsxs(Text, { color: "gray", children: ["Runtime: ", formatDuration(state.runtime), " | Tick: ", state.tick] })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: phaseColor(state.phase), children: phaseIndicator(state.phase) }) })] }), _jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Price: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.price.toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["Vol24h: ", _jsxs(Text, { color: "white", children: [state.volatility24h.toFixed(1), "%"] })] }), _jsxs(Text, { color: "gray", children: ["FR: ", _jsxs(Text, { color: state.fundingRate >= 0 ? "green" : "red", children: [(state.fundingRate * 100).toFixed(4), "%"] })] }), _jsxs(Text, { color: "gray", children: ["OI: ", _jsx(Text, { color: "white", children: state.openInterest })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Account" }), _jsxs(Text, { children: ["Equity: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.equity.toFixed(2)] }), _jsxs(Text, { color: "gray", children: [" (peak: $", state.peakEquity.toFixed(2), ")"] })] }), _jsxs(Text, { children: ["Daily P&L: ", _jsxs(Text, { bold: true, color: pnlColor(state.dailyPnl), children: ["$", state.dailyPnl.toFixed(2)] })] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: ["Drawdown: ", _jsxs(Text, { bold: true, color: drawdown > 0 ? "red" : "green", children: ["$", drawdown.toFixed(2), " (", drawdownPct.toFixed(1), "%)"] })] }), _jsxs(Text, { children: ["Fills: ", _jsx(Text, { bold: true, color: "white", children: state.fills }), _jsx(Text, { color: "gray", children: " | Total P&L: " }), _jsxs(Text, { bold: true, color: pnlColor(state.totalPnl), children: ["$", state.totalPnl.toFixed(2)] })] })] })] }), state.exchangeBalances && state.exchangeBalances.length > 1 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Exchange Balances" }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "gray", children: pad("Exchange", 16) }), _jsx(Text, { color: "gray", children: pad("Equity", 14) }), _jsx(Text, { color: "gray", children: "Available" })] }), state.exchangeBalances.map((eb, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "white", children: pad(eb.exchange, 16) }), _jsx(Text, { color: "white", children: pad(`$${parseFloat(eb.equity).toFixed(2)}`, 14) }), _jsxs(Text, { color: "gray", children: ["$", parseFloat(eb.available).toFixed(2)] })] }, i)))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Positions" }), state.positions.length === 0 ? (_jsx(Text, { color: "gray", children: " No open positions" })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "gray", children: pad("Exchange", 14) }), _jsx(Text, { color: "gray", children: pad("Symbol", 10) }), _jsx(Text, { color: "gray", children: pad("Side", 6) }), _jsx(Text, { color: "gray", children: pad("Size", 12) }), _jsx(Text, { color: "gray", children: pad("Entry", 12) }), _jsx(Text, { color: "gray", children: pad("Mark", 12) }), _jsx(Text, { color: "gray", children: "uPnL" })] }), state.positions.map((p, i) => {
72
73
  const upnl = parseFloat(p.unrealizedPnl);
73
- return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "white", children: pad(p.symbol, 10) }), _jsx(Text, { color: p.side.toLowerCase() === "long" ? "green" : "red", children: pad(p.side, 6) }), _jsx(Text, { color: "white", children: pad(p.size, 12) }), _jsx(Text, { color: "white", children: pad(p.entryPrice, 12) }), _jsx(Text, { color: "white", children: pad(p.markPrice, 12) }), _jsxs(Text, { color: pnlColor(upnl), children: ["$", upnl.toFixed(2)] })] }, i));
74
+ return (_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "cyan", children: pad(p.exchange ?? "—", 14) }), _jsx(Text, { color: "white", children: pad(p.symbol, 10) }), _jsx(Text, { color: p.side.toLowerCase() === "long" ? "green" : "red", children: pad(p.side, 6) }), _jsx(Text, { color: "white", children: pad(p.size, 12) }), _jsx(Text, { color: "white", children: pad(p.entryPrice, 12) }), _jsx(Text, { color: "white", children: pad(p.markPrice, 12) }), _jsxs(Text, { color: pnlColor(upnl), children: ["$", upnl.toFixed(2)] })] }, i));
74
75
  })] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "green", bold: true, children: ["Bids (", bidOrders.length, ")"] }), bidOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (bidOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "green", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] }), _jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "red", bold: true, children: ["Asks (", askOrders.length, ")"] }), askOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (askOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "red", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Strategy State" }), _jsxs(Box, { flexDirection: "row", flexWrap: "wrap", children: [Object.entries(state.strategyState).slice(0, 12).map(([k, v]) => (_jsxs(Box, { marginRight: 2, children: [_jsxs(Text, { color: "gray", children: [k, ": "] }), _jsx(Text, { color: "white", children: String(v) })] }, k))), Object.keys(state.strategyState).length === 0 && (_jsx(Text, { color: "gray", children: " --" }))] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, height: MAX_LOG_LINES + 2, children: [_jsx(Text, { color: "cyan", bold: true, children: "Log" }), logs.length === 0 ? (_jsx(Text, { color: "gray", children: " Waiting for events..." })) : (logs.map((entry, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: "gray", children: [entry.time, " "] }), _jsx(Text, { children: colorizeLog(entry.message) })] }, i))))] }), _jsx(Box, { marginTop: 1, flexDirection: "row", justifyContent: "center", children: _jsxs(Text, { color: "gray", children: ["[", _jsx(Text, { color: "white", bold: true, children: "Q" }), "]uit [", _jsx(Text, { color: "white", bold: true, children: "P" }), "]", paused ? "resume" : "ause"] }) })] }));
75
76
  }
76
77
  // ── Utility ──
@@ -1,5 +1,5 @@
1
1
  export { BotDashboard } from "./BotDashboard.js";
2
- export type { BotTuiState, Position, OpenOrder, LogEntry, StateListener, LogListener, DashboardProps } from "./BotDashboard.js";
2
+ export type { BotTuiState, ExchangeBalance, Position, OpenOrder, LogEntry, StateListener, LogListener, DashboardProps } from "./BotDashboard.js";
3
3
  import type { DashboardProps } from "./BotDashboard.js";
4
4
  export declare function startDashboard(props: DashboardProps): {
5
5
  unmount: () => void;
package/dist/cli-spec.js CHANGED
@@ -38,7 +38,7 @@ export function getCliSpec(program) {
38
38
  description: "Multi-DEX Perpetual Futures CLI (Pacifica, Hyperliquid, Lighter)",
39
39
  globalOptions,
40
40
  commands,
41
- exchanges: ["pacifica", "hyperliquid", "lighter"],
41
+ exchanges: ["pacifica", "hyperliquid", "lighter", "aster"],
42
42
  envelope: {
43
43
  success: "{ ok: true, data: T, meta: { timestamp } }",
44
44
  error: "{ ok: false, error: { code, message, retryable?, retryAfterMs? }, meta: { timestamp } }",
@@ -2,7 +2,7 @@ import { makeTable, formatUsd, formatPnl, printJson, jsonOk, jsonError, symbolMa
2
2
  import chalk from "chalk";
3
3
  import { assessRisk } from "../risk.js";
4
4
  import { hasPacificaSdk, isDexCapable } from "../exchanges/capabilities.js";
5
- const EXCHANGES = ["pacifica", "hyperliquid", "lighter"];
5
+ const EXCHANGES = ["pacifica", "hyperliquid", "lighter", "aster"];
6
6
  async function fetchExchangeSnapshot(name, getAdapter) {
7
7
  try {
8
8
  const adapter = await getAdapter(name);
@@ -26,11 +26,12 @@ function formatDuration(ms) {
26
26
  }
27
27
  async function fetchFundingRatesMap() {
28
28
  const rateMap = new Map();
29
- const [pacAssets, hlAssets, ltDetails, ltFunding] = await Promise.all([
29
+ const [pacAssets, hlAssets, ltDetails, ltFunding, asterPremiums] = await Promise.all([
30
30
  fetchPacificaPrices(),
31
31
  fetchHyperliquidMeta(),
32
32
  fetchLighterOrderBookDetails(),
33
33
  fetchLighterFundingRates(),
34
+ fetch("https://fapi.asterdex.com/fapi/v1/premiumIndex").then(r => r.json()).catch(() => []),
34
35
  ]);
35
36
  const addRate = (sym, exchange, rate, markPrice) => {
36
37
  if (!sym)
@@ -51,6 +52,13 @@ async function fetchFundingRatesMap() {
51
52
  const mp = fr.markPrice || ltPriceMap.get(fr.marketId) || 0;
52
53
  addRate(sym, "lighter", fr.rate, mp);
53
54
  }
55
+ // Aster: premiumIndex
56
+ for (const p of asterPremiums ?? []) {
57
+ const rawSym = String(p.symbol ?? "");
58
+ if (!rawSym.endsWith("USDT"))
59
+ continue;
60
+ addRate(rawSym.replace(/USDT$/, ""), "aster", Number(p.lastFundingRate ?? 0), Number(p.markPrice ?? 0));
61
+ }
54
62
  return rateMap;
55
63
  }
56
64
  function findArbEntryForPair(symbol, longExchange, shortExchange) {
@@ -454,7 +462,7 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
454
462
  // ── Spot-Perp detailed view ──
455
463
  if (spotPerpPositions.length > 0) {
456
464
  for (const sp of spotPerpPositions) {
457
- const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : "LT";
465
+ const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : e === "aster" ? "AST" : e.toUpperCase();
458
466
  const spreadStr = sp.currentSpread !== null ? `${sp.currentSpread.toFixed(1)}%` : "-";
459
467
  const spotPriceEach = sp.spotAmount > 0 ? sp.spotValueUsd / sp.spotAmount : 0;
460
468
  const spotPnl = sp.spotValueUsd - (sp.perpEntryPrice * sp.spotAmount); // spot value vs entry
@@ -1272,8 +1280,8 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
1272
1280
  const snapshots = await fetchAllBalances(adapters);
1273
1281
  const totalEquity = snapshots.reduce((s, e) => s + e.equity, 0);
1274
1282
  const totalAvailable = snapshots.reduce((s, e) => s + e.available, 0);
1275
- const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : e.toUpperCase();
1276
- const exChain = (e) => e === "pacifica" ? "Solana" : e === "hyperliquid" ? "Hyperliquid" : e === "lighter" ? "Arbitrum" : "unknown";
1283
+ const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : e === "aster" ? "AST" : e.toUpperCase();
1284
+ const exChain = (e) => e === "pacifica" ? "Solana" : e === "hyperliquid" ? "Hyperliquid" : e === "lighter" ? "Arbitrum" : e === "aster" ? "BNB" : "unknown";
1277
1285
  if (opts.check) {
1278
1286
  // Show current balance distribution
1279
1287
  if (isJson()) {
@@ -5,6 +5,7 @@ interface FundingSnapshot {
5
5
  pacRate: number;
6
6
  hlRate: number;
7
7
  ltRate: number;
8
+ astRate: number;
8
9
  spread: number;
9
10
  longExch: string;
10
11
  shortExch: string;
@@ -12,6 +13,7 @@ interface FundingSnapshot {
12
13
  pacMarkPrice: number;
13
14
  hlMarkPrice: number;
14
15
  ltMarkPrice: number;
16
+ astMarkPrice: number;
15
17
  }
16
18
  /**
17
19
  * Compute the estimated round-trip cost as a percentage of notional.
@@ -98,28 +98,41 @@ export function isNearSettlement(longExchange, shortExchange, bufferMinutes = 5,
98
98
  * meaning we're now paying on both sides instead of collecting.
99
99
  */
100
100
  export function isSpreadReversed(longExchange, shortExchange, snapshot) {
101
- const rateFor = (e) => e === "pacifica" ? snapshot.pacRate : e === "hyperliquid" ? snapshot.hlRate : snapshot.ltRate;
101
+ const rateFor = (e) => e === "pacifica" ? snapshot.pacRate : e === "hyperliquid" ? snapshot.hlRate : e === "aster" ? snapshot.astRate : snapshot.ltRate;
102
102
  const longHourly = toHourlyRate(rateFor(longExchange), longExchange);
103
103
  const shortHourly = toHourlyRate(rateFor(shortExchange), shortExchange);
104
104
  // Reversed if the long side rate exceeds the short side rate
105
105
  return longHourly > shortHourly;
106
106
  }
107
107
  async function fetchFundingSpreads() {
108
- const [pacRes, hlRes, ltDetailsRes, ltFundingRes] = await Promise.all([
108
+ const [pacRes, hlRes, ltDetailsRes, ltFundingRes, astPremiums] = await Promise.all([
109
109
  fetchPacificaPricesRaw(),
110
110
  fetchHyperliquidMetaRaw(),
111
111
  fetchLighterOrderBookDetailsRaw(),
112
112
  fetchLighterFundingRatesRaw(),
113
+ fetch("https://fapi.asterdex.com/fapi/v1/premiumIndex").then(r => r.json()).catch(() => []),
113
114
  ]);
114
115
  const { rates: pacRates, prices: pacPrices } = parsePacificaRaw(pacRes);
115
116
  const { rates: hlRates, prices: hlPrices } = parseHyperliquidMetaRaw(hlRes);
116
117
  const { rates: ltRates, prices: ltPrices } = parseLighterRaw(ltDetailsRes, ltFundingRes);
118
+ // Aster: parse premiumIndex
119
+ const astRates = new Map();
120
+ const astPrices = new Map();
121
+ for (const p of astPremiums ?? []) {
122
+ const rawSym = String(p.symbol ?? "");
123
+ if (!rawSym.endsWith("USDT"))
124
+ continue;
125
+ const sym = rawSym.replace(/USDT$/, "");
126
+ astRates.set(sym, Number(p.lastFundingRate ?? 0));
127
+ astPrices.set(sym, Number(p.markPrice ?? 0));
128
+ }
117
129
  const snapshots = [];
118
- const allSymbols = new Set([...pacRates.keys(), ...hlRates.keys(), ...ltRates.keys()]);
130
+ const allSymbols = new Set([...pacRates.keys(), ...hlRates.keys(), ...ltRates.keys(), ...astRates.keys()]);
119
131
  for (const sym of allSymbols) {
120
132
  const pac = pacRates.get(sym);
121
133
  const hl = hlRates.get(sym);
122
134
  const lt = ltRates.get(sym);
135
+ const ast = astRates.get(sym);
123
136
  // Need at least 2 exchanges
124
137
  const available = [];
125
138
  if (pac !== undefined)
@@ -128,6 +141,8 @@ async function fetchFundingSpreads() {
128
141
  available.push({ exchange: "hyperliquid", rate: hl });
129
142
  if (lt !== undefined)
130
143
  available.push({ exchange: "lighter", rate: lt });
144
+ if (ast !== undefined)
145
+ available.push({ exchange: "aster", rate: ast });
131
146
  if (available.length < 2)
132
147
  continue;
133
148
  const norm = (r) => toHourlyRate(r.rate, r.exchange);
@@ -135,13 +150,14 @@ async function fetchFundingSpreads() {
135
150
  const lowest = available[0];
136
151
  const highest = available[available.length - 1];
137
152
  const spread = computeAnnualSpread(highest.rate, highest.exchange, lowest.rate, lowest.exchange);
138
- // Use best available mark price (prefer HL as most liquid, then PAC, then LT)
139
- const markPrice = hlPrices.get(sym) ?? pacPrices.get(sym) ?? ltPrices.get(sym) ?? 0;
153
+ // Use best available mark price (prefer HL as most liquid, then PAC, then LT, then AST)
154
+ const markPrice = hlPrices.get(sym) ?? pacPrices.get(sym) ?? ltPrices.get(sym) ?? astPrices.get(sym) ?? 0;
140
155
  snapshots.push({
141
156
  symbol: sym,
142
157
  pacRate: pac ?? 0,
143
158
  hlRate: hl ?? 0,
144
159
  ltRate: lt ?? 0,
160
+ astRate: ast ?? 0,
145
161
  spread,
146
162
  longExch: lowest.exchange, // long where funding is lowest
147
163
  shortExch: highest.exchange, // short where funding is highest
@@ -149,6 +165,7 @@ async function fetchFundingSpreads() {
149
165
  pacMarkPrice: pacPrices.get(sym) ?? 0,
150
166
  hlMarkPrice: hlPrices.get(sym) ?? 0,
151
167
  ltMarkPrice: ltPrices.get(sym) ?? 0,
168
+ astMarkPrice: astPrices.get(sym) ?? 0,
152
169
  });
153
170
  }
154
171
  return snapshots.sort((a, b) => Math.abs(b.spread) - Math.abs(a.spread));
@@ -541,7 +558,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
541
558
  // Log next settlement times and funding estimation
542
559
  if (!isJson() && settleStrategy !== "off") {
543
560
  const nowDate = new Date();
544
- const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : "LT";
561
+ const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : e === "aster" ? "AST" : e.toUpperCase();
545
562
  const nextHL = getNextSettlement("hyperliquid", nowDate);
546
563
  const nextPAC = getNextSettlement("pacifica", nowDate);
547
564
  const nextLT = getNextSettlement("lighter", nowDate);
@@ -553,7 +570,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
553
570
  const fSnap = filtered.find(s => s.symbol === fPos.symbol);
554
571
  if (!fSnap)
555
572
  continue;
556
- const fRateFor = (e) => e === "pacifica" ? fSnap.pacRate : e === "hyperliquid" ? fSnap.hlRate : fSnap.ltRate;
573
+ const fRateFor = (e) => e === "pacifica" ? fSnap.pacRate : e === "hyperliquid" ? fSnap.hlRate : e === "aster" ? fSnap.astRate : fSnap.ltRate;
557
574
  const fHlHourly = toHourlyRate(fRateFor("hyperliquid"), "hyperliquid");
558
575
  const fPacHourly = toHourlyRate(fRateFor("pacifica"), "pacifica");
559
576
  const fNotional = fPos.size * fSnap.markPrice;
@@ -679,7 +696,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
679
696
  const effectiveNetSpread = netSpread * settleBoostMultiplier;
680
697
  if (effectiveNetSpread < minSpread)
681
698
  continue;
682
- const rateForExch = (e) => e === "pacifica" ? snap.pacRate : e === "hyperliquid" ? snap.hlRate : snap.ltRate;
699
+ const rateForExch = (e) => e === "pacifica" ? snap.pacRate : e === "hyperliquid" ? snap.hlRate : e === "aster" ? snap.astRate : snap.ltRate;
683
700
  console.log(chalk.green(` ${now} ENTER ${snap.symbol} — gross ${grossSpread.toFixed(1)}% net ${netSpread.toFixed(1)}%` +
684
701
  ` | Long ${longExchange} (${(rateForExch(longExchange) * 100).toFixed(4)}%)` +
685
702
  ` | Short ${shortExchange} (${(rateForExch(shortExchange) * 100).toFixed(4)}%)`));
@@ -885,7 +902,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
885
902
  const elapsedHours = (nowMs - pos.lastCheckTime) / (1000 * 60 * 60);
886
903
  const notional = pos.size * current.markPrice;
887
904
  // Estimate funding collected: normalize all rates to hourly before comparing
888
- const rateFor = (e) => e === "pacifica" ? current.pacRate : e === "hyperliquid" ? current.hlRate : current.ltRate;
905
+ const rateFor = (e) => e === "pacifica" ? current.pacRate : e === "hyperliquid" ? current.hlRate : e === "aster" ? current.astRate : current.ltRate;
889
906
  const longHourly = toHourlyRate(rateFor(pos.longExchange), pos.longExchange);
890
907
  const shortHourly = toHourlyRate(rateFor(pos.shortExchange), pos.shortExchange);
891
908
  // Income = short gets paid positive funding, long pays; net = (shortRate - longRate) * notional * hours
@@ -1074,7 +1091,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1074
1091
  console.log(chalk.gray(` Liquidity check: ON`));
1075
1092
  console.log(chalk.gray(` Press Ctrl+C to stop\n`));
1076
1093
  }
1077
- const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : "LT";
1094
+ const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : e === "aster" ? "AST" : e.toUpperCase();
1078
1095
  while (true) {
1079
1096
  cycle++;
1080
1097
  const ts = new Date().toLocaleTimeString();
@@ -1144,7 +1161,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1144
1161
  if (opts.compare)
1145
1162
  return handleCompare(isJson, opts.compare);
1146
1163
  if (opts.rates)
1147
- return handleRates(isJson, { symbol: opts.symbol, symbols: opts.symbols, all: opts.all, minSpread: opts.min });
1164
+ return handleRates(isJson, { symbol: opts.symbol, symbols: opts.symbols, all: opts.all, minSpread: opts.min, top: opts.top });
1148
1165
  if (opts.basis)
1149
1166
  return handleBasisScan(isJson, { minBasis: opts.min, symbol: typeof opts.basis === "string" ? opts.basis : opts.symbol });
1150
1167
  if (opts.hip3)
@@ -1191,7 +1208,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1191
1208
  console.log(chalk.gray(` No spot+perp opportunities above ${minSpread}%\n`));
1192
1209
  }
1193
1210
  else {
1194
- const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : "LT";
1211
+ const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : e === "aster" ? "AST" : e.toUpperCase();
1195
1212
  const rows = spotSpreads.slice(0, parseInt(opts.top ?? "30")).map(s => {
1196
1213
  const spotEx = s.spotExchanges[0] || "lighter";
1197
1214
  const rtCost = computeRoundTripCostPct(spotEx, s.perpExchange);
@@ -1226,15 +1243,18 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1226
1243
  if (!isJson())
1227
1244
  console.log(chalk.cyan(" Scanning perp-perp funding rate spreads...\n"));
1228
1245
  const spreads = await fetchFundingSpreads();
1229
- const filtered = spreads.filter(s => Math.abs(s.spread) >= minSpread);
1246
+ let filtered = spreads.filter(s => Math.abs(s.spread) >= minSpread);
1247
+ if (opts.top)
1248
+ filtered = filtered.slice(0, parseInt(opts.top));
1230
1249
  if (isJson()) {
1231
1250
  const perpPerp = filtered.map(s => {
1232
1251
  const grossSpread = Math.abs(s.spread);
1233
1252
  const rtCost = computeRoundTripCostPct(s.longExch, s.shortExch);
1234
1253
  const net = computeNetSpread(grossSpread, holdDays, rtCost, bridgeCostUsd, sizeUsd);
1235
1254
  // Compute price gap between long and short exchanges
1236
- const longPrice = s.longExch === "pacifica" ? s.pacMarkPrice : s.longExch === "hyperliquid" ? s.hlMarkPrice : s.ltMarkPrice;
1237
- const shortPrice = s.shortExch === "pacifica" ? s.pacMarkPrice : s.shortExch === "hyperliquid" ? s.hlMarkPrice : s.ltMarkPrice;
1255
+ const pFor = (ex) => ex === "pacifica" ? s.pacMarkPrice : ex === "hyperliquid" ? s.hlMarkPrice : ex === "lighter" ? s.ltMarkPrice : s.astMarkPrice;
1256
+ const longPrice = pFor(s.longExch);
1257
+ const shortPrice = pFor(s.shortExch);
1238
1258
  const mid = (longPrice + shortPrice) / 2;
1239
1259
  const priceGapPct = mid > 0 ? Math.abs(longPrice - shortPrice) / mid * 100 : 0;
1240
1260
  return { mode: "perp-perp", symbol: s.symbol, longExch: s.longExch, shortExch: s.shortExch, markPrice: s.markPrice, grossSpread, netSpread: net, estFeesPct: rtCost, priceGapPct: Math.round(priceGapPct * 10000) / 10000 };
@@ -1262,7 +1282,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1262
1282
  console.log(chalk.gray(` No perp-perp spreads above ${minSpread}%\n`));
1263
1283
  return;
1264
1284
  }
1265
- const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : "LT";
1285
+ const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : e === "aster" ? "AST" : e.toUpperCase();
1266
1286
  const rows = filtered.map(s => {
1267
1287
  const direction = `${exAbbr(s.shortExch)}>${exAbbr(s.longExch)}`;
1268
1288
  const grossSpread = Math.abs(s.spread);
@@ -1270,8 +1290,9 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1270
1290
  const netSpread = computeNetSpread(grossSpread, holdDays, rtCost, bridgeCostUsd, sizeUsd);
1271
1291
  const grossColor = grossSpread >= 50 ? chalk.green.bold : grossSpread >= 30 ? chalk.green : chalk.yellow;
1272
1292
  const netColor = netSpread >= 20 ? chalk.green : netSpread >= 0 ? chalk.yellow : chalk.red;
1273
- const longPrice = s.longExch === "pacifica" ? s.pacMarkPrice : s.longExch === "hyperliquid" ? s.hlMarkPrice : s.ltMarkPrice;
1274
- const shortPrice = s.shortExch === "pacifica" ? s.pacMarkPrice : s.shortExch === "hyperliquid" ? s.hlMarkPrice : s.ltMarkPrice;
1293
+ const priceFor = (ex) => ex === "pacifica" ? s.pacMarkPrice : ex === "hyperliquid" ? s.hlMarkPrice : ex === "lighter" ? s.ltMarkPrice : s.astMarkPrice;
1294
+ const longPrice = priceFor(s.longExch);
1295
+ const shortPrice = priceFor(s.shortExch);
1275
1296
  const mid = (longPrice + shortPrice) / 2;
1276
1297
  const priceGapPct = mid > 0 ? Math.abs(longPrice - shortPrice) / mid * 100 : 0;
1277
1298
  const gapColor = priceGapPct >= 0.5 ? chalk.red : priceGapPct >= 0.1 ? chalk.yellow : chalk.gray;
@@ -1282,6 +1303,8 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1282
1303
  rates.push(`HL:${(s.hlRate * 100).toFixed(4)}%`);
1283
1304
  if (s.ltRate)
1284
1305
  rates.push(`LT:${(s.ltRate * 100).toFixed(4)}%`);
1306
+ if (s.astRate)
1307
+ rates.push(`AST:${(s.astRate * 100).toFixed(4)}%`);
1285
1308
  return [
1286
1309
  chalk.white.bold(s.symbol),
1287
1310
  grossColor(`${grossSpread.toFixed(1)}%`),
@@ -25,6 +25,7 @@ export declare function handleRates(isJson: () => boolean, opts: {
25
25
  symbols?: string;
26
26
  all?: boolean;
27
27
  minSpread: string;
28
+ top?: string;
28
29
  }): Promise<void>;
29
30
  export declare function handleBasisScan(isJson: () => boolean, opts: {
30
31
  minBasis: string;
@@ -24,6 +24,23 @@ async function fetchHyperliquidRates() {
24
24
  markPrice: a.markPx,
25
25
  }));
26
26
  }
27
+ async function fetchAsterRates() {
28
+ try {
29
+ const premiums = await fetch("https://fapi.asterdex.com/fapi/v1/premiumIndex").then(r => r.json());
30
+ return (premiums ?? [])
31
+ .filter(p => String(p.symbol ?? "").endsWith("USDT"))
32
+ .map(p => ({
33
+ exchange: "aster",
34
+ symbol: String(p.symbol ?? "").replace(/USDT$/, ""),
35
+ fundingRate: Number(p.lastFundingRate ?? 0),
36
+ markPrice: Number(p.markPrice ?? 0),
37
+ nextFunding: Number(p.nextFundingTime ?? 0),
38
+ }));
39
+ }
40
+ catch {
41
+ return [];
42
+ }
43
+ }
27
44
  async function fetchLighterRates() {
28
45
  try {
29
46
  const [details, funding] = await Promise.all([
@@ -222,18 +239,20 @@ function printSnapshotTable(snapshot) {
222
239
  const pacRate = s.rates.find(r => r.exchange === "pacifica");
223
240
  const hlRate = s.rates.find(r => r.exchange === "hyperliquid");
224
241
  const ltRate = s.rates.find(r => r.exchange === "lighter");
242
+ const astRate = s.rates.find(r => r.exchange === "aster");
225
243
  const spreadColor = s.maxSpreadAnnual >= 30 ? chalk.green : s.maxSpreadAnnual >= 10 ? chalk.yellow : chalk.white;
226
- const bestRate = hlRate ?? pacRate ?? ltRate;
244
+ const bestRate = hlRate ?? pacRate ?? ltRate ?? astRate;
227
245
  const trend = trendMap.get(s.symbol) ?? chalk.gray("-");
228
246
  return [
229
247
  chalk.white.bold(s.symbol), pacRate ? formatPercent(pacRate.fundingRate) : chalk.gray("-"),
230
248
  hlRate ? formatPercent(hlRate.fundingRate) : chalk.gray("-"), ltRate ? formatPercent(ltRate.fundingRate) : chalk.gray("-"),
249
+ astRate ? formatPercent(astRate.fundingRate) : chalk.gray("-"),
231
250
  spreadColor(`${s.maxSpreadAnnual.toFixed(1)}%`), trend,
232
251
  s.maxSpreadAnnual >= 5 ? `${exAbbr(s.shortExchange)}>${exAbbr(s.longExchange)}` : chalk.gray("-"),
233
252
  formatAvgRate(bestRate?.historicalAvg?.avg8h), formatAvgRate(bestRate?.historicalAvg?.avg24h), formatAvgRate(bestRate?.historicalAvg?.avg7d),
234
253
  ];
235
254
  });
236
- console.log(makeTable(["Symbol", "Pacifica", "Hyperliquid", "Lighter", "Spread", "24h", "Direction", "Avg 8h", "Avg 24h", "Avg 7d"], rows));
255
+ console.log(makeTable(["Symbol", "Pacifica", "Hyperliquid", "Lighter", "Aster", "Spread", "24h", "Direction", "Avg 8h", "Avg 24h", "Avg 7d"], rows));
237
256
  console.log(chalk.gray(`\n ${snapshot.symbols.length} symbols compared across exchanges.\n`));
238
257
  }
239
258
  function printFundingExchangeStatus(snapshot) {
@@ -278,6 +297,8 @@ export async function handleRates(isJson, opts) {
278
297
  saveFundingSnapshot(allRates);
279
298
  }
280
299
  catch { /* non-critical */ }
300
+ if (opts.top)
301
+ snapshot.symbols = snapshot.symbols.slice(0, parseInt(opts.top));
281
302
  if (isJson()) {
282
303
  // Enrich with 24h rate history for each symbol
283
304
  const now = new Date();
@@ -299,10 +320,10 @@ export async function handleRates(isJson, opts) {
299
320
  export async function handleBasisScan(isJson, opts) {
300
321
  if (!isJson())
301
322
  console.log(chalk.cyan(" Fetching prices for basis calculation...\n"));
302
- const [pacRates, hlRates, ltRates] = await Promise.all([fetchPacificaRates(), fetchHyperliquidRates(), fetchLighterRates()]);
323
+ const [pacRates, hlRates, ltRates, astRates] = await Promise.all([fetchPacificaRates(), fetchHyperliquidRates(), fetchLighterRates(), fetchAsterRates()]);
303
324
  const exchangePrices = new Map();
304
325
  const filterSymbol = opts.symbol?.toUpperCase();
305
- for (const r of [...pacRates, ...hlRates, ...ltRates]) {
326
+ for (const r of [...pacRates, ...hlRates, ...ltRates, ...astRates]) {
306
327
  if (r.markPrice <= 0)
307
328
  continue;
308
329
  if (filterSymbol && r.symbol.toUpperCase() !== filterSymbol)
@@ -506,7 +527,7 @@ export async function handleFundingPositions(isJson, getAdapterForExchange, opts
506
527
  const tA = results.reduce((s, r) => s + r.actualNet24h, 0);
507
528
  if (isJson())
508
529
  return printJson(jsonOk({ positions: results.map(r => ({ exchange: r.exchange, symbol: r.symbol, side: r.side, size: r.size, notionalUsd: +r.notionalUsd.toFixed(2), fundingRate: r.fundingRate, annualPct: +r.annualPct.toFixed(2), predicted: { hourly: +r.hourlyPayment.toFixed(6), daily: +r.dailyPayment.toFixed(4) }, actual24h: { received: +r.actualReceived24h.toFixed(6), paid: +r.actualPaid24h.toFixed(6), net: +r.actualNet24h.toFixed(6) }, unrealizedPnl: r.unrealizedPnl })), totals: { predicted: { hourly: +tH.toFixed(6), daily: +tD.toFixed(4) }, actual24h: { net: +tA.toFixed(6) }, notionalUsd: +tN.toFixed(2) }, errors: Object.keys(exchangeErrors).length > 0 ? exchangeErrors : undefined }));
509
- const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : e.toUpperCase();
530
+ const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : e === "aster" ? "AST" : e.toUpperCase();
510
531
  console.log(chalk.cyan.bold(" Position Funding Impact\n"));
511
532
  const rows = results.map(r => { const sc = r.side === "long" ? chalk.green : chalk.red; const rc = r.fundingRate > 0 ? chalk.red : r.fundingRate < 0 ? chalk.green : chalk.white; const as = r.actualNet24h !== 0 ? (r.actualNet24h > 0 ? chalk.green : chalk.red)(`${r.actualNet24h > 0 ? "+" : ""}$${r.actualNet24h.toFixed(4)}`) : chalk.gray("-"); return [chalk.white.bold(exAbbr(r.exchange)), chalk.white.bold(r.symbol), sc(r.side.toUpperCase()), `$${formatUsd(r.notionalUsd)}`, rc(formatPercent(r.fundingRate)), rc(`${r.annualPct.toFixed(1)}%`), as, formatPnl(r.unrealizedPnl)]; });
512
533
  console.log(makeTable(["Ex", "Symbol", "Side", "Notional", "Rate(now)", "Annual(now)", "Actual 24h", "uPnL"], rows));