openbroker 1.0.55 → 1.0.57
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 +24 -0
- package/SKILL.md +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/core/client.ts +130 -17
- package/scripts/info/account.ts +98 -40
- package/scripts/info/fills.ts +17 -3
- package/scripts/info/funding.ts +9 -3
- package/scripts/info/markets.ts +9 -3
- package/scripts/info/orders.ts +18 -3
- package/scripts/info/positions.ts +42 -7
- package/scripts/plugin/tools.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Open Broker will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.0.57] - 2026-03-10
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Unified Account $0 Equity**: Made account mode detection more robust — handles wrapped API responses, case-insensitive matching. Added debug logging for spot balance lookup to diagnose edge cases. Plugin `ob_account` now returns `accountMode` field.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **`--json` Flag**: Info commands now support `--json` for machine-readable output
|
|
12
|
+
- `account --json` — full account state as JSON
|
|
13
|
+
- `positions --json` — positions array with mark prices and liquidation distances
|
|
14
|
+
- `funding --json` — funding rate data
|
|
15
|
+
- `fills --json` — trade fill history
|
|
16
|
+
- `orders --json` — order history
|
|
17
|
+
- `markets --json` — market data
|
|
18
|
+
|
|
19
|
+
## [1.0.56] - 2026-03-10
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **Unified Account Support**: Detects and handles Hyperliquid's account abstraction modes (standard, unified, portfolio margin, dex abstraction)
|
|
23
|
+
- `getAccountMode()` / `isUnifiedAccount()` — queries `userAbstraction` API to detect mode
|
|
24
|
+
- **Unified accounts**: equity comes from `spotClearinghouseState` (single USDC balance shared across all dexes); skips `sendAsset` transfers for HIP-3 trading
|
|
25
|
+
- **Standard accounts**: keeps existing behavior (per-dex margin aggregation + USDC transfers)
|
|
26
|
+
- `account` command now displays the account abstraction mode
|
|
27
|
+
- Prepares for deprecation of DEX abstraction mode per Hyperliquid docs
|
|
28
|
+
|
|
5
29
|
## [1.0.55] - 2026-03-09
|
|
6
30
|
|
|
7
31
|
### Added
|
package/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: Hyperliquid trading plugin with background position monitoring. Exe
|
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Requires Node.js 22+, network access to api.hyperliquid.xyz
|
|
6
6
|
homepage: https://www.npmjs.com/package/openbroker
|
|
7
|
-
metadata: {"author": "monemetrics", "version": "1.0.
|
|
7
|
+
metadata: {"author": "monemetrics", "version": "1.0.57", "openclaw": {"requires": {"bins": ["openbroker"], "env": ["HYPERLIQUID_PRIVATE_KEY"]}, "primaryEnv": "HYPERLIQUID_PRIVATE_KEY", "install": [{"id": "node", "kind": "node", "package": "openbroker", "bins": ["openbroker"], "label": "Install openbroker (npm)"}]}}
|
|
8
8
|
allowed-tools: ob_account ob_positions ob_funding ob_markets ob_search ob_spot ob_fills ob_orders ob_order_status ob_fees ob_candles ob_funding_history ob_trades ob_rate_limit ob_funding_scan ob_buy ob_sell ob_limit ob_trigger ob_tpsl ob_cancel ob_twap ob_bracket ob_chase ob_watcher_status Bash(openbroker:*)
|
|
9
9
|
---
|
|
10
10
|
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/scripts/core/client.ts
CHANGED
|
@@ -35,6 +35,8 @@ export class HyperliquidClient {
|
|
|
35
35
|
private hip3IsolatedSet: Set<string> = new Set();
|
|
36
36
|
/** Cached maxLeverage for HIP-3 assets */
|
|
37
37
|
private hip3MaxLeverageMap: Map<string, number> = new Map();
|
|
38
|
+
/** Cached account abstraction mode: 'standard' | 'unified' | 'portfolio' | 'dexAbstraction' */
|
|
39
|
+
private accountMode: string | null = null;
|
|
38
40
|
public verbose: boolean = false;
|
|
39
41
|
|
|
40
42
|
constructor(config?: OpenBrokerConfig) {
|
|
@@ -631,6 +633,60 @@ export class HyperliquidClient {
|
|
|
631
633
|
|
|
632
634
|
// ============ Account Info ============
|
|
633
635
|
|
|
636
|
+
/**
|
|
637
|
+
* Get the account's abstraction mode.
|
|
638
|
+
* Returns: 'standard' | 'unified' | 'portfolio' | 'dexAbstraction'
|
|
639
|
+
* Unified accounts have a single USDC balance shared across all dexes.
|
|
640
|
+
* Standard accounts have separate balances per dex (need sendAsset transfers).
|
|
641
|
+
*/
|
|
642
|
+
async getAccountMode(user?: string): Promise<string> {
|
|
643
|
+
if (this.accountMode) return this.accountMode;
|
|
644
|
+
|
|
645
|
+
const baseUrl = isMainnet()
|
|
646
|
+
? 'https://api.hyperliquid.xyz'
|
|
647
|
+
: 'https://api.hyperliquid-testnet.xyz';
|
|
648
|
+
|
|
649
|
+
try {
|
|
650
|
+
const response = await fetch(baseUrl + '/info', {
|
|
651
|
+
method: 'POST',
|
|
652
|
+
headers: { 'Content-Type': 'application/json' },
|
|
653
|
+
body: JSON.stringify({
|
|
654
|
+
type: 'userAbstraction',
|
|
655
|
+
user: user ?? this.address,
|
|
656
|
+
}),
|
|
657
|
+
});
|
|
658
|
+
const data = await response.json();
|
|
659
|
+
this.log('userAbstraction response:', JSON.stringify(data));
|
|
660
|
+
|
|
661
|
+
// API may return a bare string or an object. Normalize to string for matching.
|
|
662
|
+
const mode = typeof data === 'string' ? data : (data?.abstraction ?? data?.mode ?? String(data));
|
|
663
|
+
const modeLower = mode.toLowerCase();
|
|
664
|
+
|
|
665
|
+
if (modeLower.includes('unified')) {
|
|
666
|
+
this.accountMode = 'unified';
|
|
667
|
+
} else if (modeLower.includes('portfolio')) {
|
|
668
|
+
this.accountMode = 'portfolio';
|
|
669
|
+
} else if (modeLower.includes('dex')) {
|
|
670
|
+
this.accountMode = 'dexAbstraction';
|
|
671
|
+
} else {
|
|
672
|
+
// "default" or "disabled" both mean standard mode
|
|
673
|
+
this.accountMode = 'standard';
|
|
674
|
+
}
|
|
675
|
+
} catch (err) {
|
|
676
|
+
this.log('Failed to fetch account abstraction mode:', err instanceof Error ? err.message : String(err));
|
|
677
|
+
this.accountMode = 'standard'; // Safe fallback
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
this.log('Account mode:', this.accountMode);
|
|
681
|
+
return this.accountMode;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/** Whether the account uses unified balances (unified or portfolio margin) */
|
|
685
|
+
async isUnifiedAccount(user?: string): Promise<boolean> {
|
|
686
|
+
const mode = await this.getAccountMode(user);
|
|
687
|
+
return mode === 'unified' || mode === 'portfolio';
|
|
688
|
+
}
|
|
689
|
+
|
|
634
690
|
/**
|
|
635
691
|
* Check if an address has sub-accounts (is a master account)
|
|
636
692
|
* Sub-accounts cannot approve builder fees - only master accounts can
|
|
@@ -1075,14 +1131,17 @@ export class HyperliquidClient {
|
|
|
1075
1131
|
|
|
1076
1132
|
/**
|
|
1077
1133
|
* Get user state across all dexes (main + HIP-3).
|
|
1078
|
-
*
|
|
1134
|
+
* For unified accounts: equity comes from spotClearinghouseState (single USDC balance).
|
|
1135
|
+
* For standard accounts: aggregates margin summaries from each dex.
|
|
1079
1136
|
*/
|
|
1080
1137
|
async getUserStateAll(user?: string): Promise<ClearinghouseState> {
|
|
1081
1138
|
await this.getMetaAndAssetCtxs(); // Ensure HIP-3 dex list is loaded
|
|
1082
1139
|
|
|
1140
|
+
const unified = await this.isUnifiedAccount(user);
|
|
1083
1141
|
const mainState = await this.getUserState(user);
|
|
1084
1142
|
const dexs = await this.getPerpDexs();
|
|
1085
1143
|
|
|
1144
|
+
// Collect positions from all HIP-3 dexes
|
|
1086
1145
|
for (let i = 1; i < dexs.length; i++) {
|
|
1087
1146
|
const dex = dexs[i];
|
|
1088
1147
|
if (!dex) continue;
|
|
@@ -1093,24 +1152,70 @@ export class HyperliquidClient {
|
|
|
1093
1152
|
mainState.assetPositions.push(...dexState.assetPositions);
|
|
1094
1153
|
}
|
|
1095
1154
|
|
|
1096
|
-
//
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1155
|
+
// For standard accounts, aggregate margin from each dex
|
|
1156
|
+
if (!unified) {
|
|
1157
|
+
const dexMargin = dexState.marginSummary;
|
|
1158
|
+
if (dexMargin) {
|
|
1159
|
+
const addToSummary = (summary: { accountValue: string; totalNtlPos: string; totalRawUsd: string; totalMarginUsed: string; withdrawable: string }) => {
|
|
1160
|
+
summary.accountValue = String(parseFloat(summary.accountValue) + parseFloat(dexMargin.accountValue));
|
|
1161
|
+
summary.totalNtlPos = String(parseFloat(summary.totalNtlPos) + parseFloat(dexMargin.totalNtlPos));
|
|
1162
|
+
summary.totalRawUsd = String(parseFloat(summary.totalRawUsd) + parseFloat(dexMargin.totalRawUsd));
|
|
1163
|
+
summary.totalMarginUsed = String(parseFloat(summary.totalMarginUsed) + parseFloat(dexMargin.totalMarginUsed));
|
|
1164
|
+
summary.withdrawable = String(parseFloat(summary.withdrawable) + parseFloat(dexMargin.withdrawable));
|
|
1165
|
+
};
|
|
1166
|
+
addToSummary(mainState.marginSummary);
|
|
1167
|
+
addToSummary(mainState.crossMarginSummary);
|
|
1168
|
+
}
|
|
1108
1169
|
}
|
|
1109
1170
|
} catch (err) {
|
|
1110
1171
|
this.log(`Failed to fetch state for dex ${dex.name}:`, err instanceof Error ? err.message : String(err));
|
|
1111
1172
|
}
|
|
1112
1173
|
}
|
|
1113
1174
|
|
|
1175
|
+
// For unified accounts: equity is the USDC balance from spot clearinghouse
|
|
1176
|
+
if (unified) {
|
|
1177
|
+
try {
|
|
1178
|
+
const spotState = await this.getSpotBalances(user);
|
|
1179
|
+
this.log('Unified spot balances:', JSON.stringify(spotState));
|
|
1180
|
+
|
|
1181
|
+
// Find USDC balance (case-insensitive, handles variations)
|
|
1182
|
+
const balances = spotState?.balances ?? [];
|
|
1183
|
+
const usdcBalance = balances.find(b => b.coin?.toUpperCase() === 'USDC');
|
|
1184
|
+
|
|
1185
|
+
if (usdcBalance) {
|
|
1186
|
+
const totalUsdc = usdcBalance.total;
|
|
1187
|
+
const holdUsdc = usdcBalance.hold;
|
|
1188
|
+
const withdrawable = String(parseFloat(totalUsdc) - parseFloat(holdUsdc));
|
|
1189
|
+
|
|
1190
|
+
// Compute total margin used and notional from all positions
|
|
1191
|
+
let totalMarginUsed = 0;
|
|
1192
|
+
let totalNtlPos = 0;
|
|
1193
|
+
for (const ap of mainState.assetPositions) {
|
|
1194
|
+
const pos = ap.position;
|
|
1195
|
+
if (parseFloat(pos.szi) === 0) continue;
|
|
1196
|
+
totalMarginUsed += parseFloat(pos.marginUsed);
|
|
1197
|
+
totalNtlPos += Math.abs(parseFloat(pos.positionValue));
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
const summary = {
|
|
1201
|
+
accountValue: totalUsdc,
|
|
1202
|
+
totalNtlPos: String(totalNtlPos),
|
|
1203
|
+
totalRawUsd: totalUsdc,
|
|
1204
|
+
totalMarginUsed: String(totalMarginUsed),
|
|
1205
|
+
withdrawable,
|
|
1206
|
+
};
|
|
1207
|
+
mainState.marginSummary = summary;
|
|
1208
|
+
mainState.crossMarginSummary = { ...summary };
|
|
1209
|
+
|
|
1210
|
+
this.log(`Unified account: USDC balance $${parseFloat(totalUsdc).toFixed(2)}, margin used $${totalMarginUsed.toFixed(2)}`);
|
|
1211
|
+
} else {
|
|
1212
|
+
this.log('Unified account: no USDC balance found in spot state. Balances:', balances.map(b => b.coin));
|
|
1213
|
+
}
|
|
1214
|
+
} catch (err) {
|
|
1215
|
+
this.log('Failed to fetch spot balances for unified account:', err instanceof Error ? err.message : String(err));
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1114
1219
|
return mainState;
|
|
1115
1220
|
}
|
|
1116
1221
|
|
|
@@ -1123,9 +1228,10 @@ export class HyperliquidClient {
|
|
|
1123
1228
|
// ============ Trading ============
|
|
1124
1229
|
|
|
1125
1230
|
/**
|
|
1126
|
-
* HIP-3 perps
|
|
1231
|
+
* HIP-3 perps: prepare for trading.
|
|
1127
1232
|
* 1. Set isolated margin mode (required for HIP-3)
|
|
1128
|
-
* 2.
|
|
1233
|
+
* 2. For standard accounts only: transfer USDC from main perp to HIP-3 dex
|
|
1234
|
+
* (unified accounts share USDC across all dexes automatically)
|
|
1129
1235
|
*/
|
|
1130
1236
|
private async ensureHip3Ready(coin: string, notional: number, leverage?: number): Promise<void> {
|
|
1131
1237
|
if (!this.isHip3(coin)) return;
|
|
@@ -1148,12 +1254,19 @@ export class HyperliquidClient {
|
|
|
1148
1254
|
}
|
|
1149
1255
|
}
|
|
1150
1256
|
|
|
1151
|
-
//
|
|
1257
|
+
// Unified accounts share USDC across all dexes — no transfer needed
|
|
1258
|
+
const unified = await this.isUnifiedAccount();
|
|
1259
|
+
if (unified) {
|
|
1260
|
+
this.log(`Unified account — skipping USDC transfer for ${coin} (shared balance)`);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Standard accounts: transfer USDC to the HIP-3 dex to cover margin
|
|
1152
1265
|
const requiredMargin = notional / effectiveLev;
|
|
1153
1266
|
// Add 20% buffer for fees and slippage
|
|
1154
1267
|
const transferAmount = Math.ceil(requiredMargin * 1.2 * 100) / 100;
|
|
1155
1268
|
|
|
1156
|
-
this.log(`HIP-3 margin transfer: ${transferAmount} USDC from main → ${dexInfo.dexName} (notional: ${notional}, leverage: ${
|
|
1269
|
+
this.log(`HIP-3 margin transfer: ${transferAmount} USDC from main → ${dexInfo.dexName} (notional: ${notional}, leverage: ${effectiveLev}x)`);
|
|
1157
1270
|
try {
|
|
1158
1271
|
await this.exchange.sendAsset({
|
|
1159
1272
|
destination: this.address as `0x${string}`,
|
package/scripts/info/account.ts
CHANGED
|
@@ -6,28 +6,14 @@ import { formatUsd, formatPercent, parseArgs } from '../core/utils.js';
|
|
|
6
6
|
|
|
7
7
|
async function main() {
|
|
8
8
|
const args = parseArgs(process.argv.slice(2));
|
|
9
|
+
const jsonOutput = args.json as boolean;
|
|
9
10
|
const client = getClient();
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
console.log('Wallet Configuration');
|
|
15
|
-
console.log('--------------------');
|
|
16
|
-
console.log(`Trading Account: ${client.address}`);
|
|
17
|
-
console.log(`Signing Wallet: ${client.walletAddress}`);
|
|
18
|
-
console.log(`Wallet Type: ${client.isApiWallet ? 'API Wallet' : 'Main Wallet'}`);
|
|
19
|
-
|
|
20
|
-
// Check builder fee approval
|
|
21
|
-
const builderApproval = await client.getMaxBuilderFee();
|
|
22
|
-
console.log(`Builder Address: ${client.builderAddress}`);
|
|
23
|
-
console.log(`Builder Fee: ${client.builderFeeBps} bps`);
|
|
24
|
-
if (builderApproval) {
|
|
25
|
-
console.log(`Builder Approved: ✅ Yes (max: ${builderApproval})`);
|
|
26
|
-
} else {
|
|
27
|
-
console.log(`Builder Approved: ❌ No`);
|
|
28
|
-
console.log(`\n⚠️ Run: npx tsx scripts/setup/approve-builder.ts`);
|
|
12
|
+
if (args.verbose) {
|
|
13
|
+
client.verbose = true;
|
|
29
14
|
}
|
|
30
|
-
|
|
15
|
+
|
|
16
|
+
const accountMode = await client.getAccountMode();
|
|
31
17
|
|
|
32
18
|
try {
|
|
33
19
|
const state = await client.getUserStateAll();
|
|
@@ -38,6 +24,93 @@ async function main() {
|
|
|
38
24
|
const withdrawable = parseFloat(margin.withdrawable);
|
|
39
25
|
const totalNotional = parseFloat(margin.totalNtlPos);
|
|
40
26
|
|
|
27
|
+
const positions = state.assetPositions
|
|
28
|
+
.filter(ap => Math.abs(parseFloat(ap.position.szi)) >= 0.0001)
|
|
29
|
+
.map(ap => {
|
|
30
|
+
const pos = ap.position;
|
|
31
|
+
const size = parseFloat(pos.szi);
|
|
32
|
+
const entryPx = parseFloat(pos.entryPx);
|
|
33
|
+
const notional = parseFloat(pos.positionValue);
|
|
34
|
+
const markPx = Math.abs(notional / size);
|
|
35
|
+
const pnl = parseFloat(pos.unrealizedPnl);
|
|
36
|
+
return {
|
|
37
|
+
coin: pos.coin,
|
|
38
|
+
side: size > 0 ? 'long' : 'short',
|
|
39
|
+
size: pos.szi,
|
|
40
|
+
entryPrice: pos.entryPx,
|
|
41
|
+
markPrice: markPx,
|
|
42
|
+
notional: Math.abs(notional),
|
|
43
|
+
unrealizedPnl: pnl,
|
|
44
|
+
leverage: `${pos.leverage.value}x ${pos.leverage.type}`,
|
|
45
|
+
liquidationPx: pos.liquidationPx,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const totalPnl = positions.reduce((sum, p) => sum + p.unrealizedPnl, 0);
|
|
50
|
+
|
|
51
|
+
// JSON output
|
|
52
|
+
if (jsonOutput) {
|
|
53
|
+
const result: Record<string, unknown> = {
|
|
54
|
+
address: client.address,
|
|
55
|
+
signingWallet: client.walletAddress,
|
|
56
|
+
walletType: client.isApiWallet ? 'api' : 'main',
|
|
57
|
+
accountMode,
|
|
58
|
+
equity: accountValue,
|
|
59
|
+
totalNotional,
|
|
60
|
+
totalMarginUsed,
|
|
61
|
+
withdrawable,
|
|
62
|
+
marginRatio: totalMarginUsed > 0 && accountValue > 0 ? totalMarginUsed / accountValue : 0,
|
|
63
|
+
totalUnrealizedPnl: totalPnl,
|
|
64
|
+
positions,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (args.orders) {
|
|
68
|
+
const orders = await client.getOpenOrders();
|
|
69
|
+
result.openOrders = orders.map(o => ({
|
|
70
|
+
coin: o.coin,
|
|
71
|
+
oid: o.oid,
|
|
72
|
+
side: o.side === 'B' ? 'buy' : 'sell',
|
|
73
|
+
size: o.sz,
|
|
74
|
+
price: o.limitPx,
|
|
75
|
+
orderType: o.orderType,
|
|
76
|
+
timestamp: o.timestamp,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(JSON.stringify(result, null, 2));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Human-readable output
|
|
85
|
+
console.log('Open Broker - Account Info');
|
|
86
|
+
console.log('==========================\n');
|
|
87
|
+
|
|
88
|
+
console.log('Wallet Configuration');
|
|
89
|
+
console.log('--------------------');
|
|
90
|
+
console.log(`Trading Account: ${client.address}`);
|
|
91
|
+
console.log(`Signing Wallet: ${client.walletAddress}`);
|
|
92
|
+
console.log(`Wallet Type: ${client.isApiWallet ? 'API Wallet' : 'Main Wallet'}`);
|
|
93
|
+
|
|
94
|
+
const modeLabel: Record<string, string> = {
|
|
95
|
+
standard: 'Standard (separate balances per dex)',
|
|
96
|
+
unified: 'Unified Account (shared USDC across all dexes)',
|
|
97
|
+
portfolio: 'Portfolio Margin',
|
|
98
|
+
dexAbstraction: 'DEX Abstraction (deprecated)',
|
|
99
|
+
};
|
|
100
|
+
console.log(`Account Mode: ${modeLabel[accountMode] ?? accountMode}`);
|
|
101
|
+
|
|
102
|
+
// Check builder fee approval
|
|
103
|
+
const builderApproval = await client.getMaxBuilderFee();
|
|
104
|
+
console.log(`Builder Address: ${client.builderAddress}`);
|
|
105
|
+
console.log(`Builder Fee: ${client.builderFeeBps} bps`);
|
|
106
|
+
if (builderApproval) {
|
|
107
|
+
console.log(`Builder Approved: ✅ Yes (max: ${builderApproval})`);
|
|
108
|
+
} else {
|
|
109
|
+
console.log(`Builder Approved: ❌ No`);
|
|
110
|
+
console.log(`\n⚠️ Run: npx tsx scripts/setup/approve-builder.ts`);
|
|
111
|
+
}
|
|
112
|
+
console.log('');
|
|
113
|
+
|
|
41
114
|
console.log('Margin Summary');
|
|
42
115
|
console.log('--------------');
|
|
43
116
|
console.log(`Account Value: ${formatUsd(accountValue)}`);
|
|
@@ -53,33 +126,18 @@ async function main() {
|
|
|
53
126
|
console.log('\nPositions Summary');
|
|
54
127
|
console.log('-----------------');
|
|
55
128
|
|
|
56
|
-
if (
|
|
129
|
+
if (positions.length === 0) {
|
|
57
130
|
console.log('No open positions');
|
|
58
131
|
} else {
|
|
59
|
-
let totalPnl = 0;
|
|
60
132
|
console.log('Coin | Size | Entry | Mark | PnL | Leverage');
|
|
61
133
|
console.log('---------|------------|------------|------------|------------|----------');
|
|
62
134
|
|
|
63
|
-
for (const
|
|
64
|
-
const
|
|
65
|
-
const size = parseFloat(pos.szi);
|
|
66
|
-
if (Math.abs(size) < 0.0001) continue;
|
|
67
|
-
|
|
68
|
-
const entryPx = parseFloat(pos.entryPx);
|
|
69
|
-
const pnl = parseFloat(pos.unrealizedPnl);
|
|
70
|
-
totalPnl += pnl;
|
|
71
|
-
|
|
72
|
-
// Get mark price from leverage calculation
|
|
73
|
-
const notional = parseFloat(pos.positionValue);
|
|
74
|
-
const markPx = Math.abs(notional / size);
|
|
75
|
-
|
|
76
|
-
const side = size > 0 ? 'L' : 'S';
|
|
77
|
-
const leverageStr = `${pos.leverage.value}x ${pos.leverage.type}`;
|
|
78
|
-
|
|
135
|
+
for (const p of positions) {
|
|
136
|
+
const side = p.side === 'long' ? 'L' : 'S';
|
|
79
137
|
console.log(
|
|
80
|
-
`${
|
|
81
|
-
`${formatUsd(
|
|
82
|
-
`${formatUsd(
|
|
138
|
+
`${p.coin.padEnd(8)} | ${side} ${Math.abs(parseFloat(p.size)).toFixed(4).padStart(8)} | ` +
|
|
139
|
+
`${formatUsd(parseFloat(p.entryPrice)).padStart(10)} | ${formatUsd(p.markPrice).padStart(10)} | ` +
|
|
140
|
+
`${formatUsd(p.unrealizedPnl).padStart(10)} | ${p.leverage}`
|
|
83
141
|
);
|
|
84
142
|
}
|
|
85
143
|
|
package/scripts/info/fills.ts
CHANGED
|
@@ -32,11 +32,9 @@ async function main() {
|
|
|
32
32
|
const filterCoin = args.coin as string | undefined;
|
|
33
33
|
const filterSide = args.side as string | undefined;
|
|
34
34
|
const top = parseInt(args.top as string) || 20;
|
|
35
|
+
const jsonOutput = args.json as boolean;
|
|
35
36
|
const client = getClient();
|
|
36
37
|
|
|
37
|
-
console.log('Open Broker - Trade Fills');
|
|
38
|
-
console.log('========================\n');
|
|
39
|
-
|
|
40
38
|
try {
|
|
41
39
|
let fills = await client.getUserFills();
|
|
42
40
|
|
|
@@ -52,6 +50,22 @@ async function main() {
|
|
|
52
50
|
fills.sort((a, b) => b.time - a.time);
|
|
53
51
|
fills = fills.slice(0, top);
|
|
54
52
|
|
|
53
|
+
if (jsonOutput) {
|
|
54
|
+
console.log(JSON.stringify(fills.map(f => ({
|
|
55
|
+
time: new Date(f.time).toISOString(),
|
|
56
|
+
coin: f.coin,
|
|
57
|
+
side: f.side === 'B' ? 'buy' : 'sell',
|
|
58
|
+
size: f.sz,
|
|
59
|
+
price: f.px,
|
|
60
|
+
fee: f.fee,
|
|
61
|
+
closedPnl: f.closedPnl,
|
|
62
|
+
})), null, 2));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log('Open Broker - Trade Fills');
|
|
67
|
+
console.log('========================\n');
|
|
68
|
+
|
|
55
69
|
if (fills.length === 0) {
|
|
56
70
|
console.log('No fills found');
|
|
57
71
|
return;
|
package/scripts/info/funding.ts
CHANGED
|
@@ -19,9 +19,7 @@ async function main() {
|
|
|
19
19
|
const filterCoin = args.coin as string | undefined;
|
|
20
20
|
const sortBy = (args.sort as string) || 'annualized'; // annualized, hourly, oi
|
|
21
21
|
const showAll = args.all as boolean;
|
|
22
|
-
|
|
23
|
-
console.log('Open Broker - Funding Rates');
|
|
24
|
-
console.log('===========================\n');
|
|
22
|
+
const jsonOutput = args.json as boolean;
|
|
25
23
|
|
|
26
24
|
const client = getClient();
|
|
27
25
|
const includeHip3 = args['include-hip3'] as boolean || args['hip3'] as boolean || (filterCoin?.includes(':') ?? false);
|
|
@@ -101,11 +99,19 @@ async function main() {
|
|
|
101
99
|
// Limit
|
|
102
100
|
const displayData = filterCoin ? fundingData : fundingData.slice(0, topN);
|
|
103
101
|
|
|
102
|
+
if (jsonOutput) {
|
|
103
|
+
console.log(JSON.stringify(displayData, null, 2));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
104
107
|
if (displayData.length === 0) {
|
|
105
108
|
console.log(filterCoin ? `No data for ${filterCoin}` : 'No funding data available');
|
|
106
109
|
return;
|
|
107
110
|
}
|
|
108
111
|
|
|
112
|
+
console.log('Open Broker - Funding Rates');
|
|
113
|
+
console.log('===========================\n');
|
|
114
|
+
|
|
109
115
|
// Table header
|
|
110
116
|
console.log('Coin | Hourly Rate | Annualized | Premium | Open Interest | Mark');
|
|
111
117
|
console.log('---------|-------------|------------|-------------|---------------|----------');
|
package/scripts/info/markets.ts
CHANGED
|
@@ -21,9 +21,7 @@ async function main() {
|
|
|
21
21
|
const filterCoin = args.coin as string | undefined;
|
|
22
22
|
const topN = parseInt(args.top as string) || 30;
|
|
23
23
|
const sortBy = (args.sort as string) || 'volume'; // volume, oi, change
|
|
24
|
-
|
|
25
|
-
console.log('Open Broker - Markets');
|
|
26
|
-
console.log('=====================\n');
|
|
24
|
+
const jsonOutput = args.json as boolean;
|
|
27
25
|
|
|
28
26
|
const client = getClient();
|
|
29
27
|
const includeHip3 = args['include-hip3'] as boolean || args['hip3'] as boolean || (filterCoin?.includes(':') ?? false);
|
|
@@ -109,6 +107,14 @@ async function main() {
|
|
|
109
107
|
// Limit
|
|
110
108
|
const displayData = filterCoin ? markets : markets.slice(0, topN);
|
|
111
109
|
|
|
110
|
+
if (jsonOutput) {
|
|
111
|
+
console.log(JSON.stringify(displayData, null, 2));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log('Open Broker - Markets');
|
|
116
|
+
console.log('=====================\n');
|
|
117
|
+
|
|
112
118
|
if (displayData.length === 0) {
|
|
113
119
|
console.log(filterCoin ? `No data for ${filterCoin}` : 'No market data available');
|
|
114
120
|
return;
|
package/scripts/info/orders.ts
CHANGED
|
@@ -32,11 +32,9 @@ async function main() {
|
|
|
32
32
|
const filterCoin = args.coin as string | undefined;
|
|
33
33
|
const filterStatus = args.status as string | undefined;
|
|
34
34
|
const top = parseInt(args.top as string) || 20;
|
|
35
|
+
const jsonOutput = args.json as boolean;
|
|
35
36
|
const client = getClient();
|
|
36
37
|
|
|
37
|
-
console.log('Open Broker - Order History');
|
|
38
|
-
console.log('==========================\n');
|
|
39
|
-
|
|
40
38
|
try {
|
|
41
39
|
let orders = await client.getHistoricalOrders();
|
|
42
40
|
|
|
@@ -52,6 +50,23 @@ async function main() {
|
|
|
52
50
|
orders.sort((a, b) => b.order.timestamp - a.order.timestamp);
|
|
53
51
|
orders = orders.slice(0, top);
|
|
54
52
|
|
|
53
|
+
if (jsonOutput) {
|
|
54
|
+
console.log(JSON.stringify(orders.map(entry => ({
|
|
55
|
+
time: new Date(entry.order.timestamp).toISOString(),
|
|
56
|
+
coin: entry.order.coin,
|
|
57
|
+
side: entry.order.side === 'B' ? 'buy' : 'sell',
|
|
58
|
+
orderType: entry.order.orderType,
|
|
59
|
+
size: entry.order.sz,
|
|
60
|
+
price: entry.order.limitPx,
|
|
61
|
+
status: entry.status,
|
|
62
|
+
oid: entry.order.oid,
|
|
63
|
+
})), null, 2));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log('Open Broker - Order History');
|
|
68
|
+
console.log('==========================\n');
|
|
69
|
+
|
|
55
70
|
if (orders.length === 0) {
|
|
56
71
|
console.log('No orders found');
|
|
57
72
|
return;
|
|
@@ -7,10 +7,12 @@ import { formatUsd, formatPercent, parseArgs } from '../core/utils.js';
|
|
|
7
7
|
async function main() {
|
|
8
8
|
const args = parseArgs(process.argv.slice(2));
|
|
9
9
|
const filterCoin = args.coin as string | undefined;
|
|
10
|
+
const jsonOutput = args.json as boolean;
|
|
10
11
|
const client = getClient();
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
if (args.verbose) {
|
|
14
|
+
client.verbose = true;
|
|
15
|
+
}
|
|
14
16
|
|
|
15
17
|
try {
|
|
16
18
|
const [state, mids, fundingHistory] = await Promise.all([
|
|
@@ -26,11 +28,6 @@ async function main() {
|
|
|
26
28
|
return true;
|
|
27
29
|
});
|
|
28
30
|
|
|
29
|
-
if (positions.length === 0) {
|
|
30
|
-
console.log(filterCoin ? `No position in ${filterCoin}` : 'No open positions');
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
31
|
// Sum cumulative funding per coin
|
|
35
32
|
const fundingByCoin = new Map<string, number>();
|
|
36
33
|
for (const entry of fundingHistory) {
|
|
@@ -39,6 +36,44 @@ async function main() {
|
|
|
39
36
|
fundingByCoin.set(coin, (fundingByCoin.get(coin) ?? 0) + usdc);
|
|
40
37
|
}
|
|
41
38
|
|
|
39
|
+
// JSON output
|
|
40
|
+
if (jsonOutput) {
|
|
41
|
+
const result = positions.map(ap => {
|
|
42
|
+
const pos = ap.position;
|
|
43
|
+
const size = parseFloat(pos.szi);
|
|
44
|
+
const markPx = parseFloat(mids[pos.coin] || '0');
|
|
45
|
+
const liqPx = pos.liquidationPx ? parseFloat(pos.liquidationPx) : null;
|
|
46
|
+
return {
|
|
47
|
+
coin: pos.coin,
|
|
48
|
+
side: size > 0 ? 'long' : 'short',
|
|
49
|
+
size: pos.szi,
|
|
50
|
+
entryPrice: pos.entryPx,
|
|
51
|
+
markPrice: markPx,
|
|
52
|
+
notional: Math.abs(parseFloat(pos.positionValue)),
|
|
53
|
+
unrealizedPnl: parseFloat(pos.unrealizedPnl),
|
|
54
|
+
returnOnEquity: parseFloat(pos.returnOnEquity),
|
|
55
|
+
cumulativeFunding: fundingByCoin.get(pos.coin) ?? 0,
|
|
56
|
+
marginUsed: parseFloat(pos.marginUsed),
|
|
57
|
+
leverage: `${pos.leverage.value}x`,
|
|
58
|
+
leverageType: pos.leverage.type,
|
|
59
|
+
liquidationPrice: liqPx,
|
|
60
|
+
liquidationDistance: liqPx && markPx ? Math.abs((markPx - liqPx) / markPx) : null,
|
|
61
|
+
maxLeverage: pos.maxLeverage,
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
console.log(JSON.stringify(result, null, 2));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Human-readable output
|
|
69
|
+
console.log('Open Broker - Positions');
|
|
70
|
+
console.log('=======================\n');
|
|
71
|
+
|
|
72
|
+
if (positions.length === 0) {
|
|
73
|
+
console.log(filterCoin ? `No position in ${filterCoin}` : 'No open positions');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
42
77
|
for (const ap of positions) {
|
|
43
78
|
const pos = ap.position;
|
|
44
79
|
const size = parseFloat(pos.szi);
|
package/scripts/plugin/tools.ts
CHANGED
|
@@ -35,10 +35,12 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
|
|
|
35
35
|
const { getClient } = await import('../core/client.js');
|
|
36
36
|
const client = getClient();
|
|
37
37
|
const state = await client.getUserStateAll();
|
|
38
|
+
const accountMode = await client.getAccountMode();
|
|
38
39
|
|
|
39
40
|
const result: Record<string, unknown> = {
|
|
40
41
|
address: client.address,
|
|
41
42
|
isApiWallet: client.isApiWallet,
|
|
43
|
+
accountMode,
|
|
42
44
|
equity: state.marginSummary.accountValue,
|
|
43
45
|
totalNtlPos: state.marginSummary.totalNtlPos,
|
|
44
46
|
totalMarginUsed: state.marginSummary.totalMarginUsed,
|