From 23c28fc140c943188d430146b0ede13bf11a19f5 Mon Sep 17 00:00:00 2001 From: Mat Milbury Date: Fri, 24 Apr 2026 17:48:49 +0200 Subject: [PATCH 1/3] feat: add dependency install hints for required binaries When openclaw detects a missing binary dependency, it now has structured metadata to tell users how to install it. Install hints are declared at both the plugin level (openclaw.plugin.json) and the skill level (SKILL.md frontmatter). Co-Authored-By: Claude Opus 4.6 (1M context) --- openclaw.plugin.json | 10 ++++++++++ skills/onboard/SKILL.md | 5 ++++- skills/trade/SKILL.md | 5 ++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 58df48e..1a619c2 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -3,6 +3,16 @@ "name": "Eterna Trading", "description": "Trade on Eterna from your openclaw agent using the eterna CLI", "skills": ["./skills/trade", "./skills/onboard"], + "dependencies": { + "bins": { + "eterna": { + "package": "@eterna-hybrid-exchange/cli", + "install": "npm install -g @eterna-hybrid-exchange/cli", + "postInstall": "eterna login", + "docs": "https://github.com/EternaHybridExchange/eterna-cli" + } + } + }, "configSchema": { "type": "object", "additionalProperties": false diff --git a/skills/onboard/SKILL.md b/skills/onboard/SKILL.md index 18087ef..b387e5b 100644 --- a/skills/onboard/SKILL.md +++ b/skills/onboard/SKILL.md @@ -1,7 +1,10 @@ --- name: onboarding description: Guided onboarding flow for new Eterna users — only activate when explicitly invoked -metadata.openclaw.requires.bins: [eterna] +metadata.openclaw.requires.bins: + - name: eterna + install: npm install -g @eterna-hybrid-exchange/cli + postInstall: eterna login --- # Eterna Trading Agent — Onboarding Skill diff --git a/skills/trade/SKILL.md b/skills/trade/SKILL.md index 7d2f501..ae42927 100644 --- a/skills/trade/SKILL.md +++ b/skills/trade/SKILL.md @@ -1,7 +1,10 @@ --- name: eterna_trading description: Trade crypto on Eterna using the eterna CLI -metadata.openclaw.requires.bins: [eterna] +metadata.openclaw.requires.bins: + - name: eterna + install: npm install -g @eterna-hybrid-exchange/cli + postInstall: eterna login --- # Eterna Trading Skill From c2b712e33203d9e030e168d085eb37a5f0457747 Mon Sep 17 00:00:00 2001 From: Mat Milbury Date: Fri, 24 Apr 2026 17:48:49 +0200 Subject: [PATCH 2/3] refactor: split monolithic onboarding skill into focused, composable skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 894-line onboarding SKILL.md was too large for LLMs to follow reliably — during testing, the agent used getBalance() instead of getDepositRecords() to monitor deposits, skipped the funding-to-trading transfer, and jumped phases without confirming completion. Replace with 6 lean skills: - trade (router): always-active, detects user state, auto-onboards new users - market-scan: TA scanning, trade ideas, symbol deep-dives - deposit: deposit monitoring, funding-to-trading transfer (the broken flow) - open-position: trade execution with proper sizing and confirmation - close-position: close positions, cancel orders, modify TP/SL - withdraw: withdrawal flow Each skill includes only the SDK methods it uses and emphasizes the critical steps that were missed (e.g. deposit skill explicitly warns against using getBalance() to check deposits). Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude-plugin/plugin.json | 9 +- README.md | 32 +- openclaw.plugin.json | 9 +- skills/close-position/SKILL.md | 88 ++++ skills/deposit/SKILL.md | 113 +++++ skills/market-scan/SKILL.md | 193 +++++++ skills/onboard/SKILL.md | 893 --------------------------------- skills/open-position/SKILL.md | 161 ++++++ skills/trade/SKILL.md | 89 ++-- skills/withdraw/SKILL.md | 57 +++ 10 files changed, 696 insertions(+), 948 deletions(-) create mode 100644 skills/close-position/SKILL.md create mode 100644 skills/deposit/SKILL.md create mode 100644 skills/market-scan/SKILL.md delete mode 100644 skills/onboard/SKILL.md create mode 100644 skills/open-position/SKILL.md create mode 100644 skills/withdraw/SKILL.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 336561b..6918115 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,13 @@ { "name": "Eterna Trading", "description": "Trade on Eterna from your openclaw agent using the eterna CLI", - "skills": ["skills/trade", "skills/onboard"], + "skills": [ + "skills/trade", + "skills/market-scan", + "skills/deposit", + "skills/withdraw", + "skills/open-position", + "skills/close-position" + ], "hooks": [] } diff --git a/README.md b/README.md index 018d328..67bf0e9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Eterna Trading — Openclaw Plugin & Skill +# Eterna Trading — Openclaw Plugin Let your openclaw AI agent trade on [Eterna](https://eterna.exchange) using the `eterna` CLI. @@ -13,7 +13,7 @@ eterna login ## Installation -**As a plugin** (recommended — more versatile): +**As a plugin** (recommended): ```shell openclaw plugins install @eterna-hybrid-exchange/openclaw-plugin @@ -27,21 +27,29 @@ openclaw skills install @eterna-hybrid-exchange/eterna-trading-skill ## Skills included -### `eterna_trading` — always active +### `eterna_trading` — always active (router) -Teaches the agent to use the `eterna` CLI for trading operations: +Detects user state on first message and routes to the right skill. New users get an automatic market scan and guided onboarding flow. Returning traders see their positions. -- `eterna balance` — check account balance -- `eterna positions` — view open positions -- `eterna orders` — view active orders -- `eterna execute ` — execute TypeScript trading code -- `eterna sdk --search ` — browse SDK method docs +### `market_scan` — market analysis and trade ideas -### `onboarding` — explicit invocation only +Live market briefings, TA scanning, deep-dives on specific symbols, and trade idea generation with entry/stop/target levels. -A guided 5-phase onboarding flow for new end users (Discovery → Trust Building → First Deposit → First Trade → Preferences). Activate with `/eterna-trading:onboarding` or by asking the agent to onboard you. +### `deposit` — deposit and fund trading account -Not suitable for fully autonomous trading agents — it assumes a human user is present. +Guides through deposit address, chain selection, deposit monitoring via `getDepositRecords()`, transfer from Funding to Trading wallet, and balance confirmation. + +### `withdraw` — withdraw funds + +Check withdrawable balance, submit withdrawal, and track status. + +### `open_position` — place trades + +Pre-trade checks (balance, existing positions, instrument specs), trade proposals with clear margin/notional breakdown, and execution with proper TP/SL. + +### `close_position` — close positions and manage orders + +Close positions, cancel orders, and modify TP/SL on existing positions. ## Releasing a new version diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 1a619c2..f6cc4e7 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -2,7 +2,14 @@ "id": "eterna-trading", "name": "Eterna Trading", "description": "Trade on Eterna from your openclaw agent using the eterna CLI", - "skills": ["./skills/trade", "./skills/onboard"], + "skills": [ + "./skills/trade", + "./skills/market-scan", + "./skills/deposit", + "./skills/withdraw", + "./skills/open-position", + "./skills/close-position" + ], "dependencies": { "bins": { "eterna": { diff --git a/skills/close-position/SKILL.md b/skills/close-position/SKILL.md new file mode 100644 index 0000000..0fefe44 --- /dev/null +++ b/skills/close-position/SKILL.md @@ -0,0 +1,88 @@ +--- +name: close_position +description: Close positions and cancel orders on Eterna +metadata.openclaw.requires.bins: + - name: eterna + install: npm install -g @eterna-hybrid-exchange/cli + postInstall: eterna login +--- + +# Close Position Skill + +Close positions and cancel orders. + +## Show current positions + +```typescript +const [positions, orders] = await Promise.all([ + eterna.getPositions(), + eterna.getOrders(), +]); + +return { + positions: positions.list.filter((p) => parseFloat(p.size) > 0).map((p) => ({ + symbol: p.symbol, side: p.side, size: p.size, + entryPrice: p.avgPrice, markPrice: p.markPrice, + unrealisedPnl: p.unrealisedPnl, leverage: p.leverage, + })), + activeOrders: orders.list.map((o) => ({ + symbol: o.symbol, orderId: o.orderId, side: o.side, + type: o.orderType, qty: o.qty, price: o.price, status: o.orderStatus, + })), +}; +``` + +## Close a position + +**Confirm with user before closing.** Show current P&L so they know what they're locking in. + +```typescript +const result = await eterna.closePosition("LINKUSDT"); +return result; +``` + +Returns `{ orderId, closedSize, side, entryPrice, markPrice, unrealisedPnl }`. + +## Cancel orders + +Single order: + +```typescript +const result = await eterna.cancelOrder("LINKUSDT", "orderId123"); +return result; +``` + +All orders on a symbol: + +```typescript +const result = await eterna.cancelAllOrders("LINKUSDT"); +return result; +``` + +All orders across all symbols: + +```typescript +const result = await eterna.cancelAllOrders(); +return result; +``` + +## Modify TP/SL on existing position + +```typescript +await eterna.setTradingStop({ + symbol: "LINKUSDT", + takeProfit: "9.80", + stopLoss: "9.10", +}); +``` + +## SDK methods used + +| Method | Returns | +|--------|---------| +| `eterna.getPositions(symbol?)` | `{ list: [{ symbol, side, size, avgPrice, markPrice, unrealisedPnl, leverage, takeProfit, stopLoss }] }` | +| `eterna.getOrders(symbol?)` | `{ list: [{ symbol, orderId, side, orderType, qty, price, orderStatus }] }` | +| `eterna.closePosition(symbol)` | `{ orderId, closedSize, side, entryPrice, markPrice, unrealisedPnl }` | +| `eterna.cancelOrder(symbol, orderId)` | `{ orderId, orderLinkId }` | +| `eterna.cancelAllOrders(symbol?)` | `{ list: [{ orderId }] }` | +| `eterna.setTradingStop({ symbol, takeProfit?, stopLoss?, trailingStop?, ... })` | `{}` | diff --git a/skills/deposit/SKILL.md b/skills/deposit/SKILL.md new file mode 100644 index 0000000..3841c0c --- /dev/null +++ b/skills/deposit/SKILL.md @@ -0,0 +1,113 @@ +--- +name: deposit +description: Guide user through depositing crypto and transferring funds to the trading wallet +metadata.openclaw.requires.bins: + - name: eterna + install: npm install -g @eterna-hybrid-exchange/cli + postInstall: eterna login +--- + +# Deposit Skill + +Guide the user through depositing funds into their Eterna trading account. + +**Critical flow:** Deposit address → send crypto → monitor with `getDepositRecords()` → transfer to trading wallet → confirm balance. + +## Important context + +- Deposits arrive in the **Funding wallet**, NOT the trading wallet. +- After deposit confirms, you MUST call `transferToTrading()` before funds are usable. +- `getBalance()` checks the **trading wallet** — it will show zero until you transfer. **Do NOT use `getBalance()` to check if a deposit has arrived.** +- Use `getDepositRecords()` to monitor incoming deposits. +- Recommend **Arbitrum** for USDT deposits — cheapest and fastest. + +## Step 1 — Show deposit options + +```typescript +const coins = await eterna.getAllowedDepositCoins("USDT"); +return coins.configList.map((c) => ({ + coin: c.coin, chain: c.chain, chainName: c.chainType, + minDeposit: c.minDepositAmount, confirmations: c.blockConfirmNumber, +})); +``` + +Present as a simple choice: "I'd recommend **Arbitrum** — fast and cheap. Which chain works for your wallet?" + +If the user wants a different coin (BTC, ETH, USDC), run `getAllowedDepositCoins` for that coin. + +## Step 2 — Get deposit address + +```typescript +// Replace coin/chain based on user's choice +const addr = await eterna.getDepositAddress("USDT", "ARBI"); +return addr; +``` + +Show address clearly. If there's a tag/memo, emphasize it — missing tags can lose funds. + +## Step 3 — Monitor deposit + +Once the user says they've sent funds, check `getDepositRecords()`: + +```typescript +const records = await eterna.getDepositRecords("USDT"); +const pending = records.rows.filter((r) => r.status !== 3 && r.status !== 4); +const confirmed = records.rows.filter((r) => r.status === 3); +return { pending, confirmed }; +``` + +**Status codes:** +- 0 = unknown +- 1 = waiting for confirmations — tell user: "I can see it, waiting for blockchain confirmations." +- 2 = processing — "Almost there, Eterna is processing it." +- **3 = success** — proceed to Step 4 immediately. +- 4 = failed — "Something went wrong. Check the tx on a block explorer." + +Check when the user asks. If they ask you to poll, check every minute. + +## Step 4 — Transfer to trading wallet + +**This step is mandatory.** Funds sit in the Funding wallet until transferred. + +If they deposited USDT: + +```typescript +const transfer = await eterna.transferToTrading("USDT", "ALL_BALANCE"); +return transfer; +``` + +If they deposited a non-USDT coin, swap first: + +```typescript +const swap = await eterna.swapToUsdt("ETH"); // omit amount for full balance +const transfer = await eterna.transferToTrading("USDT", "ALL_BALANCE"); +return { swap, transfer }; +``` + +## Step 5 — Confirm balance is ready + +```typescript +const balance = await eterna.getBalance(); +const account = balance.list[0]; +return { + totalEquity: account.totalEquity, + availableBalance: account.totalAvailableBalance, + coins: account.coin.filter((c) => parseFloat(c.equity) > 0).map((c) => ({ + coin: c.coin, equity: c.equity, usdValue: c.usdValue, + })), +}; +``` + +Celebrate: "You're funded! $X ready to trade." Then suggest looking at trade ideas. + +## SDK methods used + +| Method | Returns | +|--------|---------| +| `eterna.getAllowedDepositCoins(coin?, chain?)` | `{ configList: [{ coin, chain, chainType, minDepositAmount, blockConfirmNumber }] }` | +| `eterna.getDepositAddress(coin, chainType)` | `{ coin, chains: { chainType, addressDeposit, tagDeposit } }` | +| `eterna.getDepositRecords(coin?)` | `{ rows: [{ coin, chain, amount, txID, status, confirmations }] }` — status: 0-4 | +| `eterna.transferToTrading(coin, amount)` | `{ transferId: string }` — use `"ALL_BALANCE"` for full transfer | +| `eterna.swapToUsdt(coin, amount?)` | `{ coin, orderId, qty, success, message }` — omit amount for full balance | +| `eterna.getBalance()` | `{ list: [{ totalEquity, totalAvailableBalance, coin: [...] }] }` — **trading wallet only** | +| `eterna.getAllCoinsBalance(accountType)` | Balance by account type: `"FUND"`, `"UNIFIED"`, `"SPOT"` | diff --git a/skills/market-scan/SKILL.md b/skills/market-scan/SKILL.md new file mode 100644 index 0000000..5e93189 --- /dev/null +++ b/skills/market-scan/SKILL.md @@ -0,0 +1,193 @@ +--- +name: market_scan +description: Live market scanning, technical analysis, and trade idea generation using Eterna CLI +metadata.openclaw.requires.bins: + - name: eterna + install: npm install -g @eterna-hybrid-exchange/cli + postInstall: eterna login +--- + +# Market Scan Skill + +Scan live markets and generate trade ideas using the `eterna` CLI. + +## Quick market briefing + +Run this to show the user what's happening right now: + +```typescript +const [tickers, btcRsi, btcMacd, btcBb, ethRsi, ethMacd] = await Promise.all([ + eterna.getTickers(), + eterna.getRsi("BTCUSDT", "1h"), + eterna.getMacd("BTCUSDT", "1h"), + eterna.getBollingerBands("BTCUSDT", "1h"), + eterna.getRsi("ETHUSDT", "1h"), + eterna.getMacd("ETHUSDT", "1h"), +]); + +const pairs = tickers.list; +const btc = pairs.find((t) => t.symbol === "BTCUSDT"); +const eth = pairs.find((t) => t.symbol === "ETHUSDT"); + +const sorted = pairs + .filter((t) => parseFloat(t.turnover24h) > 5_000_000) + .sort((a, b) => Math.abs(parseFloat(b.price24hPcnt)) - Math.abs(parseFloat(a.price24hPcnt))) + .slice(0, 5); + +return { + btc: { + price: btc.lastPrice, + change24h: (parseFloat(btc.price24hPcnt) * 100).toFixed(2) + "%", + rsi: btcRsi.value.toFixed(1), + macdSignal: btcMacd.valueMACDHist > 0 ? "bullish" : "bearish", + bbWidth: (((btcBb.valueUpperBand - btcBb.valueLowerBand) / btcBb.valueMiddleBand) * 100).toFixed(2) + "%", + }, + eth: { + price: eth.lastPrice, + change24h: (parseFloat(eth.price24hPcnt) * 100).toFixed(2) + "%", + rsi: ethRsi.value.toFixed(1), + macdSignal: ethMacd.valueMACDHist > 0 ? "bullish" : "bearish", + }, + topMovers: sorted.map((t) => ({ + symbol: t.symbol, + price: t.lastPrice, + change: (parseFloat(t.price24hPcnt) * 100).toFixed(2) + "%", + volume: "$" + (parseFloat(t.turnover24h) / 1_000_000).toFixed(1) + "M", + })), +}; +``` + +**Present as a market briefing, not raw data.** Example tone: + +> BTC at $94,200 — up 1.3%. RSI 62 on the hourly, MACD flipping bullish. Bollinger Bands tight (1.8%) — a move is brewing. +> SUI is the big mover — up 8.4% with $340M volume. ETH flat at +0.2%. + +**Important:** Filter top movers to `turnover24h > 5_000_000` to avoid obscure low-volume tokens. + +## Trade idea scan + +When the user wants trade ideas, scan high-volume pairs with TA: + +```typescript +const tickers = await eterna.getTickers(); +const pairs = tickers.list + .filter((t) => parseFloat(t.turnover24h) > 5_000_000) + .sort((a, b) => Math.abs(parseFloat(b.price24hPcnt)) - Math.abs(parseFloat(a.price24hPcnt))) + .slice(0, 8); + +const analyses = await Promise.all( + pairs.map(async (t) => { + const [rsi, macd, bb] = await Promise.all([ + eterna.getRsi(t.symbol, "1h"), + eterna.getMacd(t.symbol, "1h"), + eterna.getBollingerBands(t.symbol, "1h"), + ]); + const price = parseFloat(t.lastPrice); + const change = parseFloat(t.price24hPcnt) * 100; + const bbPos = (price - bb.valueLowerBand) / (bb.valueUpperBand - bb.valueLowerBand); + + let score = 0; + let direction = ""; + + if (change > 0.3 && rsi.value < 70 && macd.valueMACDHist > 0) { + score = change + (70 - rsi.value) / 20; + direction = "long"; + } + if (change < -0.3 && rsi.value > 30 && macd.valueMACDHist < 0) { + score = Math.abs(change) + (rsi.value - 30) / 20; + direction = "short"; + } + if (change < -5 && rsi.value < 35 && bbPos < 0.2) { + score = Math.abs(change) * 1.5; + direction = "long (mean reversion)"; + } + if (change > 5 && rsi.value > 65 && bbPos > 0.8) { + score = change * 1.5; + direction = "short (mean reversion)"; + } + + return { + symbol: t.symbol, price: t.lastPrice, + change24h: change.toFixed(2) + "%", + rsi: rsi.value.toFixed(1), + macd: macd.valueMACDHist > 0 ? "bullish" : "bearish", + bbPosition: (bbPos * 100).toFixed(0) + "%", + fundingRate: (parseFloat(t.fundingRate) * 100).toFixed(4) + "%", + score, direction, + }; + }), +); + +const candidates = analyses.filter((a) => a.score > 0).sort((a, b) => b.score - a.score); +return { topSetups: candidates.slice(0, 3), noSignal: analyses.filter((a) => a.score === 0).map((a) => a.symbol) }; +``` + +**Present trade ideas with reasoning:** signal, confirmation, entry/stop/target, risk/reward. If nothing looks good, say so — "market's choppy, I wouldn't trade right now" builds more trust than a forced idea. + +## Deep-dive on a symbol + +When the user asks about a specific symbol: + +```typescript +const symbol = "BTCUSDT"; // replace + +const [ticker, ob, instruments] = await Promise.all([ + eterna.getTickers(symbol), + eterna.getOrderbook(symbol, 50), + eterna.getInstruments(symbol), +]); + +const [rsi1h, rsi4h, rsi1d] = await Promise.all([ + eterna.getRsi(symbol, "1h"), eterna.getRsi(symbol, "4h"), eterna.getRsi(symbol, "1d"), +]); +const [macd1h, macd4h] = await Promise.all([ + eterna.getMacd(symbol, "1h"), eterna.getMacd(symbol, "4h"), +]); +const [bb1h, vwap] = await Promise.all([ + eterna.getBollingerBands(symbol, "1h"), eterna.getVwap(symbol, "1h"), +]); + +const t = ticker.list[0]; +const price = parseFloat(t.lastPrice); +const totalBids = ob.b.reduce((s, [, q]) => s + parseFloat(q), 0); +const totalAsks = ob.a.reduce((s, [, q]) => s + parseFloat(q), 0); + +return { + price: t.lastPrice, change24h: (parseFloat(t.price24hPcnt) * 100).toFixed(2) + "%", + volume24h: "$" + (parseFloat(t.turnover24h) / 1_000_000).toFixed(1) + "M", + fundingRate: (parseFloat(t.fundingRate) * 100).toFixed(4) + "%", + rsi: { "1h": rsi1h.value.toFixed(1), "4h": rsi4h.value.toFixed(1), "1d": rsi1d.value.toFixed(1) }, + macd: { + "1h": { signal: macd1h.valueMACDHist > 0 ? "bullish" : "bearish", hist: macd1h.valueMACDHist.toFixed(2) }, + "4h": { signal: macd4h.valueMACDHist > 0 ? "bullish" : "bearish", hist: macd4h.valueMACDHist.toFixed(2) }, + }, + vwap: vwap.value.toFixed(2), + bollingerBands: { + upper: bb1h.valueUpperBand.toFixed(2), mid: bb1h.valueMiddleBand.toFixed(2), lower: bb1h.valueLowerBand.toFixed(2), + width: (((bb1h.valueUpperBand - bb1h.valueLowerBand) / bb1h.valueMiddleBand) * 100).toFixed(2) + "%", + }, + orderbook: { + bidVolume: totalBids.toFixed(2), askVolume: totalAsks.toFixed(2), + imbalance: (totalBids / totalAsks).toFixed(3), + spread: (parseFloat(ob.a[0][0]) - parseFloat(ob.b[0][0])).toFixed(2), + }, +}; +``` + +Synthesize — identify confluence (multiple indicators agreeing) and conflicts (RSI overbought but MACD still bullish). Lead with the actionable insight. + +## SDK methods used + +| Method | Returns | +|--------|---------| +| `eterna.getTickers(symbol?)` | `{ list: [{ symbol, lastPrice, price24hPcnt, turnover24h, fundingRate, ... }] }` — all values are strings | +| `eterna.getOrderbook(symbol, limit?)` | `{ b: [[price, qty], ...], a: [[price, qty], ...] }` — strings | +| `eterna.getInstruments(symbol?)` | `{ list: [{ lotSizeFilter, priceFilter, leverageFilter }] }` | +| `eterna.getRsi(symbol, interval, period?)` | `{ value: number }` — 0-100 | +| `eterna.getMacd(symbol, interval, ...)` | `{ valueMACD, valueMACDSignal, valueMACDHist: number }` | +| `eterna.getBollingerBands(symbol, interval, ...)` | `{ valueUpperBand, valueMiddleBand, valueLowerBand: number }` | +| `eterna.getVwap(symbol, interval)` | `{ value: number }` | +| `eterna.getEma(symbol, interval, period?)` | `{ value: number }` | +| `eterna.getSma(symbol, interval, period?)` | `{ value: number }` | + +Valid intervals: `1m`, `5m`, `15m`, `30m`, `1h`, `2h`, `4h`, `1d`, `1w`. TA methods return numbers (no parseFloat needed). Bybit ticker values are strings (parseFloat needed). diff --git a/skills/onboard/SKILL.md b/skills/onboard/SKILL.md deleted file mode 100644 index b387e5b..0000000 --- a/skills/onboard/SKILL.md +++ /dev/null @@ -1,893 +0,0 @@ ---- -name: onboarding -description: Guided onboarding flow for new Eterna users — only activate when explicitly invoked -metadata.openclaw.requires.bins: - - name: eterna - install: npm install -g @eterna-hybrid-exchange/cli - postInstall: eterna login ---- - -# Eterna Trading Agent — Onboarding Skill - -This skill activates ONLY when the user explicitly triggers it (e.g. the user says -"onboard me", "get started", or invokes `/eterna-trading:onboarding`). -Do NOT run this flow automatically. - ---- - -You are a trading agent connected to Eterna via the `eterna` CLI. Your job is to onboard -a new user through a guided experience that builds excitement and trust — not a boring setup wizard. - -You have access to the `eterna` CLI — use it to execute all trading operations: - -1. **Execute TypeScript code** — your primary tool for all market data and trades: - - Write code to a temp file (e.g. `/tmp/eterna-op.ts`) and run `eterna execute /tmp/eterna-op.ts` - - Or pipe directly: `eterna execute - << 'EOF' ... EOF` - - The sandbox provides `eterna.*` globally. Use `await` for all calls. - - Output appears on stdout. Errors include a category and hint. - - Timeout: 30 seconds. -2. **Search SDK docs** — when you need to discover methods or check signatures: - - `eterna sdk --search "" --detail summary` — quick overview - - `eterna sdk --search "" --detail full` — full signatures and params - - `eterna sdk --search "" --detail list` — just method names - -Everything you need is in this document. - -## Your personality - -- Confident but not arrogant. You know markets and you show it through data, not claims. -- Concise. 2-5 sentences per message unless the user asks for detail. -- You use real numbers from real markets. Never invent data or use placeholder values. -- When showing analysis, lead with the insight ("BTC is compressing — Bollinger bandwidth at 1.2%, lowest this week") not the method ("I ran getBollingerBands and here are the results"). -- Match the user's language and energy. - -## Onboarding phases - -Track which phase the user is in. Move forward when the transition criteria are met. Never rush — if the user wants to stay in Phase 1 exploring, let them. - -``` -Phase 1: Discovery → Phase 2: Trust Building → Phase 3: First Deposit → Phase 4: First Trade → Phase 5: Preferences -``` - ---- - -### Phase 1 — Discovery (no account needed) - -**Goal:** Show the user what you can do. Make them want more. - -**Start here** when the conversation begins. Greet the user briefly and immediately demonstrate value by running a live market scan. Do not ask "what would you like to do?" — show, don't ask. - -**Opening move — run this on your first message:** - -```typescript -const [tickers, btcRsi, btcMacd, btcBb, ethRsi, ethMacd] = await Promise.all([ - eterna.getTickers(), - eterna.getRsi("BTCUSDT", "1h"), - eterna.getMacd("BTCUSDT", "1h"), - eterna.getBollingerBands("BTCUSDT", "1h"), - eterna.getRsi("ETHUSDT", "1h"), - eterna.getMacd("ETHUSDT", "1h"), -]); - -const pairs = tickers.list; -const btc = pairs.find((t) => t.symbol === "BTCUSDT"); -const eth = pairs.find((t) => t.symbol === "ETHUSDT"); - -// Find top movers -const sorted = pairs - .filter((t) => parseFloat(t.turnover24h) > 1_000_000) - .sort( - (a, b) => - Math.abs(parseFloat(b.price24hPcnt)) - - Math.abs(parseFloat(a.price24hPcnt)), - ) - .slice(0, 5); - -return { - btc: { - price: btc.lastPrice, - change24h: (parseFloat(btc.price24hPcnt) * 100).toFixed(2) + "%", - rsi: btcRsi.value.toFixed(1), - macdSignal: btcMacd.valueMACDHist > 0 ? "bullish" : "bearish", - macdHistogram: btcMacd.valueMACDHist.toFixed(2), - bbWidth: - ( - ((btcBb.valueUpperBand - btcBb.valueLowerBand) / - btcBb.valueMiddleBand) * - 100 - ).toFixed(2) + "%", - priceVsBbMid: - parseFloat(btc.lastPrice) > btcBb.valueMiddleBand - ? "above midline" - : "below midline", - }, - eth: { - price: eth.lastPrice, - change24h: (parseFloat(eth.price24hPcnt) * 100).toFixed(2) + "%", - rsi: ethRsi.value.toFixed(1), - macdSignal: ethMacd.valueMACDHist > 0 ? "bullish" : "bearish", - }, - topMovers: sorted.map((t) => ({ - symbol: t.symbol, - price: t.lastPrice, - change: (parseFloat(t.price24hPcnt) * 100).toFixed(2) + "%", - volume: "$" + (parseFloat(t.turnover24h) / 1_000_000).toFixed(1) + "M", - })), -}; -``` - -**How to present the results:** Weave the data into a market briefing. Example tone: - -> BTC is sitting at $94,200 — up 1.3% today. RSI at 62 on the hourly, MACD histogram flipping bullish. Bollinger Bands are tight (1.8% width) which usually means a move is coming. -> -> Meanwhile, SUI is the big mover — up 8.4% with $340M in volume. ETH lagging at +0.2%, RSI neutral at 48. -> -> Want me to dig deeper into any of these? I can pull up orderbook depth, multi-timeframe analysis, or scan for momentum setups. - -**What you can demo on request:** - -- Deep-dive on any symbol (multi-timeframe RSI + MACD + Bollinger + VWAP + orderbook) -- Momentum scanning (find pairs with aligned signals across timeframes) -- Orderbook analysis (bid/ask imbalance, support/resistance levels) -- Funding rate scan (which pairs have extreme funding — who's paying whom) - -**Deep-dive code pattern** (when user asks about a specific symbol): - -```typescript -const symbol = "BTCUSDT"; // replace with requested symbol - -const [ticker, ob, instruments] = await Promise.all([ - eterna.getTickers(symbol), - eterna.getOrderbook(symbol, 50), - eterna.getInstruments(symbol), -]); - -// Multi-timeframe technical analysis -const [rsi1h, rsi4h, rsi1d] = await Promise.all([ - eterna.getRsi(symbol, "1h"), - eterna.getRsi(symbol, "4h"), - eterna.getRsi(symbol, "1d"), -]); - -const [macd1h, macd4h] = await Promise.all([ - eterna.getMacd(symbol, "1h"), - eterna.getMacd(symbol, "4h"), -]); - -const [bb1h, ema9, ema21, sma50, vwap] = await Promise.all([ - eterna.getBollingerBands(symbol, "1h"), - eterna.getEma(symbol, "1h", 9), - eterna.getEma(symbol, "1h", 21), - eterna.getSma(symbol, "1d", 50), - eterna.getVwap(symbol, "1h"), -]); - -const t = ticker.list[0]; -const price = parseFloat(t.lastPrice); - -// Orderbook analysis -const totalBids = ob.b.reduce((s, [, q]) => s + parseFloat(q), 0); -const totalAsks = ob.a.reduce((s, [, q]) => s + parseFloat(q), 0); -const imbalance = totalBids / totalAsks; - -// Contract specs -const spec = instruments.list[0]; - -return { - price: t.lastPrice, - change24h: (parseFloat(t.price24hPcnt) * 100).toFixed(2) + "%", - volume24h: "$" + (parseFloat(t.turnover24h) / 1_000_000).toFixed(1) + "M", - fundingRate: (parseFloat(t.fundingRate) * 100).toFixed(4) + "%", - technicals: { - rsi: { - "1h": rsi1h.value.toFixed(1), - "4h": rsi4h.value.toFixed(1), - "1d": rsi1d.value.toFixed(1), - }, - macd: { - "1h": { - signal: macd1h.valueMACDHist > 0 ? "bullish" : "bearish", - hist: macd1h.valueMACDHist.toFixed(2), - }, - "4h": { - signal: macd4h.valueMACDHist > 0 ? "bullish" : "bearish", - hist: macd4h.valueMACDHist.toFixed(2), - }, - }, - ema: { - "9": ema9.value.toFixed(2), - "21": ema21.value.toFixed(2), - cross: ema9.value > ema21.value ? "bullish" : "bearish", - }, - sma50d: sma50.value.toFixed(2), - vwap: vwap.value.toFixed(2), - bollingerBands: { - upper: bb1h.valueUpperBand.toFixed(2), - mid: bb1h.valueMiddleBand.toFixed(2), - lower: bb1h.valueLowerBand.toFixed(2), - width: - ( - ((bb1h.valueUpperBand - bb1h.valueLowerBand) / bb1h.valueMiddleBand) * - 100 - ).toFixed(2) + "%", - position: - price > bb1h.valueUpperBand - ? "above upper" - : price < bb1h.valueLowerBand - ? "below lower" - : price > bb1h.valueMiddleBand - ? "upper half" - : "lower half", - }, - }, - orderbook: { - bidVolume: totalBids.toFixed(2), - askVolume: totalAsks.toFixed(2), - imbalance: imbalance.toFixed(3), - interpretation: - imbalance > 1.2 - ? "strong buy pressure" - : imbalance < 0.8 - ? "strong sell pressure" - : "balanced", - topBid: ob.b[0][0], - topAsk: ob.a[0][0], - spread: (parseFloat(ob.a[0][0]) - parseFloat(ob.b[0][0])).toFixed(2), - }, - contract: { - maxLeverage: spec.leverageFilter.maxLeverage, - minOrderQty: spec.lotSizeFilter.minOrderQty, - qtyStep: spec.lotSizeFilter.qtyStep, - tickSize: spec.priceFilter.tickSize, - }, -}; -``` - -**Present deep-dive results as a narrative analysis**, not raw data. Synthesize — identify confluence signals (multiple indicators agreeing), conflicts (RSI says overbought but MACD still bullish), and actionable setups. - -**Transition to Phase 2:** When the user shows interest beyond just looking (e.g., "what would you trade?", "any good setups?", "how does trading work?"), move naturally into Phase 2. You can also nudge: "Want me to show you what I'd actually trade right now and why?" - ---- - -### Phase 2 — Trust Building (no account needed) - -**Goal:** Show trading competence. Explain exactly what you'd trade and why — without executing anything. - -**When the user asks for a trade idea**, run a full scan and present a hypothetical trade with complete reasoning: - -```typescript -const [balance, tickers] = await Promise.all([ - eterna.getBalance().catch(() => null), // may fail if no account yet — that's fine - eterna.getTickers(), -]); - -const pairs = tickers.list - .filter((t) => parseFloat(t.turnover24h) > 5_000_000) - .sort( - (a, b) => - Math.abs(parseFloat(b.price24hPcnt)) - - Math.abs(parseFloat(a.price24hPcnt)), - ) - .slice(0, 8); - -// Analyze top candidates with TA -const analyses = await Promise.all( - pairs.map(async (t) => { - const [rsi, macd, bb] = await Promise.all([ - eterna.getRsi(t.symbol, "1h"), - eterna.getMacd(t.symbol, "1h"), - eterna.getBollingerBands(t.symbol, "1h"), - ]); - const price = parseFloat(t.lastPrice); - const change = parseFloat(t.price24hPcnt) * 100; - const bbPos = - (price - bb.valueLowerBand) / (bb.valueUpperBand - bb.valueLowerBand); - - let score = 0; - let direction = ""; - - // Momentum long - if (change > 0.3 && rsi.value < 70 && macd.valueMACDHist > 0) { - score = change + (70 - rsi.value) / 20; - direction = "long"; - } - // Momentum short - if (change < -0.3 && rsi.value > 30 && macd.valueMACDHist < 0) { - score = Math.abs(change) + (rsi.value - 30) / 20; - direction = "short"; - } - // Mean reversion long - if (change < -5 && rsi.value < 35 && bbPos < 0.2) { - score = Math.abs(change) * 1.5; - direction = "long (mean reversion)"; - } - // Mean reversion short - if (change > 5 && rsi.value > 65 && bbPos > 0.8) { - score = change * 1.5; - direction = "short (mean reversion)"; - } - - return { - symbol: t.symbol, - price: t.lastPrice, - change24h: change.toFixed(2) + "%", - rsi: rsi.value.toFixed(1), - macd: macd.valueMACDHist > 0 ? "bullish" : "bearish", - bbPosition: (bbPos * 100).toFixed(0) + "% (0=lower band, 100=upper)", - fundingRate: (parseFloat(t.fundingRate) * 100).toFixed(4) + "%", - score, - direction, - }; - }), -); - -const candidates = analyses - .filter((a) => a.score > 0) - .sort((a, b) => b.score - a.score); - -return { - scanned: pairs.length + " high-volume pairs", - topSetups: candidates.slice(0, 3), - skipped: analyses - .filter((a) => a.score === 0) - .map((a) => a.symbol + " (no clear signal)"), -}; -``` - -**How to present the trade idea:** - -> Here's what I'd do right now if I had funds loaded: -> -> **Long SUI at $3.82** — it's up 6.2% today but RSI is only 58 (room to run). MACD histogram is positive and growing. Bollinger position at 72% — trending but not overextended. Orderbook shows 1.4x more bids than asks. -> -> I'd use 3x leverage, set a stop at $3.74 (-2.1%) and target $3.95 (+3.4%). Risk/reward: 1.6:1. -> -> This is a momentum play — riding the trend while it has confirmation across indicators. I'd size it at ~25% of account equity. -> -> Want to see the orderbook for this, or should we get you set up so you can actually take trades like this? - -**Key rules for Phase 2:** - -- Always show the reasoning chain: signal → confirmation → risk management -- Include specific entry, stop loss, and take profit levels -- Explain position sizing logic -- Never say "trust me" — let the analysis speak -- If there are no good setups, say so. "Market's choppy, I wouldn't trade right now" builds more trust than forcing a bad idea. - -**Transition to Phase 3:** When the user signals readiness ("how do I start?", "let's do it", "how do I fund this?", "I want to trade"), guide them to deposit. You can also nudge: "Ready to fund your account and catch the next setup?" - ---- - -### Phase 3 — First Deposit - -**Goal:** Guide the user through depositing crypto into their trading account. Make it frictionless. - -**Important context:** - -- Deposits arrive in the **Funding wallet**, not the Trading wallet. After deposit confirms, funds must be transferred. -- Recommend **Arbitrum (ARBI)** for USDT deposits — cheapest and fastest. -- Minimum to trade meaningfully: ~$20 USDT. - -**Step 3a — Show deposit options:** - -```typescript -const coins = await eterna.getAllowedDepositCoins("USDT"); -return coins.configList.map((c) => ({ - coin: c.coin, - chain: c.chain, - chainName: c.chainType, - minDeposit: c.minDepositAmount, - confirmations: c.blockConfirmNumber, -})); -``` - -Present this as a simple choice: "You can deposit USDT on several chains. I'd recommend **Arbitrum** — fast confirmations and low fees. Minimum deposit is [X] USDT. Which chain works best for your wallet?" - -If the user wants to deposit a different coin (USDC, BTC, ETH), run `getAllowedDepositCoins` for that coin instead. - -**Step 3b — Get and display deposit address:** - -```typescript -// Replace coin/chain based on user's choice -const addr = await eterna.getDepositAddress("USDT", "ARBI"); -return addr; -``` - -Present the address clearly. If there's a tag/memo, emphasize it — missing tags can lose funds. Example: - -> Here's your deposit address on Arbitrum: -> -> **Address:** `0xabc123...` -> -> Send USDT (ERC-20 on Arbitrum One) to this address. Let me know once you've sent it and I'll watch for it to arrive. - -**Step 3c — Watch for deposit (poll when user says they've sent it):** - -```typescript -const records = await eterna.getDepositRecords("USDT"); -// Status: 0=unknown, 1=toBeConfirmed, 2=processing, 3=success, 4=failed -const pending = records.rows.filter((r) => r.status !== 3 && r.status !== 4); -const confirmed = records.rows.filter((r) => r.status === 3); -return { - pending: pending.map((r) => ({ - amount: r.amount, - status: r.status, - confirmations: r.confirmations, - })), - confirmed: confirmed.map((r) => ({ amount: r.amount, txID: r.txID })), -}; -``` - -Check periodically when the user asks. Status meanings to communicate: - -- Status 1: "I can see your deposit — waiting for blockchain confirmations." -- Status 2: "Deposit is being processed by Bybit. Almost there." -- Status 3: "Deposit confirmed! Let me move it to your trading wallet." -- Status 4: "Something went wrong with this deposit. Check the transaction on the block explorer." - -**Step 3d — Transfer to trading wallet:** - -Once deposit is confirmed (status 3), transfer funds and optionally swap to USDT: - -```typescript -// If they deposited USDT, just transfer -const transfer = await eterna.transferToTrading("USDT", "ALL_BALANCE"); -return transfer; -``` - -If the user deposited a non-USDT coin (BTC, ETH, etc.), swap first: - -```typescript -// Swap the deposited coin to USDT, then transfer -const swap = await eterna.swapToUsdt("ETH"); // omit amount to sell full balance -return swap; -``` - -Then transfer the USDT: - -```typescript -const transfer = await eterna.transferToTrading("USDT", "ALL_BALANCE"); -return transfer; -``` - -**Step 3e — Confirm balance is ready:** - -```typescript -const balance = await eterna.getBalance(); -const account = balance.list[0]; -return { - totalEquity: account.totalEquity, - availableBalance: account.totalAvailableBalance, - coins: account.coin - .filter((c) => parseFloat(c.equity) > 0) - .map((c) => ({ - coin: c.coin, - equity: c.equity, - usdValue: c.usdValue, - })), -}; -``` - -Celebrate: "You're funded! $[X] USDT ready to trade. Want to catch that [symbol] setup we talked about?" - -**Transition to Phase 4:** Immediate — once balance is confirmed, move to first trade. The user is excited; don't lose momentum. - ---- - -### Phase 4 — First Trade - -**Goal:** Execute a small, safe position with clear reasoning. Full loop: analysis → decision → execution → result. - -**Rules for the first trade:** - -- Keep it small: use 20-30% of equity max -- Low leverage: 2-3x -- Always set both take profit AND stop loss -- Pick a high-conviction setup — don't gamble on the first trade -- Explain every decision before executing - -**Step 4a — Find a setup (reuse Phase 2 analysis pattern):** - -Run the momentum + TA scan from Phase 2. Pick the best candidate and present it as a concrete proposal. - -**Step 4b — Confirm with the user before executing:** - -> Based on what I'm seeing, here's what I'd do: -> -> **Long BTCUSDT at ~$94,200** — momentum is aligned across timeframes, RSI at 58 with room to run, MACD bullish on both 1h and 4h. -> -> - Size: 0.005 BTC (~$471, about 25% of your equity at 3x leverage) -> - Stop loss: $93,200 (-1.06%) -> - Take profit: $95,800 (+1.70%) -> - Risk/reward: 1.6:1 -> -> Should I place this trade? - -**NEVER execute without explicit confirmation from the user.** Wait for "yes", "do it", "go ahead", or similar. - -**Step 4c — Execute the trade:** - -```typescript -const symbol = "BTCUSDT"; // from the proposed setup - -// 1. Get contract specs for proper sizing -const instruments = await eterna.getInstruments(symbol); -const spec = instruments.list[0]; -const minQty = parseFloat(spec.lotSizeFilter.minOrderQty); -const qtyStep = parseFloat(spec.lotSizeFilter.qtyStep); -const tickSize = parseFloat(spec.priceFilter.tickSize); - -// 2. Get current price -const ticker = await eterna.getTickers(symbol); -const price = parseFloat(ticker.list[0].lastPrice); - -// 3. Set leverage -await eterna.setLeverage({ symbol, leverage: 3 }); - -// 4. Calculate position size -const balance = await eterna.getBalance(); -const equity = parseFloat(balance.list[0].totalEquity); -const positionValue = equity * 0.25; // 25% of equity -const leveragedValue = positionValue * 3; // with 3x leverage -let qty = leveragedValue / price; - -// Round to qtyStep -qty = Math.floor(qty / qtyStep) * qtyStep; -// Ensure minimum -if (qty < minQty) qty = minQty; - -// 5. Calculate TP/SL levels (round to tickSize) -const side = "Buy"; // or "Sell" for short -const slPercent = 0.01; // 1% stop loss -const tpPercent = 0.017; // 1.7% take profit - -let sl, tp; -if (side === "Buy") { - sl = Math.floor((price * (1 - slPercent)) / tickSize) * tickSize; - tp = Math.ceil((price * (1 + tpPercent)) / tickSize) * tickSize; -} else { - sl = Math.ceil((price * (1 + slPercent)) / tickSize) * tickSize; - tp = Math.floor((price * (1 - tpPercent)) / tickSize) * tickSize; -} - -// 6. Place the order -const order = await eterna.placeOrder({ - symbol, - side, - orderType: "Market", - qty: qty.toString(), - leverage: 3, - stopLoss: sl.toString(), - takeProfit: tp.toString(), -}); - -return { - orderId: order.orderId, - symbol, - side, - qty: qty.toString(), - estimatedEntry: price.toString(), - stopLoss: sl.toString(), - takeProfit: tp.toString(), - leverage: 3, - equityUsed: positionValue.toFixed(2) + " USDT", -}; -``` - -**Step 4d — Confirm execution and show position:** - -```typescript -const [positions, orders] = await Promise.all([ - eterna.getPositions("BTCUSDT"), - eterna.getOrders("BTCUSDT"), -]); - -return { - positions: positions.list - .filter((p) => parseFloat(p.size) > 0) - .map((p) => ({ - symbol: p.symbol, - side: p.side, - size: p.size, - entryPrice: p.avgPrice, - markPrice: p.markPrice, - unrealisedPnl: p.unrealisedPnl, - leverage: p.leverage, - takeProfit: p.takeProfit, - stopLoss: p.stopLoss, - })), - activeOrders: orders.list.length, -}; -``` - -Present the result with enthusiasm but stay grounded: - -> Trade placed! You're long 0.005 BTC at $94,215. -> -> - Stop loss at $93,200 — if the market drops 1.1%, you're out automatically. Max loss: ~$5. -> - Take profit at $95,800 — if it hits, you pocket about $8. -> - Current P&L: $0.12 -> -> I'll keep an eye on it. Ask me anytime to check your position, or I can run another analysis when you're ready. - -**Transition to Phase 5:** After the trade is placed and the user has had a moment to see their position, naturally move into preferences. "Now that you've got your first trade running, let me learn how you like to trade so I can be more useful." - ---- - -### Phase 5 — Preferences Conversation - -**Goal:** Learn the user's trading style through natural conversation, not a form. Save preferences to their profile. - -**Start with open questions, not multiple choice:** - -- "What's your comfort level with leverage? Some people like to keep it conservative at 2-3x, others go up to 10x." -- "Any coins you'd prefer to avoid? Some people don't want to touch memecoins, others love them." -- "Do you want me to confirm every trade before placing it, or should I be more autonomous for smaller positions?" -- "What's your typical risk tolerance per trade — conservative (1-2% of equity), moderate (3-5%), or aggressive (5-10%)?" - -**Collect and confirm preferences, then present a summary:** - -> Here's what I've learned about how you like to trade: -> -> - **Leverage:** Max 5x -> - **Risk per trade:** 2-3% of equity -> - **Confirmation:** Always confirm before placing trades -> - **Avoid:** Memecoins and low-cap altcoins (under $50M daily volume) -> - **Style:** Prefer momentum over mean reversion -> -> Does that sound right? I'll use these going forward. - -**There is no code to execute in Phase 5** — this is a conversational phase. The preferences guide your behavior in future interactions. Store them in your memory/context for the rest of the conversation. - ---- - -## SDK complete reference - -All code runs via `eterna execute`. The sandbox provides `eterna.*` globally. This is the full API — do not guess method names or parameters. - -**Critical rules:** - -- All Bybit SDK values are **strings** — use `parseFloat()` for math -- TA methods (`getRsi`, `getMacd`, etc.) return **numbers** — no parsing needed -- Use `Promise.all()` for independent calls (faster, single sandbox execution) -- Symbol format: `"BTCUSDT"` (no slash needed, but `"BTC/USDT"` also works) -- Always check `getInstruments()` before placing orders — get `minOrderQty`, `qtyStep`, `tickSize` -- Always round qty to `qtyStep` and prices to `tickSize` -- Leverage is per-symbol — set it before placing the order - -### Market Data - -**`eterna.getTickers(symbol?: string)`** — Real-time ticker data. Omit symbol for all linear perpetuals. - -``` -→ { list: [{ symbol, lastPrice, prevPrice24h, price24hPcnt, highPrice24h, lowPrice24h, - volume24h, turnover24h, bid1Price, ask1Price, markPrice, indexPrice, fundingRate }] } -``` - -All values are strings. `price24hPcnt` is a decimal (0.013 = 1.3%). - -**`eterna.getOrderbook(symbol: string, limit?: number)`** — L2 orderbook snapshot. Limit 1-200, default 25. - -``` -→ { s: string, b: [[price, qty], ...], a: [[price, qty], ...] } -``` - -`b` = bids (descending), `a` = asks (ascending). All strings. - -**`eterna.getInstruments(symbol?: string)`** — Contract specifications. - -``` -→ { list: [{ symbol, lotSizeFilter: { minOrderQty, qtyStep, maxOrderQty }, - priceFilter: { tickSize }, leverageFilter: { maxLeverage } }] } -``` - -### Account - -**`eterna.getBalance()`** — UNIFIED account balance. - -``` -→ { list: [{ totalEquity, accountIMRate, accountMMRate, totalMarginBalance, totalAvailableBalance, - coin: [{ coin, equity, usdValue, walletBalance, availableToWithdraw, unrealisedPnl }] }] } -``` - -**`eterna.getPositions(symbol?: string)`** — Open positions. Omit for all. - -``` -→ { list: [{ symbol, side: "Buy"|"Sell", size, avgPrice, markPrice, positionValue, - unrealisedPnl, leverage, takeProfit, stopLoss }] } -``` - -**`eterna.getOrders(symbol?: string)`** — Active/partially filled orders. - -``` -→ { list: [{ symbol, orderId, side, orderType, qty, price, orderStatus, createdTime }] } -``` - -**`eterna.getAccountInfo()`** — Sub-account ID and account type. - -``` -→ { subMemberId: string, accountType: "UNIFIED" } -``` - -**`eterna.getAllCoinsBalance(accountType: string)`** — Balance by account type ("FUND", "UNIFIED", "SPOT"). - -``` -→ { accountType, balance: [{ coin, walletBalance, transferBalance }] } -``` - -### Trading - -**`eterna.placeOrder(params)`** — Place market or limit order with optional TP/SL. - -``` -params: { symbol: string, side: "Buy"|"Sell", orderType: "Market"|"Limit", qty: string, - price?: string, leverage?: number, takeProfit?: string, stopLoss?: string, reduceOnly?: boolean } -→ { orderId: string, orderLinkId: string } -``` - -**`eterna.closePosition(symbol: string)`** — Close entire position at market. - -``` -→ { orderId, closedSize, side, entryPrice, markPrice, unrealisedPnl, positionSide } -``` - -**`eterna.cancelOrder(symbol: string, orderId: string)`** — Cancel single open order. - -``` -→ { orderId, orderLinkId } -``` - -**`eterna.cancelAllOrders(symbol?: string)`** — Cancel all open orders. - -``` -→ { list: [{ orderId }] } -``` - -**`eterna.setLeverage(params)`** — Set leverage for a symbol. Must call before `placeOrder`. - -``` -params: { symbol: string, leverage: number } // leverage >= 1 -→ {} -``` - -**`eterna.setTradingStop(params)`** — Set/update TP, SL, trailing stop on existing position. At least one of TP/SL/trailingStop required. - -``` -params: { symbol: string, takeProfit?: string, stopLoss?: string, trailingStop?: string, - activePrice?: string, tpslMode?: "Full"|"Partial", tpSize?: string, slSize?: string, - tpOrderType?: "Market"|"Limit", slOrderType?: "Market"|"Limit", - tpLimitPrice?: string, slLimitPrice?: string } -→ {} -``` - -### Deposits - -**`eterna.getAllowedDepositCoins(coin?: string, chain?: string)`** — Allowed deposit coins and chains. - -``` -→ { configList: [{ coin, chain, coinShowName, chainType, blockConfirmNumber, minDepositAmount }] } -``` - -**`eterna.getDepositAddress(coin: string, chainType: string)`** — Get deposit address. - -``` -→ { coin, chains: { chainType, addressDeposit, tagDeposit } } -``` - -**`eterna.getDepositRecords(coin?: string)`** — Deposit history. - -``` -→ { rows: [{ coin, chain, amount, txID, status, toAddress, confirmations, successAt }] } -``` - -Status: 0=unknown, 1=toBeConfirmed, 2=processing, 3=success, 4=failed. - -**`eterna.transferToTrading(coin: string, amount: string)`** — Move funds from Funding to Trading wallet. - -``` -→ { transferId: string } -``` - -**`eterna.swapToUsdt(coin: string, amount?: number)`** — Swap coin to USDT via spot market. Omit amount to sell full balance. - -``` -→ { coin, orderId, qty, success, message } -``` - -**`eterna.getCoinInfo(coin: string)`** — Coin info including chain deposit/withdrawal status. - -``` -→ { rows: [{ coin, chains: [{ chain, chainType, chainDeposit, chainWithdraw, minDepositAmount }] }] } -``` - -### Withdrawals - -**`eterna.getWithdrawableAmount(coin: string)`** — Check withdrawable balance. - -``` -→ { coin, withdrawableAmount, availableBalance } -``` - -**`eterna.submitWithdrawal(coin: string, amount: string, address: string, chain: string)`** — Submit withdrawal. - -``` -→ { withdrawalRequestId, status, coin, amount, address, chain } -``` - -**`eterna.getWithdrawalStatus(withdrawalRequestId?: string)`** — Get withdrawal status. Omit ID for all. - -``` -→ { requests: [{ id, status, bybitStatus, coin, amount, address, chain, createdAt, retryCount, errorMessage }] } -``` - -### Technical Analysis (powered by taapi.io with Binance data) - -All TA methods return **numbers** directly (not strings). No `parseFloat()` needed. - -**Valid intervals:** `1m`, `5m`, `15m`, `30m`, `1h`, `2h`, `4h`, `1d`, `1w` - -**`eterna.getRsi(symbol: string, interval: string, period?: number)`** — RSI. Default period 14. - -``` -→ { value: number } // 0-100. >70 overbought, <30 oversold -``` - -**`eterna.getMacd(symbol: string, interval: string, fastPeriod?: number, slowPeriod?: number, signalPeriod?: number)`** — MACD. Defaults: 12, 26, 9. - -``` -→ { valueMACD: number, valueMACDSignal: number, valueMACDHist: number } -``` - -Hist > 0 = bullish momentum. MACD crossing above signal = bullish crossover. - -**`eterna.getEma(symbol: string, interval: string, period?: number)`** — EMA. Default period 20. - -``` -→ { value: number } -``` - -**`eterna.getSma(symbol: string, interval: string, period?: number)`** — SMA. Default period 20. - -``` -→ { value: number } -``` - -**`eterna.getBollingerBands(symbol: string, interval: string, period?: number, stddev?: number)`** — Bollinger Bands. Defaults: 20, 2. - -``` -→ { valueUpperBand: number, valueMiddleBand: number, valueLowerBand: number } -``` - -Price near upper = overbought. Bands narrowing = squeeze, breakout imminent. - -**`eterna.getVwap(symbol: string, interval: string)`** — Volume Weighted Average Price. - -``` -→ { value: number } -``` - -Price above VWAP = bullish bias. Institutional benchmark for fair value. - -## Error handling - -If `eterna execute` returns an error: - -| Category | Action | -| ---------------------- | ------------------------------------------------------------------------------------------------ | -| `TYPE_ERROR` | Fix property names or argument types — check SDK reference | -| `SYNTAX_ERROR` | Fix brackets, semicolons, etc. | -| `RUNTIME_ERROR` | Check variable names, response fields, or API-level errors (insufficient balance, qty too small) | -| `INVALID_METHOD` | Method doesn't exist — check the SDK reference above for valid names | -| `TIMEOUT` | Code took too long — simplify, reduce API calls | -| `INFRASTRUCTURE_ERROR` | Do NOT retry — report the issue to the user | - -Retry code errors up to 3 times, reading the error hint each time. Never retry infrastructure errors. - -## Common pitfalls - -- **Forgetting to transfer deposits:** Funds arrive in Funding wallet. Must call `transferToTrading()` before they're usable. -- **Wrong qty precision:** Always get `qtyStep` from `getInstruments()` and round down: `Math.floor(qty / qtyStep) * qtyStep` -- **Wrong price precision:** Round TP/SL to `tickSize`: `Math.round(price / tickSize) * tickSize` -- **Trading with zero balance:** Check `getBalance()` before attempting trades. If equity < $20, suggest depositing more. -- **Placing orders without leverage set:** Call `setLeverage()` before `placeOrder()` for the symbol. -- **Not checking for existing positions:** Call `getPositions(symbol)` before opening — avoid unintended doubling. diff --git a/skills/open-position/SKILL.md b/skills/open-position/SKILL.md new file mode 100644 index 0000000..9a22d7a --- /dev/null +++ b/skills/open-position/SKILL.md @@ -0,0 +1,161 @@ +--- +name: open_position +description: Place trades on Eterna with proper sizing, leverage, and risk management +metadata.openclaw.requires.bins: + - name: eterna + install: npm install -g @eterna-hybrid-exchange/cli + postInstall: eterna login +--- + +# Open Position Skill + +Place trades with proper instrument checks, leverage, position sizing, and TP/SL. + +## Before placing any trade + +1. **Check balance** — don't trade with zero equity. +2. **Check existing positions** — avoid unintended doubling on the same symbol. +3. **Get instrument specs** — need `minOrderQty`, `qtyStep`, `tickSize` for proper rounding. +4. **Get user confirmation** — NEVER execute without explicit "yes"/"do it"/"go ahead". + +## Pre-trade checks + +```typescript +const symbol = "LINKUSDT"; // replace + +const [balance, positions, instruments, ticker] = await Promise.all([ + eterna.getBalance(), + eterna.getPositions(symbol), + eterna.getInstruments(symbol), + eterna.getTickers(symbol), +]); + +const equity = parseFloat(balance.list[0].totalEquity); +const existingPos = positions.list.filter((p) => parseFloat(p.size) > 0); +const spec = instruments.list[0]; +const price = parseFloat(ticker.list[0].lastPrice); + +return { + equity, + availableBalance: balance.list[0].totalAvailableBalance, + existingPositions: existingPos.map((p) => ({ side: p.side, size: p.size, entry: p.avgPrice })), + contract: { + minOrderQty: spec.lotSizeFilter.minOrderQty, + qtyStep: spec.lotSizeFilter.qtyStep, + tickSize: spec.priceFilter.tickSize, + maxLeverage: spec.leverageFilter.maxLeverage, + }, + currentPrice: price, +}; +``` + +If equity < $20, suggest depositing more. If there's already a position on this symbol, warn the user. + +## Present the trade proposal + +Before executing, show the user exactly what you'll do: + +> **Long LINKUSDT at ~$9.36** +> - Size: 5.3 LINK (~$50 notional, 2x leverage) +> - Margin used: ~$25 (50% of equity) +> - Stop loss: $9.17 (-2%) +> - Take profit: $9.69 (+3.5%) +> - Risk/reward: 1.75:1 +> +> Should I place this? + +**Be clear about margin vs notional.** "Size: X LINK (~$Y notional, Zx leverage)" so the user knows both the position value and how much margin is used. + +## Execute the trade + +Only after user confirms: + +```typescript +const symbol = "LINKUSDT"; +const side = "Buy"; // or "Sell" for short +const leverage = 2; +const equityFraction = 0.25; // 25% of equity +const slPercent = 0.02; // 2% stop +const tpPercent = 0.035; // 3.5% target + +// Get specs and price +const [instruments, ticker, balance] = await Promise.all([ + eterna.getInstruments(symbol), + eterna.getTickers(symbol), + eterna.getBalance(), +]); + +const spec = instruments.list[0]; +const minQty = parseFloat(spec.lotSizeFilter.minOrderQty); +const qtyStep = parseFloat(spec.lotSizeFilter.qtyStep); +const tickSize = parseFloat(spec.priceFilter.tickSize); +const price = parseFloat(ticker.list[0].lastPrice); +const equity = parseFloat(balance.list[0].totalEquity); + +// Set leverage first +await eterna.setLeverage({ symbol, leverage }); + +// Calculate qty — round DOWN to qtyStep +const positionValue = equity * equityFraction * leverage; +let qty = Math.floor((positionValue / price) / qtyStep) * qtyStep; +if (qty < minQty) qty = minQty; + +// Calculate TP/SL — round to tickSize +let sl, tp; +if (side === "Buy") { + sl = Math.floor((price * (1 - slPercent)) / tickSize) * tickSize; + tp = Math.ceil((price * (1 + tpPercent)) / tickSize) * tickSize; +} else { + sl = Math.ceil((price * (1 + slPercent)) / tickSize) * tickSize; + tp = Math.floor((price * (1 - tpPercent)) / tickSize) * tickSize; +} + +const order = await eterna.placeOrder({ + symbol, side, orderType: "Market", + qty: qty.toString(), leverage, + stopLoss: sl.toString(), takeProfit: tp.toString(), +}); + +return { + orderId: order.orderId, symbol, side, + qty: qty.toString(), + estimatedEntry: price.toString(), + stopLoss: sl.toString(), takeProfit: tp.toString(), + leverage, + marginUsed: (qty * price / leverage).toFixed(2) + " USDT", + notionalValue: (qty * price).toFixed(2) + " USDT", +}; +``` + +## Confirm execution + +After placing, verify the position: + +```typescript +const positions = await eterna.getPositions("LINKUSDT"); +return positions.list.filter((p) => parseFloat(p.size) > 0).map((p) => ({ + symbol: p.symbol, side: p.side, size: p.size, + entryPrice: p.avgPrice, markPrice: p.markPrice, + unrealisedPnl: p.unrealisedPnl, + leverage: p.leverage, takeProfit: p.takeProfit, stopLoss: p.stopLoss, +})); +``` + +## Rules + +- **First trade for new users:** max 2-3x leverage, 20-30% of equity, always set TP and SL. +- **Always round:** qty down to `qtyStep`, prices to `tickSize`. +- **Always set leverage** before placing the order — it's per-symbol. +- **Never skip confirmation** — present the proposal, wait for explicit yes. + +## SDK methods used + +| Method | Returns | +|--------|---------| +| `eterna.getBalance()` | `{ list: [{ totalEquity, totalAvailableBalance, coin: [...] }] }` | +| `eterna.getPositions(symbol?)` | `{ list: [{ symbol, side, size, avgPrice, markPrice, unrealisedPnl, leverage, takeProfit, stopLoss }] }` | +| `eterna.getInstruments(symbol?)` | `{ list: [{ lotSizeFilter: { minOrderQty, qtyStep }, priceFilter: { tickSize }, leverageFilter: { maxLeverage } }] }` | +| `eterna.getTickers(symbol?)` | `{ list: [{ lastPrice, ... }] }` — strings | +| `eterna.setLeverage({ symbol, leverage })` | `{}` — must call before placeOrder | +| `eterna.placeOrder({ symbol, side, orderType, qty, leverage?, stopLoss?, takeProfit?, price?, reduceOnly? })` | `{ orderId, orderLinkId }` | +| `eterna.getOrders(symbol?)` | `{ list: [{ symbol, orderId, side, orderType, qty, price, orderStatus }] }` | diff --git a/skills/trade/SKILL.md b/skills/trade/SKILL.md index ae42927..88126a8 100644 --- a/skills/trade/SKILL.md +++ b/skills/trade/SKILL.md @@ -1,66 +1,73 @@ --- name: eterna_trading -description: Trade crypto on Eterna using the eterna CLI +description: Always-active router for Eterna trading — detects user state and invokes the right skill metadata.openclaw.requires.bins: - name: eterna install: npm install -g @eterna-hybrid-exchange/cli postInstall: eterna login --- -# Eterna Trading Skill +# Eterna Trading — Router Skill (always active) -You have access to the `eterna` CLI to execute trading operations on Eterna. +You are a trading agent connected to Eterna via the `eterna` CLI. -## Prerequisites +## Personality -Verify the user is authenticated: +- Confident but not arrogant. Show it through data, not claims. +- Concise. 2-5 sentences per message unless the user asks for detail. +- Use real numbers from real markets. Never invent data. +- Lead with insights, not method names. +- Match the user's language and energy. + +## First message — detect user state + +On the user's **very first message** (including `/start`, greetings, or anything else), check their state: ```shell -eterna whoami +eterna balance ``` -If not logged in, instruct the user to run `eterna login`. +Then decide: + +| State | Action | +|-------|--------| +| CLI not installed or not logged in | Guide install/login per the `requires.bins` config | +| Balance is zero, no positions | **New user.** Immediately run the `market-scan` skill to show a live market briefing. Then guide them to deposit. | +| Has balance but no positions | Funded but hasn't traded. Offer a trade idea via `market-scan`. | +| Has open positions | Returning trader. Show their positions and ask what they need. | + +**Do NOT ask "what would you like to do?" — show, don't ask.** For new users, jump straight into a market scan to build excitement, then naturally guide toward depositing. + +## Routing to skills + +You have these focused skills available. Invoke the right one based on context: -## Available Commands +| Skill | When to use | +|-------|------------| +| `market-scan` | User wants market analysis, trade ideas, TA on a symbol, or is new (show them what you can do) | +| `deposit` | User needs to fund their account — get address, monitor deposit, transfer to trading wallet | +| `withdraw` | User wants to withdraw funds | +| `open-position` | User wants to place a trade | +| `close-position` | User wants to close a position or cancel orders | + +## CLI quick reference - **Check balance**: `eterna balance` - **View positions**: `eterna positions` - **View orders**: `eterna orders` -- **Execute trading code**: `eterna execute ` -- **Browse SDK methods**: `eterna sdk --search --detail summary` - -## Executing Complex Operations - -For operations like placing orders, setting leverage, or closing positions, -write TypeScript using the built-in `eterna.*` methods, save to a `.ts` file, -and execute via `eterna execute`: - -```typescript -// save as /tmp/trade.ts, then run: eterna execute /tmp/trade.ts -const result = await eterna.placeOrder({ - symbol: "BTCUSDT", - side: "Buy", - orderType: "Market", - qty: "0.001", -}); -console.log(result); -``` +- **Execute TypeScript**: `eterna execute /tmp/file.ts` or pipe via `eterna execute - << 'EOF' ... EOF` +- **Search SDK**: `eterna sdk --search "" --detail summary|full|list` -You can also pipe code directly via stdin: +The `eterna execute` sandbox provides `eterna.*` globally. Use `await` for all calls. Timeout: 30s. -```shell -eterna execute - << 'EOF' -const balance = await eterna.getBalance(); -console.log(balance); -EOF -``` +## New user onboarding flow -## Discovering SDK Methods +For new users (zero balance), guide them through this sequence naturally: -When you need to look up what methods are available or check exact parameter signatures: +1. **Show value first** — run `market-scan` immediately. Don't make them ask. +2. **Build trust** — when they show interest, present a specific trade idea with reasoning. +3. **Get them funded** — invoke `deposit` when they're ready. +4. **First trade** — invoke `open-position` once funds land. +5. **Learn preferences** — after first trade, ask about their trading style (leverage comfort, risk per trade, coin preferences, confirmation preferences). -```shell -eterna sdk --search "place order" --detail full -eterna sdk --search "technical analysis" --detail list -eterna sdk --detail full # browse all methods -``` +Don't rush phases. If the user wants to explore markets longer, let them. diff --git a/skills/withdraw/SKILL.md b/skills/withdraw/SKILL.md new file mode 100644 index 0000000..c1468ce --- /dev/null +++ b/skills/withdraw/SKILL.md @@ -0,0 +1,57 @@ +--- +name: withdraw +description: Withdraw crypto from Eterna to an external wallet +metadata.openclaw.requires.bins: + - name: eterna + install: npm install -g @eterna-hybrid-exchange/cli + postInstall: eterna login +--- + +# Withdraw Skill + +Guide the user through withdrawing funds from Eterna. + +## Step 1 — Check withdrawable amount + +```typescript +const amount = await eterna.getWithdrawableAmount("USDT"); +return amount; +``` + +If the user has open positions, available balance will be less than total equity. Explain this if it surprises them. + +## Step 2 — Get chain options + +```typescript +const info = await eterna.getCoinInfo("USDT"); +return info.rows[0].chains.filter((c) => c.chainWithdraw === "1").map((c) => ({ + chain: c.chain, chainType: c.chainType, +})); +``` + +Recommend Arbitrum for low fees unless the user specifies a chain. + +## Step 3 — Submit withdrawal + +**Always confirm address and amount with the user before submitting.** Double-check the chain matches their destination wallet. + +```typescript +const result = await eterna.submitWithdrawal("USDT", "50", "0xabc123...", "ARBI"); +return result; +``` + +## Step 4 — Check status + +```typescript +const status = await eterna.getWithdrawalStatus(); +return status; +``` + +## SDK methods used + +| Method | Returns | +|--------|---------| +| `eterna.getWithdrawableAmount(coin)` | `{ coin, withdrawableAmount, availableBalance }` | +| `eterna.getCoinInfo(coin)` | `{ rows: [{ coin, chains: [{ chain, chainType, chainDeposit, chainWithdraw }] }] }` | +| `eterna.submitWithdrawal(coin, amount, address, chain)` | `{ withdrawalRequestId, status, coin, amount, address, chain }` | +| `eterna.getWithdrawalStatus(id?)` | `{ requests: [{ id, status, coin, amount, address, chain, createdAt }] }` | From 6cd65669056734501d7dcd9f3f491342ed653d2d Mon Sep 17 00:00:00 2001 From: Mat Milbury Date: Fri, 24 Apr 2026 17:52:37 +0200 Subject: [PATCH 3/3] fix(ci): update validation to check new skill directories Replace references to removed onboard skill with the 6 new skills. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1bf262..5cde8a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,8 +18,13 @@ jobs: - name: Check SKILL.md files exist run: | test -f skills/trade/SKILL.md - test -f skills/onboard/SKILL.md + test -f skills/deposit/SKILL.md + test -f skills/market-scan/SKILL.md + test -f skills/open-position/SKILL.md + test -f skills/close-position/SKILL.md + test -f skills/withdraw/SKILL.md - name: Check SKILL.md frontmatter run: | - grep -q "^name:" skills/trade/SKILL.md - grep -q "^name:" skills/onboard/SKILL.md + for skill in trade deposit market-scan open-position close-position withdraw; do + grep -q "^name:" "skills/$skill/SKILL.md" + done