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/.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 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 58df48e..f6cc4e7 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -2,7 +2,24 @@ "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": { + "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/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 18087ef..0000000 --- a/skills/onboard/SKILL.md +++ /dev/null @@ -1,890 +0,0 @@ ---- -name: onboarding -description: Guided onboarding flow for new Eterna users — only activate when explicitly invoked -metadata.openclaw.requires.bins: [eterna] ---- - -# 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 7d2f501..88126a8 100644 --- a/skills/trade/SKILL.md +++ b/skills/trade/SKILL.md @@ -1,63 +1,73 @@ --- name: eterna_trading -description: Trade crypto on Eterna using the eterna CLI -metadata.openclaw.requires.bins: [eterna] +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 }] }` |