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 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.62", "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)"}]}}
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.1% |
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:
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openbroker",
3
3
  "name": "OpenBroker — Hyperliquid Trading",
4
- "version": "1.0.62",
4
+ "version": "1.0.63",
5
5
  "description": "Trade on Hyperliquid DEX with background position monitoring",
6
6
  "configSchema": {
7
7
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbroker",
3
- "version": "1.0.62",
3
+ "version": "1.0.63",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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.1) { // 0.1% minimum to fire
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.warn('No hooks token configured skipping hook delivery. Set hooksToken in plugin config or OPENCLAW_HOOKS_TOKEN env var.');
291
+ this.logger.debug('sendHook skippedno 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 response = await fetch(hookUrl, {
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: this.buildHookMessage(event),
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 (response.status === 202 || response.ok) {
341
- this.logger.debug(`Hook delivered for ${event.type}`);
309
+ if (!res.ok) {
310
+ this.logger.warn(`sendHook failed: HTTP ${res.status} ${res.statusText}`);
342
311
  } else {
343
- this.logger.warn(`Hook POST failed: ${response.status} ${response.statusText}`);
312
+ this.logger.debug(`sendHook delivered for ${event.type} (${event.message.length} chars)`);
344
313
  }
345
314
  } catch (err) {
346
- this.logger.warn(`Hook POST error: ${err instanceof Error ? err.message : String(err)}`);
315
+ this.logger.warn(`sendHook error: ${err instanceof Error ? err.message : String(err)}`);
347
316
  }
348
317
  }
349
318
  }