openbroker 1.0.62 → 1.0.63
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/SKILL.md +196 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/auto/runtime.ts +1 -1
- package/scripts/plugin/watcher.ts +8 -39
package/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: Hyperliquid trading plugin with background position monitoring and
|
|
|
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.63", "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 ob_auto_run ob_auto_stop ob_auto_list Bash(openbroker:*)
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -557,7 +557,7 @@ export default function(api) {
|
|
|
557
557
|
| Event | Payload | When |
|
|
558
558
|
|-------|---------|------|
|
|
559
559
|
| `tick` | `{ timestamp, pollCount }` | Every poll cycle (default: 10s) |
|
|
560
|
-
| `price_change` | `{ coin, oldPrice, newPrice, changePct }` | Mid price moved > 0.
|
|
560
|
+
| `price_change` | `{ coin, oldPrice, newPrice, changePct }` | Mid price moved > 0.01% between polls |
|
|
561
561
|
| `funding_update` | `{ coin, fundingRate, annualized, premium }` | Every poll for all assets |
|
|
562
562
|
| `position_opened` | `{ coin, side, size, entryPrice }` | New position detected |
|
|
563
563
|
| `position_closed` | `{ coin, previousSize, entryPrice }` | Position no longer present |
|
|
@@ -565,6 +565,200 @@ export default function(api) {
|
|
|
565
565
|
| `pnl_threshold` | `{ coin, unrealizedPnl, changePct, positionValue }` | PnL moved > 5% of position value |
|
|
566
566
|
| `margin_warning` | `{ marginUsedPct, equity, marginUsed }` | Margin usage > 80% |
|
|
567
567
|
|
|
568
|
+
### Event Details — Choosing the Right Event
|
|
569
|
+
|
|
570
|
+
#### `tick` — The universal heartbeat
|
|
571
|
+
Fires **every single poll cycle** (default: 10s) regardless of market conditions. Use this when you need to check something on every poll — absolute price thresholds, custom conditions, periodic account checks. This is the most reliable event because it always fires.
|
|
572
|
+
|
|
573
|
+
**Payload:** `{ timestamp: number, pollCount: number }`
|
|
574
|
+
|
|
575
|
+
**When to use:**
|
|
576
|
+
- Checking if a price is above/below an absolute threshold (e.g. "alert me when ETH < $3000")
|
|
577
|
+
- Custom conditions that don't fit other events (e.g. "if I have no positions and funding is high, enter")
|
|
578
|
+
- Periodic tasks that need to run every poll (though `api.every()` is better for longer intervals)
|
|
579
|
+
|
|
580
|
+
**Example — absolute price alert:**
|
|
581
|
+
```typescript
|
|
582
|
+
api.on('tick', async () => {
|
|
583
|
+
const mids = await api.client.getAllMids();
|
|
584
|
+
const price = parseFloat(mids['HYPE']);
|
|
585
|
+
if (price < 38 && !api.state.get('alerted')) {
|
|
586
|
+
api.state.set('alerted', true);
|
|
587
|
+
await api.publish(`HYPE dropped below $38 — now at $${price.toFixed(3)}`);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
**Note:** `tick` does not include price data in its payload — you must fetch it yourself via `api.client.getAllMids()`. This is because tick fires before any other event processing. If you only care about price movements, use `price_change` instead.
|
|
593
|
+
|
|
594
|
+
#### `price_change` — Relative price movements
|
|
595
|
+
Fires when a coin's mid price moves **≥ 0.01%** compared to the previous poll. This filters out rounding noise while catching virtually any real price movement. The comparison is between consecutive polls (not from a fixed baseline), so it detects incremental changes.
|
|
596
|
+
|
|
597
|
+
**Payload:** `{ coin: string, oldPrice: number, newPrice: number, changePct: number }`
|
|
598
|
+
|
|
599
|
+
**When to use:**
|
|
600
|
+
- Reacting to price movements (breakouts, momentum, mean reversion)
|
|
601
|
+
- Monitoring specific coins for volatility
|
|
602
|
+
- Building price-triggered entry/exit logic
|
|
603
|
+
|
|
604
|
+
**When NOT to use:**
|
|
605
|
+
- Checking if price is above/below a fixed threshold — use `tick` instead, because `price_change` only fires on relative movement between polls. During slow drifts (e.g. price slowly declining $0.001/s), the change between any two 10s polls may be < 0.01%, so the event won't fire even though the price has crossed your threshold.
|
|
606
|
+
|
|
607
|
+
**Example — momentum detector:**
|
|
608
|
+
```typescript
|
|
609
|
+
api.on('price_change', async ({ coin, changePct, newPrice }) => {
|
|
610
|
+
if (coin !== 'ETH') return;
|
|
611
|
+
if (changePct > 0.5) {
|
|
612
|
+
api.log.info(`ETH surging +${changePct.toFixed(2)}% — price $${newPrice}`);
|
|
613
|
+
// Enter long on strong upward momentum
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
#### `funding_update` — Funding rate data
|
|
619
|
+
Fires **every poll** for **every asset** that has funding rate data. This is high-frequency — if there are 150 perp assets, this fires 150 times per poll. Filter by coin in your handler.
|
|
620
|
+
|
|
621
|
+
**Payload:** `{ coin: string, fundingRate: number, annualized: number, premium: number }`
|
|
622
|
+
- `fundingRate` — the raw hourly funding rate (e.g. 0.0001 = 0.01%/hr)
|
|
623
|
+
- `annualized` — annualized rate (fundingRate × 8760 × 100, as a percentage)
|
|
624
|
+
- `premium` — the premium component
|
|
625
|
+
|
|
626
|
+
**When to use:**
|
|
627
|
+
- Funding rate arbitrage strategies
|
|
628
|
+
- Monitoring for extreme funding (entry/exit signals)
|
|
629
|
+
- Scanning for highest/lowest funding across all assets
|
|
630
|
+
|
|
631
|
+
**Example — funding scalp:**
|
|
632
|
+
```typescript
|
|
633
|
+
api.on('funding_update', async ({ coin, annualized }) => {
|
|
634
|
+
if (coin !== 'ETH') return;
|
|
635
|
+
if (annualized > 50 && !api.state.get('isShort')) {
|
|
636
|
+
api.log.info(`ETH funding at ${annualized.toFixed(1)}% annualized — shorting`);
|
|
637
|
+
await api.client.marketOrder('ETH', false, 0.1);
|
|
638
|
+
api.state.set('isShort', true);
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
#### `position_opened` — New position detected
|
|
644
|
+
Fires when a position appears that wasn't present in the previous poll. Useful for tracking entries made by other systems or confirming your own orders filled.
|
|
645
|
+
|
|
646
|
+
**Payload:** `{ coin: string, side: 'long' | 'short', size: number, entryPrice: number }`
|
|
647
|
+
|
|
648
|
+
**When to use:**
|
|
649
|
+
- Setting TP/SL on new positions automatically
|
|
650
|
+
- Logging/alerting when positions are opened (by you or another system)
|
|
651
|
+
- Starting position-specific monitoring
|
|
652
|
+
|
|
653
|
+
**Example — auto TP/SL on new positions:**
|
|
654
|
+
```typescript
|
|
655
|
+
api.on('position_opened', async ({ coin, side, size, entryPrice }) => {
|
|
656
|
+
const tpPrice = side === 'long' ? entryPrice * 1.05 : entryPrice * 0.95;
|
|
657
|
+
const slPrice = side === 'long' ? entryPrice * 0.97 : entryPrice * 1.03;
|
|
658
|
+
await api.client.takeProfit(coin, side !== 'long', size, tpPrice);
|
|
659
|
+
await api.client.stopLoss(coin, side !== 'long', size, slPrice);
|
|
660
|
+
api.log.info(`Set TP at ${tpPrice} / SL at ${slPrice} for ${coin}`);
|
|
661
|
+
});
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
#### `position_closed` — Position gone
|
|
665
|
+
Fires when a position that existed in the previous poll is no longer present. The position was either closed by you, liquidated, or filled by TP/SL.
|
|
666
|
+
|
|
667
|
+
**Payload:** `{ coin: string, previousSize: number, entryPrice: number }`
|
|
668
|
+
|
|
669
|
+
**When to use:**
|
|
670
|
+
- Logging/alerting when positions close
|
|
671
|
+
- Cleaning up related orders or state
|
|
672
|
+
- Re-entry logic after a position closes
|
|
673
|
+
|
|
674
|
+
**Example:**
|
|
675
|
+
```typescript
|
|
676
|
+
api.on('position_closed', async ({ coin, previousSize, entryPrice }) => {
|
|
677
|
+
api.log.info(`${coin} position closed (was ${previousSize} @ ${entryPrice})`);
|
|
678
|
+
api.state.delete(`${coin}_tp`);
|
|
679
|
+
await api.publish(`Position closed: ${coin} (entry: $${entryPrice})`);
|
|
680
|
+
});
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
#### `position_changed` — Size or direction changed
|
|
684
|
+
Fires when an existing position's size changes (partial close, add to position, or flip direction). Does NOT fire when a new position opens or an existing one fully closes — use `position_opened` and `position_closed` for those.
|
|
685
|
+
|
|
686
|
+
**Payload:** `{ coin: string, oldSize: number, newSize: number, entryPrice: number }`
|
|
687
|
+
- `oldSize`/`newSize` are signed: positive = long, negative = short
|
|
688
|
+
|
|
689
|
+
**When to use:**
|
|
690
|
+
- Detecting partial closes or position scaling
|
|
691
|
+
- Adjusting TP/SL when position size changes
|
|
692
|
+
- Tracking DCA entries
|
|
693
|
+
|
|
694
|
+
**Example:**
|
|
695
|
+
```typescript
|
|
696
|
+
api.on('position_changed', async ({ coin, oldSize, newSize }) => {
|
|
697
|
+
if (Math.abs(newSize) > Math.abs(oldSize)) {
|
|
698
|
+
api.log.info(`${coin} position increased: ${oldSize} → ${newSize}`);
|
|
699
|
+
} else {
|
|
700
|
+
api.log.info(`${coin} position reduced: ${oldSize} → ${newSize}`);
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
#### `pnl_threshold` — Significant PnL movement
|
|
706
|
+
Fires when unrealized PnL changes by **≥ 5% of position value** between consecutive polls. This is a large move detector — useful for risk management alerts rather than routine monitoring.
|
|
707
|
+
|
|
708
|
+
**Payload:** `{ coin: string, unrealizedPnl: number, changePct: number, positionValue: number }`
|
|
709
|
+
- `changePct` — the PnL change as a percentage of total position value (not % of PnL itself)
|
|
710
|
+
|
|
711
|
+
**When to use:**
|
|
712
|
+
- Risk alerts for large PnL swings
|
|
713
|
+
- Auto-close or reduce positions on sudden adverse moves
|
|
714
|
+
- Escalating alerts to the user via `api.publish()`
|
|
715
|
+
|
|
716
|
+
**Example:**
|
|
717
|
+
```typescript
|
|
718
|
+
api.on('pnl_threshold', async ({ coin, unrealizedPnl, changePct }) => {
|
|
719
|
+
if (unrealizedPnl < 0) {
|
|
720
|
+
await api.publish(
|
|
721
|
+
`⚠️ ${coin} PnL dropped sharply: $${unrealizedPnl.toFixed(2)} (${changePct.toFixed(1)}% of position)`,
|
|
722
|
+
{ name: 'pnl-alert' },
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
#### `margin_warning` — High margin usage
|
|
729
|
+
Fires when margin usage exceeds **80%** of equity. After the first trigger, it only fires again if margin usage increases by another 5 percentage points (prevents spam). Resets when margin drops back below 80%.
|
|
730
|
+
|
|
731
|
+
**Payload:** `{ marginUsedPct: number, equity: number, marginUsed: number }`
|
|
732
|
+
|
|
733
|
+
**When to use:**
|
|
734
|
+
- Automated risk reduction (close smallest position to free margin)
|
|
735
|
+
- Alerting the user before liquidation risk
|
|
736
|
+
- Pausing new entries when margin is high
|
|
737
|
+
|
|
738
|
+
**Example:**
|
|
739
|
+
```typescript
|
|
740
|
+
api.on('margin_warning', async ({ marginUsedPct, equity }) => {
|
|
741
|
+
await api.publish(
|
|
742
|
+
`🚨 Margin at ${marginUsedPct.toFixed(1)}% — equity: $${equity.toFixed(2)}. Consider reducing exposure.`,
|
|
743
|
+
{ name: 'margin-alert' },
|
|
744
|
+
);
|
|
745
|
+
});
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
### Choosing the Right Event — Quick Guide
|
|
749
|
+
|
|
750
|
+
| Use case | Best event | Why |
|
|
751
|
+
|----------|-----------|-----|
|
|
752
|
+
| Alert when price crosses a fixed level | `tick` | Fires every poll — no minimum change threshold |
|
|
753
|
+
| React to price momentum/volatility | `price_change` | Provides relative change data between polls |
|
|
754
|
+
| Funding rate strategy | `funding_update` | Gives annualized rate directly |
|
|
755
|
+
| Auto TP/SL on new positions | `position_opened` | Fires exactly when a new position appears |
|
|
756
|
+
| Log when positions close | `position_closed` | Fires when position disappears |
|
|
757
|
+
| Track position scaling | `position_changed` | Fires on size changes only |
|
|
758
|
+
| Risk management — PnL spikes | `pnl_threshold` | Only fires on large moves (≥5% of position value) |
|
|
759
|
+
| Risk management — margin | `margin_warning` | Fires at 80%+ margin usage |
|
|
760
|
+
| Periodic task (DCA, rebalance) | `api.every(ms, fn)` | Better than tick for longer intervals |
|
|
761
|
+
|
|
568
762
|
### Client Methods Available
|
|
569
763
|
|
|
570
764
|
The `api.client` object exposes the full Hyperliquid SDK:
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/scripts/auto/runtime.ts
CHANGED
|
@@ -341,7 +341,7 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
341
341
|
const oldPrice = previousSnapshot.prices.get(coin);
|
|
342
342
|
if (oldPrice === undefined || oldPrice === 0) continue;
|
|
343
343
|
const changePct = ((newPrice - oldPrice) / oldPrice) * 100;
|
|
344
|
-
if (Math.abs(changePct) >= 0.
|
|
344
|
+
if (Math.abs(changePct) >= 0.01) { // 0.01% minimum to fire (filters rounding noise)
|
|
345
345
|
const errors = await eventBus.emit('price_change', { coin, oldPrice, newPrice, changePct });
|
|
346
346
|
if (errors.length) await handleErrors(errors);
|
|
347
347
|
eventsEmitted++;
|
|
@@ -284,66 +284,35 @@ export class PositionWatcher implements PluginService {
|
|
|
284
284
|
return events;
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
private buildHookMessage(event: PositionEvent): string {
|
|
288
|
-
const lines: string[] = [
|
|
289
|
-
`[OpenBroker Alert] ${this.formatEventHeadline(event)}`,
|
|
290
|
-
'',
|
|
291
|
-
'Details:',
|
|
292
|
-
...Object.entries(event.details ?? {}).map(([k, v]) => ` ${k}: ${v}`),
|
|
293
|
-
'',
|
|
294
|
-
`Notify the user of this trading alert via their preferred channel.`,
|
|
295
|
-
];
|
|
296
|
-
return lines.join('\n');
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
private formatEventHeadline(event: PositionEvent): string {
|
|
300
|
-
switch (event.type) {
|
|
301
|
-
case 'position_opened':
|
|
302
|
-
return `New ${event.details?.side ?? ''} position opened on ${event.coin}: ${Math.abs(parseFloat(String(event.details?.size ?? '0')))} ${event.coin} at $${event.details?.entryPrice}`;
|
|
303
|
-
case 'position_closed':
|
|
304
|
-
return `Position on ${event.coin} has been closed (was ${event.details?.previousSize} at $${event.details?.entryPrice}, last unrealized PnL: $${event.details?.lastPnl})`;
|
|
305
|
-
case 'position_size_changed':
|
|
306
|
-
return `Position on ${event.coin} size changed from ${event.details?.previousSize} to ${event.details?.newSize}`;
|
|
307
|
-
case 'pnl_threshold':
|
|
308
|
-
return `Significant PnL movement on ${event.coin}: $${Number(event.details?.previousPnl ?? 0).toFixed(2)} → $${Number(event.details?.currentPnl ?? 0).toFixed(2)} (${Number(event.details?.changePct ?? 0).toFixed(1)}% of position value)`;
|
|
309
|
-
case 'margin_warning':
|
|
310
|
-
return `Margin usage at ${Number(event.details?.marginUsedPct ?? 0).toFixed(1)}% — approaching risk threshold (equity: $${event.details?.equity}, margin used: $${event.details?.marginUsed})`;
|
|
311
|
-
default:
|
|
312
|
-
return event.message;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
287
|
private async sendHook(event: PositionEvent): Promise<void> {
|
|
317
288
|
const port = this.gatewayPort || 18789;
|
|
318
289
|
|
|
319
290
|
if (!this.hooksToken) {
|
|
320
|
-
this.logger.
|
|
291
|
+
this.logger.debug('sendHook skipped — no hooks token configured');
|
|
321
292
|
return;
|
|
322
293
|
}
|
|
323
294
|
|
|
324
|
-
const hookUrl = `http://127.0.0.1:${port}/hooks/agent`;
|
|
325
|
-
|
|
326
295
|
try {
|
|
327
|
-
const
|
|
296
|
+
const res = await fetch(`http://127.0.0.1:${port}/hooks/agent`, {
|
|
328
297
|
method: 'POST',
|
|
329
298
|
headers: {
|
|
330
299
|
'Content-Type': 'application/json',
|
|
331
300
|
'Authorization': `Bearer ${this.hooksToken}`,
|
|
332
301
|
},
|
|
333
302
|
body: JSON.stringify({
|
|
334
|
-
message:
|
|
335
|
-
name: `ob-${event.type}`,
|
|
303
|
+
message: event.message,
|
|
304
|
+
name: `ob-watcher-${event.type}`,
|
|
336
305
|
wakeMode: 'now',
|
|
337
306
|
}),
|
|
338
307
|
});
|
|
339
308
|
|
|
340
|
-
if (
|
|
341
|
-
this.logger.
|
|
309
|
+
if (!res.ok) {
|
|
310
|
+
this.logger.warn(`sendHook failed: HTTP ${res.status} ${res.statusText}`);
|
|
342
311
|
} else {
|
|
343
|
-
this.logger.
|
|
312
|
+
this.logger.debug(`sendHook delivered for ${event.type} (${event.message.length} chars)`);
|
|
344
313
|
}
|
|
345
314
|
} catch (err) {
|
|
346
|
-
this.logger.warn(`
|
|
315
|
+
this.logger.warn(`sendHook error: ${err instanceof Error ? err.message : String(err)}`);
|
|
347
316
|
}
|
|
348
317
|
}
|
|
349
318
|
}
|