From 5d82e064a95f067af1b49e27e2aead7181659445 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 09:12:02 +0000 Subject: [PATCH] FIZZL_TRADE_SETUPS: liquid_top and the liquidity rank (0.8.0) Ichimoku /setups now ranks liquidity by global trading volume and accepts liquid_top (only the N most traded coins). The action passes it on: "liquid" or "most traded" asks for the 30 most traded coins, "50 most traded" for 50, or options.liquid_top; out-of-range values are dropped. Each line shows the coin's liquidity rank, and the header names the filter. Types, description, README and tests (parser cases and the paid path with liquid_top=30). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TsD5haPKxeWjYmErHzvU48 --- README.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/actions/trading-signals.ts | 8 ++++---- src/parse.ts | 9 ++++++++- src/services.ts | 6 +++++- test/actions.test.mjs | 11 +++++++++++ 7 files changed, 32 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f69a413..e97577c 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Agent: Market scan 4h: 98 of 148 coins bullish (98 bullish · 21 neutral · 29 b | Action | Service | What it does | Price | Pays on | |--------|---------|--------------|-------|---------| -| `FIZZL_TRADE_SETUPS` | [Ichimoku Signal](https://ichimoku-signal.onrender.com) | Which coins have a trade setup now: for 148 top-200 coins the six-indicator signal plus entry, stop, two targets and risk/reward, ranked best first; "long only", "R/R 2", "top 5" and the interval come from the question (default 4h) | $0.50 | Solana or Base | +| `FIZZL_TRADE_SETUPS` | [Ichimoku Signal](https://ichimoku-signal.onrender.com) | Which coins have a trade setup now: for 148 top-200 coins the six-indicator signal plus entry, stop, two targets and risk/reward, ranked best first, with each coin's liquidity rank by global trading volume (thin coins rank lower); "long only", "R/R 2", "top 5", "liquid" (the 30 most traded coins) or "50 most traded" and the interval come from the question (default 4h) | $0.50 | Solana or Base | | `FIZZL_ICHIMOKU_SIGNAL` | [Ichimoku Signal](https://ichimoku-signal.onrender.com) | Live Ichimoku Cloud signal for a crypto pair: bullish/bearish/neutral, cloud position, tenkan/kijun cross and all line values | $0.02 | Solana or Base | | `FIZZL_CONFLUENCE_SIGNAL` | [Ichimoku Signal](https://ichimoku-signal.onrender.com) | Six indicators in one call (Ichimoku, RSI, MACD, EMA 50/200, Bollinger, volume), each with a vote, plus a combined signal and confidence | $0.15 | Solana or Base | | `FIZZL_PRICE_LEVELS` | [Ichimoku Signal](https://ichimoku-signal.onrender.com) | Support/resistance, ATR, and a long/short plan with stop, two targets and risk/reward (levels, not advice) | $0.05 | Solana or Base | diff --git a/package-lock.json b/package-lock.json index 1e2daae..3a915c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "plugin-fizzl", - "version": "0.6.3", + "version": "0.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "plugin-fizzl", - "version": "0.6.3", + "version": "0.8.0", "license": "MIT", "dependencies": { "@solana/kit": "^8.3.0", diff --git a/package.json b/package.json index d9804e5..83ff2a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plugin-fizzl", - "version": "0.7.0", + "version": "0.8.0", "description": "ElizaOS plugin: 10 paid tools for crypto agents, paid per call in USDC over x402 on Solana or Base (no API keys). Trading signals (ranked trade setups with entry, stop and targets across 148 coins, Ichimoku, 6-indicator confluence, price levels, a market scan), wallet-approval risk checks, pre-sign checks and x402 endpoint checks.", "type": "module", "main": "dist/index.js", diff --git a/src/actions/trading-signals.ts b/src/actions/trading-signals.ts index d3578a0..fd6b6b8 100644 --- a/src/actions/trading-signals.ts +++ b/src/actions/trading-signals.ts @@ -156,21 +156,21 @@ export const marketScanAction: Action = { const SETUPS_TRIGGER = /\b(trade setups?|setups?|trade ideas?|trading opportunit(?:y|ies)|best trades?|what (?:should|can|could) i (?:trade|buy|long|short)|which coins? (?:should|can|could|to) (?:i )?(?:trade|buy|long|short)|coins? to (?:trade|long|short))\b/i; const setupLine = (s: TradeSetups["setups"][number]) => - `${s.rank}. ${s.pair.replace(/-USDT$/, "")} ${s.direction.toUpperCase()} · entry ${fmt(s.entry)} · stop ${fmt(s.stop)} · targets ${fmt(s.target_1)} / ${fmt(s.target_2)} · R/R ${fmt(s.risk_reward_1)} (${s.signal_from}${s.warning ? "; resistance/support right after the entry" : ""})`; + `${s.rank}. ${s.pair.replace(/-USDT$/, "")} ${s.direction.toUpperCase()} · entry ${fmt(s.entry)} · stop ${fmt(s.stop)} · targets ${fmt(s.target_1)} / ${fmt(s.target_2)} · R/R ${fmt(s.risk_reward_1)}${s.liquidity_rank ? ` · liquidity #${s.liquidity_rank}` : ""} (${s.signal_from}${s.warning ? "; resistance/support right after the entry" : ""})`; export function formatSetups(t: TradeSetups): string { const f = t.filters; - const which = [f.direction !== "both" && `${f.direction} only`, `R/R ≥ ${fmt(f.min_risk_reward)}`].filter(Boolean).join(", "); + const which = [f.direction !== "both" && `${f.direction} only`, `R/R ≥ ${fmt(f.min_risk_reward)}`, f.liquid_top && `${f.liquid_top} most traded coins`].filter(Boolean).join(", "); const head = `Trade setups ${t.interval}: ${t.setups_found} in ${t.coins_scanned} coins (${t.summary.long} long · ${t.summary.short} short; ${which})`; const lines = t.setups.length ? t.setups.map(setupLine) : ["No setup matches right now. Try a lower minimum R/R or another interval."]; - return [head, ...lines, "Ranked by signal strength × risk/reward, lower for high volatility and nearby support/resistance. Not trade advice."].join("\n"); + return [head, ...lines, "Ranked by signal strength × risk/reward, lower for high volatility, nearby support/resistance and thinly traded coins (liquidity # = rank by global trading volume). Not trade advice."].join("\n"); } export const tradeSetupsAction: Action = { name: "FIZZL_TRADE_SETUPS", similes: ["TRADE_SETUPS", "TRADE_IDEAS", "WHAT_TO_TRADE", "BEST_TRADES", "RANKED_SETUPS"], description: - "Which crypto coins have a trade setup right now: for 148 top-200 coins, the six-indicator confluence signal (Ichimoku, RSI, MACD, EMA 50/200, Bollinger, volume) plus a trade plan in that direction (entry, stop, two take profit targets, risk/reward), ranked best first. Levels from price history, not trade advice. Paid per call via x402: $0.50 USDC on Solana or Base. Parameters: interval (1m…1M, default 4h), direction (long or short, optional), min_rr (minimum risk/reward, default 1.5), top (how many, default 10).", + "Which crypto coins have a trade setup right now: for 148 top-200 coins, the six-indicator confluence signal (Ichimoku, RSI, MACD, EMA 50/200, Bollinger, volume) plus a trade plan in that direction (entry, stop, two take profit targets, risk/reward), ranked best first. Levels from price history, not trade advice. Paid per call via x402: $0.50 USDC on Solana or Base. Parameters: interval (1m…1M, default 4h), direction (long or short, optional), min_rr (minimum risk/reward, default 1.5), top (how many, default 10), liquid_top (only the N most traded coins by global volume, e.g. 30; 'liquid setups' sets 30).", validate: async (_runtime, message) => SETUPS_TRIGGER.test(message.content?.text ?? ""), handler: async (runtime, message, _state, options, callback): Promise => { const input = parseSetupsInput(message.content?.text ?? "", (options ?? {}) as Record); diff --git a/src/parse.ts b/src/parse.ts index efadaf6..8dd6dca 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -63,10 +63,12 @@ export function parseScanInput(text: string, options: Record = return signal ? { interval, signal } : { interval }; } -export interface SetupsInput { interval: string; direction?: "long" | "short"; min_rr?: number; top?: number } +export interface SetupsInput { interval: string; direction?: "long" | "short"; min_rr?: number; top?: number; liquid_top?: number } // "best long setups on the 1d, R/R 2 or better, top 5" → { interval: "1d", direction: "long", min_rr: 2, top: 5 } // "what should I trade?" → { interval: "4h" } +// "liquid setups" / "only the most traded coins" → { interval: "4h", liquid_top: 30 } +// "setups in the 50 most traded coins" → { interval: "4h", liquid_top: 50 } export function parseSetupsInput(text: string, options: Record = {}): SetupsInput { const interval = typeof options.interval === "string" && (INTERVALS as readonly string[]).includes(options.interval) ? options.interval @@ -78,6 +80,11 @@ export function parseSetupsInput(text: string, options: Record if (Number.isFinite(rr) && rr >= 1 && rr <= 10) out.min_rr = rr; const top = typeof options.top === "number" ? options.top : Number(/\btop\s*(\d{1,2})\b/i.exec(text)?.[1]); if (Number.isInteger(top) && top >= 1 && top <= 50) out.top = top; + const liquidN = Number(/\b(\d{1,3})\s+most\s+(?:liquid|traded)\b/i.exec(text)?.[1]); + const liquid = typeof options.liquid_top === "number" ? options.liquid_top + : Number.isInteger(liquidN) ? liquidN + : /\b(liquid|most traded|high[\s-]volume)\b/i.test(text) ? 30 : undefined; + if (Number.isInteger(liquid) && (liquid as number) >= 1 && (liquid as number) <= 200) out.liquid_top = liquid; return out; } diff --git a/src/services.ts b/src/services.ts index 011b2f5..b169030 100644 --- a/src/services.ts +++ b/src/services.ts @@ -108,6 +108,8 @@ export interface TradeSetup { warning?: string; atr_percent: number; volume_24h_usd: number; + liquidity_rank?: number; + liquidity_usd_daily?: number; exchange: string; } @@ -115,7 +117,8 @@ export interface TradeSetups { interval: string; timestamp: string; coins_scanned: number; - filters: { direction: "long" | "short" | "both"; min_risk_reward: number; top: number }; + filters: { direction: "long" | "short" | "both"; min_risk_reward: number; top: number; liquid_top?: number }; + liquidity_source?: "coingecko_global_24h" | "exchange_30d_median"; setups_found: number; summary: { long: number; short: number; neutral_coins: number }; setups: TradeSetup[]; @@ -221,6 +224,7 @@ export function getTradeSetups(client: FizzlClient, urls: ServiceUrls, input: Se if (input.direction) query.set("direction", input.direction); if (input.min_rr !== undefined) query.set("min_rr", String(input.min_rr)); if (input.top !== undefined) query.set("top", String(input.top)); + if (input.liquid_top !== undefined) query.set("liquid_top", String(input.liquid_top)); return client.request(`${trim(urls.ichimoku)}/setups?${query}`); } diff --git a/test/actions.test.mjs b/test/actions.test.mjs index 46293f4..419109e 100644 --- a/test/actions.test.mjs +++ b/test/actions.test.mjs @@ -341,6 +341,13 @@ test("Trade setups: pays $0.50 on Solana with the default spend cap and lists th assert.equal(result.values.bestSetupPair, "FIL-USDT"); }); +test("Trade setups: \"liquid\" asks for the 30 most traded coins", async () => { + fx.state.payments.length = 0; + const { result } = await run(SETUPS, runtimeWith({ EVM_PRIVATE_KEY: evm.secret }), "Show me liquid setups only"); + assert.equal(result.success, true, result.error); + assert.deepEqual(fx.state.payments.map((p) => p.path), ["/setups?interval=4h&liquid_top=30"]); +}); + test("Trade setups: direction, minimum R/R, top and interval come from the question", async () => { fx.state.payments.length = 0; const { result, replies } = await run(SETUPS, runtimeWith({ EVM_PRIVATE_KEY: evm.secret }), "Top 2 long setups on the 1d with a risk reward of 2 or better"); @@ -350,6 +357,10 @@ test("Trade setups: direction, minimum R/R, top and interval come from the quest const { parseSetupsInput } = await import("../dist/index.js"); assert.deepEqual(parseSetupsInput("what should I trade?"), { interval: "4h" }); assert.deepEqual(parseSetupsInput("short setups, rr 3, top 5, 1h"), { interval: "1h", direction: "short", min_rr: 3, top: 5 }); + assert.deepEqual(parseSetupsInput("liquid setups"), { interval: "4h", liquid_top: 30 }); + assert.deepEqual(parseSetupsInput("setups in the 50 most traded coins, top 3"), { interval: "4h", top: 3, liquid_top: 50 }); + assert.deepEqual(parseSetupsInput("what should I trade?", { liquid_top: 20 }), { interval: "4h", liquid_top: 20 }); + assert.deepEqual(parseSetupsInput("setups", { liquid_top: 500 }), { interval: "4h" }, "out of range is dropped"); const v = (text) => SETUPS.validate(runtimeWith({}), message(text)); assert.equal(await v("any good trade ideas?"), true); assert.equal(await v("which coin should I long?"), true);