Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
8 changes: 4 additions & 4 deletions src/actions/trading-signals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ActionResult> => {
const input = parseSetupsInput(message.content?.text ?? "", (options ?? {}) as Record<string, unknown>);
Expand Down
9 changes: 8 additions & 1 deletion src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,12 @@ export function parseScanInput(text: string, options: Record<string, unknown> =
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<string, unknown> = {}): SetupsInput {
const interval = typeof options.interval === "string" && (INTERVALS as readonly string[]).includes(options.interval)
? options.interval
Expand All @@ -78,6 +80,11 @@ export function parseSetupsInput(text: string, options: Record<string, unknown>
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;
}

Expand Down
6 changes: 5 additions & 1 deletion src/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,17 @@ export interface TradeSetup {
warning?: string;
atr_percent: number;
volume_24h_usd: number;
liquidity_rank?: number;
liquidity_usd_daily?: number;
exchange: string;
}

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[];
Expand Down Expand Up @@ -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<TradeSetups>(`${trim(urls.ichimoku)}/setups?${query}`);
}

Expand Down
11 changes: 11 additions & 0 deletions test/actions.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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);
Expand Down
Loading