Feat/perps mirror hedge#14
Conversation
Monitor source wallet(s) and automatically open/close opposite positions on a target wallet. Supports multi-source monitoring, USD value matching, and fixed leverage (40x default).
There was a problem hiding this comment.
Code Review
This pull request introduces a new mirror (hedge) command to monitor source wallets and automatically open opposite positions, implements a 30-second cache TTL for asset metadata to prevent IOC failures, updates order execution types to use Immediate-or-Cancel (IOC) limits, and refactors output formatting to support arrays and nested objects. Key feedback points include: implementing defensive checks in getUserPositions to prevent potential TypeErrors, verifying the success of updateLeverage before caching leverage status, hoisting the getAssetMeta call to the top of the polling loop to avoid redundant API requests, and removing a redundant conditional branch in formatValue where both paths are identical.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| return (data.assetPositions ?? []).map((ap) => ({ | ||
| coin: ap.position.coin, | ||
| szi: parseFloat(ap.position.szi), | ||
| entryPx: parseFloat(ap.position.entryPx ?? '0'), | ||
| unrealizedPnl: parseFloat(ap.position.unrealizedPnl ?? '0'), | ||
| })); |
There was a problem hiding this comment.
If ap.position is undefined or null, accessing ap.position.coin will throw a TypeError, causing the entire function to fail and return []. Enforce defensive programming by safely checking if ap.position exists before mapping.
return (data.assetPositions ?? [])
.map((ap) => {
const pos = ap?.position;
if (!pos) return null;
return {
coin: pos.coin,
szi: parseFloat(pos.szi),
entryPx: parseFloat(pos.entryPx ?? '0'),
unrealizedPnl: parseFloat(pos.unrealizedPnl ?? '0'),
};
})
.filter((p): p is HlPosition => p !== null);| if (!leverageSet.has(coin)) { | ||
| try { | ||
| await perpsApi.updateLeverage(creds.accessToken, { | ||
| symbol: coin, | ||
| isCross: true, | ||
| leverage: targetLeverage, | ||
| subAccountId: walletId, | ||
| }); | ||
| leverageSet.add(coin); | ||
| } catch (e) { | ||
| warn(` Could not set leverage for ${coin}: ${String(e).slice(0, 80)}`); | ||
| } | ||
| } |
There was a problem hiding this comment.
updateLeverage returns an ApiResponse<void> and does not throw on API-level failures (e.g., invalid leverage, unauthorized). We should check levRes.success before adding the coin to leverageSet to ensure we don't skip setting leverage on subsequent polls if it failed.
| if (!leverageSet.has(coin)) { | |
| try { | |
| await perpsApi.updateLeverage(creds.accessToken, { | |
| symbol: coin, | |
| isCross: true, | |
| leverage: targetLeverage, | |
| subAccountId: walletId, | |
| }); | |
| leverageSet.add(coin); | |
| } catch (e) { | |
| warn(` Could not set leverage for ${coin}: ${String(e).slice(0, 80)}`); | |
| } | |
| } | |
| if (!leverageSet.has(coin)) { | |
| try { | |
| const levRes = await perpsApi.updateLeverage(creds.accessToken, { | |
| symbol: coin, | |
| isCross: true, | |
| leverage: targetLeverage, | |
| subAccountId: walletId, | |
| }); | |
| if (levRes.success) { | |
| leverageSet.add(coin); | |
| } else { | |
| warn(' Could not set leverage for ' + coin + ': ' + (levRes.error?.message ?? 'Unknown error')); | |
| } | |
| } catch (e) { | |
| warn(' Could not set leverage for ' + coin + ': ' + String(e).slice(0, 80)); | |
| } | |
| } |
| while (running) { | ||
| try { | ||
| for (const source of sources) { | ||
| const current = await fetchSourcePositions(source); |
There was a problem hiding this comment.
To avoid redundant API calls and cache checks inside the nested loops, fetch the asset metadata once per poll interval at the top of the while (running) loop.
| while (running) { | |
| try { | |
| for (const source of sources) { | |
| const current = await fetchSourcePositions(source); | |
| while (running) { | |
| try { | |
| const assets = await perpsApi.getAssetMeta(); | |
| for (const source of sources) { |
| const assets = await perpsApi.getAssetMeta(); | ||
| const meta = assets.find((a) => a.name.toUpperCase() === coin.toUpperCase()); |
There was a problem hiding this comment.
Since assets is now fetched once per poll interval at the top of the loop, you can remove the redundant call here.
| const assets = await perpsApi.getAssetMeta(); | |
| const meta = assets.find((a) => a.name.toUpperCase() === coin.toUpperCase()); | |
| const meta = assets.find((a) => a.name.toUpperCase() === coin.toUpperCase()); |
| if (entries.length <= 3) { | ||
| return entries.map(([k, v]) => `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`).join(' '); | ||
| return entries.map(([k, v]) => `${k}=${typeof v === 'string' ? v : formatValue(v, k)}`).join(' '); | ||
| } | ||
| return chalk.dim(JSON.stringify(value)); | ||
| return entries.map(([k, v]) => `${k}=${typeof v === 'string' ? v : formatValue(v, k)}`).join(' '); |
No description provided.