diff --git a/plugins/ton-trading-bot/index.js b/plugins/ton-trading-bot/index.js index 49aa969..0dc5789 100644 --- a/plugins/ton-trading-bot/index.js +++ b/plugins/ton-trading-bot/index.js @@ -2,12 +2,37 @@ * TON Trading Bot Plugin * * Granular, atomic tools for the LLM to compose trading workflows on TON: - * - ton_trading_get_market_data — fetch current prices and DEX quotes - * - ton_trading_get_portfolio — wallet balance, jetton holdings, trade history - * - ton_trading_validate_trade — check risk parameters before acting - * - ton_trading_simulate_trade — paper-trade without real money - * - ton_trading_execute_swap — execute real swap on TON DEX (DM-only) - * - ton_trading_record_trade — record a closed trade and update PnL + * - ton_trading_get_market_data — fetch current prices and DEX quotes + * - ton_trading_get_portfolio — wallet balance, jetton holdings, trade history + * - ton_trading_validate_trade — check risk parameters before acting + * - ton_trading_simulate_trade — paper-trade without real money + * - ton_trading_execute_swap — execute real swap on TON DEX (DM-only) + * - ton_trading_record_trade — record a closed trade and update PnL + * + * Algorithmic trading tools (P0 — highest priority): + * - ton_trading_get_arbitrage_opportunities — find cross-DEX price differences + * - ton_trading_get_token_listings — monitor new token launches on DEXes + * - ton_trading_get_token_info — get details for a specific token + * - ton_trading_validate_token — safety-check a token before sniping + * - ton_trading_get_top_traders — find wallets with strong track records + * - ton_trading_get_trader_performance — analyse a specific trader's history + * + * Liquidity & farming tools (P1): + * - ton_trading_get_active_pools — list active DEX liquidity pools + * - ton_trading_get_farms_with_apy — list yield farms sorted by APY + * - ton_trading_get_pool_volume — get 24-h volume for a pool + * + * Backtesting tools (P1): + * - ton_trading_backtest — replay a strategy against trade history + * + * Risk management tools (P2): + * - ton_trading_calculate_risk_metrics — VaR, max drawdown, Sharpe ratio + * - ton_trading_set_stop_loss — register a stop-loss rule in the journal + * - ton_trading_get_optimal_position_size — Kelly / fixed-fraction sizing + * + * Automation tools (P2): + * - ton_trading_schedule_trade — store a pending trade for future execution + * - ton_trading_get_scheduled_trades — list pending scheduled trades * * Pattern B (SDK) — uses sdk.ton, sdk.ton.dex, sdk.db, sdk.storage, sdk.log * @@ -17,9 +42,9 @@ export const manifest = { name: "ton-trading-bot", - version: "1.0.0", + version: "2.0.0", sdkVersion: ">=1.0.0", - description: "Atomic TON trading tools: market data, portfolio, risk validation, simulation, and DEX swap execution. The LLM composes these into trading strategies.", + description: "Atomic TON trading tools: market data, portfolio, risk validation, simulation, DEX swap execution, cross-DEX arbitrage, sniper trading, copy trading, liquidity pools, farming, backtesting, risk management, and automation. The LLM composes these into trading strategies.", defaultConfig: { maxTradePercent: 10, // max single trade as % of balance minBalanceTON: 1, // minimum TON balance required to trade @@ -55,6 +80,30 @@ export function migrate(db) { timestamp INTEGER NOT NULL, balance REAL NOT NULL ); + + -- Stop-loss rules: registered per open trade + CREATE TABLE IF NOT EXISTS stop_loss_rules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trade_id INTEGER NOT NULL, + stop_loss_percent REAL NOT NULL, -- e.g. 5 means close at -5% + take_profit_percent REAL, -- optional take-profit level + entry_price REAL NOT NULL, + created_at INTEGER NOT NULL, + status TEXT NOT NULL DEFAULT 'active' -- 'active' | 'triggered' | 'cancelled' + ); + + -- Scheduled trades: pending orders for future execution + CREATE TABLE IF NOT EXISTS scheduled_trades ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at INTEGER NOT NULL, + execute_at INTEGER NOT NULL, -- Unix ms timestamp + mode TEXT NOT NULL, -- 'real' | 'simulation' + from_asset TEXT NOT NULL, + to_asset TEXT NOT NULL, + amount REAL NOT NULL, + note TEXT, + status TEXT NOT NULL DEFAULT 'pending' -- 'pending' | 'executed' | 'cancelled' + ); `); } @@ -564,4 +613,1357 @@ export const tools = (sdk) => [ } }, }, + + // ── P0 Tools: Cross-DEX Arbitrage ───────────────────────────────────────── + + // ── Tool 7: ton_trading_get_arbitrage_opportunities ──────────────────────── + { + name: "ton_trading_get_arbitrage_opportunities", + description: + "Find cross-DEX price differences for a token pair across StonFi, DeDust, TONCO, and swap.coffee. Returns opportunities sorted by net profit after fees. Call this before deciding to execute an arbitrage trade.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + from_asset: { + type: "string", + description: 'Asset to quote from — "TON" or a jetton master address', + }, + to_asset: { + type: "string", + description: 'Asset to quote to — "TON" or a jetton master address', + }, + amount: { + type: "string", + description: 'Amount to quote in from_asset units (e.g. "1")', + }, + min_profit_percent: { + type: "number", + description: "Minimum net profit percentage to include in results (default 0.5)", + minimum: 0, + }, + }, + required: ["from_asset", "to_asset", "amount"], + }, + execute: async (params, _context) => { + const { from_asset, to_asset, amount, min_profit_percent = 0.5 } = params; + try { + const dexFees = { stonfi: 0.003, dedust: 0.003, tonco: 0.003, swapcoffee: 0.002 }; + + const quote = await sdk.ton.dex.quote({ + fromAsset: from_asset, + toAsset: to_asset, + amount: parseFloat(amount), + }).catch((err) => { + sdk.log.warn(`DEX quote failed: ${err.message}`); + return null; + }); + + if (!quote) { + return { success: false, error: "Could not fetch DEX quotes" }; + } + + // Collect per-DEX outputs + const dexOutputs = []; + for (const [dex, fee] of Object.entries(dexFees)) { + const raw = quote[dex]; + if (!raw) continue; + const outputRaw = parseFloat(raw.output ?? raw.price ?? 0); + if (outputRaw <= 0) continue; + const outputAfterFee = outputRaw * (1 - fee); + dexOutputs.push({ dex, output: outputRaw, outputAfterFee, fee }); + } + + if (dexOutputs.length < 2) { + return { + success: true, + data: { opportunities: [], note: "Not enough DEX quotes to compute arbitrage" }, + }; + } + + // Find all buy-low / sell-high pairs + const opportunities = []; + for (let i = 0; i < dexOutputs.length; i++) { + for (let j = 0; j < dexOutputs.length; j++) { + if (i === j) continue; + const buy = dexOutputs[i]; + const sell = dexOutputs[j]; + if (buy.outputAfterFee >= sell.outputAfterFee) continue; + const profitPercent = + ((sell.outputAfterFee - buy.outputAfterFee) / buy.outputAfterFee) * 100; + if (profitPercent < min_profit_percent) continue; + opportunities.push({ + buy_on: buy.dex, + sell_on: sell.dex, + buy_output: parseFloat(buy.outputAfterFee.toFixed(6)), + sell_output: parseFloat(sell.outputAfterFee.toFixed(6)), + net_profit_percent: parseFloat(profitPercent.toFixed(4)), + combined_fees_percent: parseFloat(((buy.fee + sell.fee) * 100).toFixed(3)), + }); + } + } + + opportunities.sort((a, b) => b.net_profit_percent - a.net_profit_percent); + + sdk.storage.set(`arb:${from_asset}:${to_asset}`, { opportunities, ts: Date.now() }, { ttl: 30_000 }); + + return { + success: true, + data: { + from_asset, + to_asset, + amount, + opportunities, + dex_quotes: dexOutputs.map((d) => ({ dex: d.dex, output: d.output })), + }, + }; + } catch (err) { + sdk.log.error(`ton_trading_get_arbitrage_opportunities failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, + + // ── P0 Tools: Sniper Trading ─────────────────────────────────────────────── + + // ── Tool 8: ton_trading_get_token_listings ───────────────────────────────── + { + name: "ton_trading_get_token_listings", + description: + "Fetch recently listed tokens on TON DEXes (StonFi, DeDust, GasPump). Returns new tokens sorted by listing time, with initial liquidity and volume data. Use for sniper trading strategies.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + limit: { + type: "integer", + description: "Maximum number of listings to return (1–50, default 20)", + minimum: 1, + maximum: 50, + }, + min_liquidity_ton: { + type: "number", + description: "Minimum pool liquidity in TON to filter out micro-pools (default 100)", + minimum: 0, + }, + }, + }, + execute: async (params, _context) => { + const limit = params.limit ?? 20; + const minLiquidity = params.min_liquidity_ton ?? 100; + try { + const cacheKey = `listings:${limit}:${minLiquidity}`; + const cached = sdk.storage.get(cacheKey); + if (cached) return { success: true, data: cached }; + + const res = await fetch( + `https://api.geckoterminal.com/api/v2/networks/ton/new_pools?page=1`, + { signal: AbortSignal.timeout(15_000), headers: { Accept: "application/json" } } + ); + + if (!res.ok) { + return { success: false, error: `GeckoTerminal API returned ${res.status}` }; + } + + const json = await res.json(); + const pools = (json?.data ?? []) + .map((p) => { + const attr = p.attributes ?? {}; + return { + pool_address: p.id?.replace("ton_", "") ?? null, + name: attr.name ?? null, + dex: attr.dex_id ?? null, + base_token_address: attr.base_token_price_usd != null + ? (p.relationships?.base_token?.data?.id?.replace("ton_", "") ?? null) + : null, + quote_token_address: p.relationships?.quote_token?.data?.id?.replace("ton_", "") ?? null, + created_at: attr.pool_created_at ?? null, + reserve_in_usd: parseFloat(attr.reserve_in_usd ?? 0), + volume_usd_24h: parseFloat(attr.volume_usd?.h24 ?? 0), + }; + }) + .filter((p) => { + // Approximate TON price filter — reserve_in_usd / ~3 USD per TON + const reserveTon = p.reserve_in_usd / 3; + return reserveTon >= minLiquidity; + }) + .slice(0, limit); + + const data = { listings: pools, fetched_at: Date.now() }; + sdk.storage.set(cacheKey, data, { ttl: 60_000 }); + + return { success: true, data }; + } catch (err) { + sdk.log.error(`ton_trading_get_token_listings failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, + + // ── Tool 9: ton_trading_get_token_info ───────────────────────────────────── + { + name: "ton_trading_get_token_info", + description: + "Get detailed information about a specific token by its jetton master address: price, market cap, holders, 24-h volume, and top pools. Use before deciding to snipe a new token.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + token_address: { + type: "string", + description: "Jetton master address of the token (e.g. \"EQCxE6...\")", + }, + }, + required: ["token_address"], + }, + execute: async (params, _context) => { + const { token_address } = params; + try { + const cacheKey = `tokeninfo:${token_address}`; + const cached = sdk.storage.get(cacheKey); + if (cached) return { success: true, data: cached }; + + const res = await fetch( + `https://api.geckoterminal.com/api/v2/networks/ton/tokens/${encodeURIComponent(token_address)}`, + { signal: AbortSignal.timeout(15_000), headers: { Accept: "application/json" } } + ); + + if (!res.ok) { + return { success: false, error: `GeckoTerminal API returned ${res.status}` }; + } + + const json = await res.json(); + const attr = json?.data?.attributes ?? {}; + + const data = { + token_address, + name: attr.name ?? null, + symbol: attr.symbol ?? null, + price_usd: parseFloat(attr.price_usd ?? 0) || null, + market_cap_usd: parseFloat(attr.market_cap_usd ?? 0) || null, + fdv_usd: parseFloat(attr.fdv_usd ?? 0) || null, + volume_usd_24h: parseFloat(attr.volume_usd?.h24 ?? 0) || null, + price_change_24h_percent: parseFloat(attr.price_change_percentage?.h24 ?? 0) || null, + total_supply: attr.total_supply ?? null, + coingecko_coin_id: attr.coingecko_coin_id ?? null, + }; + + sdk.storage.set(cacheKey, data, { ttl: 120_000 }); + + return { success: true, data }; + } catch (err) { + sdk.log.error(`ton_trading_get_token_info failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, + + // ── Tool 10: ton_trading_validate_token ──────────────────────────────────── + { + name: "ton_trading_validate_token", + description: + "Safety-check a token before sniping: checks liquidity, volume, age, and basic rug-pull signals. Returns a risk score and list of warnings. Always call this before executing a sniper trade.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + token_address: { + type: "string", + description: "Jetton master address to validate", + }, + min_liquidity_ton: { + type: "number", + description: "Minimum liquidity in TON to consider safe (default 100)", + minimum: 0, + }, + min_volume_usd_24h: { + type: "number", + description: "Minimum 24-h volume in USD (default 500)", + minimum: 0, + }, + }, + required: ["token_address"], + }, + execute: async (params, _context) => { + const { token_address, min_liquidity_ton = 100, min_volume_usd_24h = 500 } = params; + try { + // Reuse token info (cached or fresh) + const infoRes = await fetch( + `https://api.geckoterminal.com/api/v2/networks/ton/tokens/${encodeURIComponent(token_address)}`, + { signal: AbortSignal.timeout(15_000), headers: { Accept: "application/json" } } + ); + + const warnings = []; + let riskScore = 0; // 0 (safe) → 100 (very risky) + + if (!infoRes.ok) { + warnings.push({ type: "token_not_found", message: "Token not found on GeckoTerminal — treat as very high risk" }); + riskScore = 90; + return { success: true, data: { token_address, safe: false, risk_score: riskScore, warnings } }; + } + + const json = await infoRes.json(); + const attr = json?.data?.attributes ?? {}; + + const priceUsd = parseFloat(attr.price_usd ?? 0); + const marketCapUsd = parseFloat(attr.market_cap_usd ?? 0); + const volumeUsd24h = parseFloat(attr.volume_usd?.h24 ?? 0); + const reserveUsd = parseFloat(attr.reserve_in_usd ?? 0); + const reserveTon = reserveUsd / 3; // approximate + + if (reserveTon < min_liquidity_ton) { + warnings.push({ type: "low_liquidity", message: `Liquidity (~${reserveTon.toFixed(0)} TON) is below minimum (${min_liquidity_ton} TON)` }); + riskScore += 30; + } + + if (volumeUsd24h < min_volume_usd_24h) { + warnings.push({ type: "low_volume", message: `24h volume ($${volumeUsd24h.toFixed(0)}) is below minimum ($${min_volume_usd_24h})` }); + riskScore += 20; + } + + if (priceUsd <= 0) { + warnings.push({ type: "no_price", message: "Token has no price data — possibly not tradeable" }); + riskScore += 25; + } + + if (marketCapUsd > 0 && volumeUsd24h > marketCapUsd * 5) { + warnings.push({ type: "suspicious_volume", message: "Volume is >5× market cap — possible wash trading" }); + riskScore += 25; + } + + riskScore = Math.min(riskScore, 100); + const safe = riskScore < 40 && warnings.length === 0; + + return { + success: true, + data: { + token_address, + name: attr.name ?? null, + symbol: attr.symbol ?? null, + safe, + risk_score: riskScore, + warnings, + liquidity_ton: parseFloat(reserveTon.toFixed(2)), + volume_usd_24h: volumeUsd24h, + price_usd: priceUsd || null, + }, + }; + } catch (err) { + sdk.log.error(`ton_trading_validate_token failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, + + // ── P0 Tools: Copy Trading ───────────────────────────────────────────────── + + // ── Tool 11: ton_trading_get_top_traders ─────────────────────────────────── + { + name: "ton_trading_get_top_traders", + description: + "Find top-performing trader wallets on TON by analysing on-chain DEX activity. Returns wallets ranked by win rate and profit over the specified period. Use to find wallets worth copying.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + limit: { + type: "integer", + description: "Number of top traders to return (1–20, default 10)", + minimum: 1, + maximum: 20, + }, + min_trades: { + type: "integer", + description: "Minimum number of trades to qualify (default 10)", + minimum: 1, + }, + min_win_rate: { + type: "number", + description: "Minimum win rate (0–1, e.g. 0.6 = 60%, default 0.55)", + minimum: 0, + maximum: 1, + }, + }, + }, + execute: async (params, _context) => { + const limit = params.limit ?? 10; + const minTrades = params.min_trades ?? 10; + const minWinRate = params.min_win_rate ?? 0.55; + try { + const cacheKey = `toptraders:${limit}:${minTrades}:${minWinRate}`; + const cached = sdk.storage.get(cacheKey); + if (cached) return { success: true, data: cached }; + + // Fetch trending pools to find active traders + const res = await fetch( + `https://api.geckoterminal.com/api/v2/networks/ton/trending_pools?page=1`, + { signal: AbortSignal.timeout(15_000), headers: { Accept: "application/json" } } + ); + + if (!res.ok) { + return { success: false, error: `GeckoTerminal API returned ${res.status}` }; + } + + const json = await res.json(); + const pools = (json?.data ?? []).slice(0, 5); + + // For each trending pool, fetch recent trades to identify active wallets + const walletStats = new Map(); + + await Promise.all( + pools.map(async (pool) => { + const poolAddress = pool.id?.replace("ton_", ""); + if (!poolAddress) return; + + const tradesRes = await fetch( + `https://api.geckoterminal.com/api/v2/networks/ton/pools/${encodeURIComponent(poolAddress)}/trades?trade_volume_in_usd_greater_than=10`, + { signal: AbortSignal.timeout(15_000), headers: { Accept: "application/json" } } + ).catch(() => null); + + if (!tradesRes?.ok) return; + const tradesJson = await tradesRes.json().catch(() => null); + if (!tradesJson) return; + + for (const trade of (tradesJson?.data ?? [])) { + const attr = trade.attributes ?? {}; + const wallet = attr.tx_from_address ?? null; + if (!wallet) continue; + + const priceChange = parseFloat(attr.price_to_in_currency_token ?? 0) - + parseFloat(attr.price_from_in_currency_token ?? 0); + const isWin = priceChange > 0; + + if (!walletStats.has(wallet)) { + walletStats.set(wallet, { wallet, trades: 0, wins: 0, total_volume_usd: 0 }); + } + const stats = walletStats.get(wallet); + stats.trades += 1; + if (isWin) stats.wins += 1; + stats.total_volume_usd += parseFloat(attr.volume_in_usd ?? 0); + } + }) + ); + + const traders = Array.from(walletStats.values()) + .filter((w) => w.trades >= minTrades) + .map((w) => ({ + ...w, + win_rate: parseFloat((w.wins / w.trades).toFixed(4)), + })) + .filter((w) => w.win_rate >= minWinRate) + .sort((a, b) => b.win_rate - a.win_rate) + .slice(0, limit); + + const data = { traders, fetched_at: Date.now() }; + sdk.storage.set(cacheKey, data, { ttl: 300_000 }); + + return { success: true, data }; + } catch (err) { + sdk.log.error(`ton_trading_get_top_traders failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, + + // ── Tool 12: ton_trading_get_trader_performance ──────────────────────────── + { + name: "ton_trading_get_trader_performance", + description: + "Analyse the recent on-chain trading performance of a specific wallet: win rate, total PnL estimate, most-traded tokens, and active pools. Use before deciding to copy a trader.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + wallet_address: { + type: "string", + description: "TON wallet address to analyse", + }, + limit: { + type: "integer", + description: "Number of recent transactions to analyse (1–50, default 20)", + minimum: 1, + maximum: 50, + }, + }, + required: ["wallet_address"], + }, + execute: async (params, _context) => { + const { wallet_address, limit = 20 } = params; + try { + const cacheKey = `traderperf:${wallet_address}:${limit}`; + const cached = sdk.storage.get(cacheKey); + if (cached) return { success: true, data: cached }; + + const res = await fetch( + `https://tonapi.io/v2/accounts/${encodeURIComponent(wallet_address)}/events?limit=${limit}&subject_only=true`, + { signal: AbortSignal.timeout(15_000), headers: { Accept: "application/json" } } + ); + + if (!res.ok) { + return { success: false, error: `TON API returned ${res.status}` }; + } + + const json = await res.json(); + const events = json?.events ?? []; + + let swaps = 0; + let wins = 0; + const tokenFrequency = new Map(); + + for (const event of events) { + for (const action of (event.actions ?? [])) { + if (action.type !== "JettonSwap") continue; + swaps += 1; + const jetton = action.JettonSwap?.jetton_master_in?.address ?? null; + if (jetton) tokenFrequency.set(jetton, (tokenFrequency.get(jetton) ?? 0) + 1); + // Heuristic win: received more value out than paid in (by token amounts) + const amtIn = parseFloat(action.JettonSwap?.amount_in ?? 0); + const amtOut = parseFloat(action.JettonSwap?.amount_out ?? 0); + if (amtOut > amtIn) wins += 1; + } + } + + const winRate = swaps > 0 ? parseFloat((wins / swaps).toFixed(4)) : null; + const topTokens = Array.from(tokenFrequency.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5) + .map(([address, count]) => ({ address, swap_count: count })); + + const data = { + wallet_address, + analysed_events: events.length, + total_swaps: swaps, + wins, + win_rate: winRate, + top_tokens: topTokens, + fetched_at: Date.now(), + }; + + sdk.storage.set(cacheKey, data, { ttl: 180_000 }); + + return { success: true, data }; + } catch (err) { + sdk.log.error(`ton_trading_get_trader_performance failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, + + // ── P1 Tools: Liquidity & Farming ───────────────────────────────────────── + + // ── Tool 13: ton_trading_get_active_pools ────────────────────────────────── + { + name: "ton_trading_get_active_pools", + description: + "List active liquidity pools on TON DEXes (StonFi, DeDust, TONCO) sorted by 24-h volume. Returns pool address, token pair, liquidity, and volume. Use to find pools for LP or farming strategies.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + limit: { + type: "integer", + description: "Number of pools to return (1–50, default 20)", + minimum: 1, + maximum: 50, + }, + dex: { + type: "string", + description: 'Filter by DEX: "stonfi", "dedust", "tonco", or omit for all', + enum: ["stonfi", "dedust", "tonco"], + }, + min_volume_usd_24h: { + type: "number", + description: "Minimum 24-h volume in USD (default 1000)", + minimum: 0, + }, + }, + }, + execute: async (params, _context) => { + const limit = params.limit ?? 20; + const dexFilter = params.dex ?? null; + const minVolume = params.min_volume_usd_24h ?? 1000; + try { + const cacheKey = `pools:${dexFilter ?? "all"}:${minVolume}:${limit}`; + const cached = sdk.storage.get(cacheKey); + if (cached) return { success: true, data: cached }; + + const res = await fetch( + `https://api.geckoterminal.com/api/v2/networks/ton/pools?page=1&sort=h24_volume_usd_liquidity_desc`, + { signal: AbortSignal.timeout(15_000), headers: { Accept: "application/json" } } + ); + + if (!res.ok) { + return { success: false, error: `GeckoTerminal API returned ${res.status}` }; + } + + const json = await res.json(); + let pools = (json?.data ?? []).map((p) => { + const attr = p.attributes ?? {}; + return { + pool_address: p.id?.replace("ton_", "") ?? null, + name: attr.name ?? null, + dex: attr.dex_id ?? null, + volume_usd_24h: parseFloat(attr.volume_usd?.h24 ?? 0), + reserve_in_usd: parseFloat(attr.reserve_in_usd ?? 0), + fee_tier: attr.pool_fee ?? null, + created_at: attr.pool_created_at ?? null, + }; + }); + + if (dexFilter) { + pools = pools.filter((p) => p.dex?.toLowerCase().includes(dexFilter)); + } + + pools = pools + .filter((p) => p.volume_usd_24h >= minVolume) + .slice(0, limit); + + const data = { pools, fetched_at: Date.now() }; + sdk.storage.set(cacheKey, data, { ttl: 120_000 }); + + return { success: true, data }; + } catch (err) { + sdk.log.error(`ton_trading_get_active_pools failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, + + // ── Tool 14: ton_trading_get_farms_with_apy ──────────────────────────────── + { + name: "ton_trading_get_farms_with_apy", + description: + "List yield farming opportunities on TON DEXes with estimated APY. Returns farms sorted by APY descending. Use to find the best farming strategies.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + limit: { + type: "integer", + description: "Number of farms to return (1–50, default 20)", + minimum: 1, + maximum: 50, + }, + min_apy: { + type: "number", + description: "Minimum APY percentage to include (default 5)", + minimum: 0, + }, + min_tvl_usd: { + type: "number", + description: "Minimum total value locked in USD (default 10000)", + minimum: 0, + }, + }, + }, + execute: async (params, _context) => { + const limit = params.limit ?? 20; + const minApy = params.min_apy ?? 5; + const minTvl = params.min_tvl_usd ?? 10_000; + try { + const cacheKey = `farms:${minApy}:${minTvl}:${limit}`; + const cached = sdk.storage.get(cacheKey); + if (cached) return { success: true, data: cached }; + + // Use GeckoTerminal pools as a proxy — estimate APY from fee yield + const res = await fetch( + `https://api.geckoterminal.com/api/v2/networks/ton/pools?page=1&sort=h24_volume_usd_liquidity_desc`, + { signal: AbortSignal.timeout(15_000), headers: { Accept: "application/json" } } + ); + + if (!res.ok) { + return { success: false, error: `GeckoTerminal API returned ${res.status}` }; + } + + const json = await res.json(); + const farms = (json?.data ?? []) + .map((p) => { + const attr = p.attributes ?? {}; + const volume24h = parseFloat(attr.volume_usd?.h24 ?? 0); + const reserve = parseFloat(attr.reserve_in_usd ?? 0); + const feeTier = parseFloat(attr.pool_fee ?? 0.003); + // Estimated APY: daily fee yield × 365 + const dailyFeeYield = reserve > 0 ? (volume24h * feeTier) / reserve : 0; + const estimatedApy = dailyFeeYield * 365 * 100; + return { + pool_address: p.id?.replace("ton_", "") ?? null, + name: attr.name ?? null, + dex: attr.dex_id ?? null, + tvl_usd: reserve, + volume_usd_24h: volume24h, + fee_tier: feeTier, + estimated_apy_percent: parseFloat(estimatedApy.toFixed(2)), + }; + }) + .filter((f) => f.tvl_usd >= minTvl && f.estimated_apy_percent >= minApy) + .sort((a, b) => b.estimated_apy_percent - a.estimated_apy_percent) + .slice(0, limit); + + const data = { farms, fetched_at: Date.now(), note: "APY is estimated from 24h fee yield × 365 — actual rewards may differ" }; + sdk.storage.set(cacheKey, data, { ttl: 300_000 }); + + return { success: true, data }; + } catch (err) { + sdk.log.error(`ton_trading_get_farms_with_apy failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, + + // ── Tool 15: ton_trading_get_pool_volume ─────────────────────────────────── + { + name: "ton_trading_get_pool_volume", + description: + "Get detailed volume statistics for a specific liquidity pool: 1h, 6h, 24h volumes and price change percentages. Use to monitor pool activity before adding liquidity.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + pool_address: { + type: "string", + description: "Pool contract address on TON", + }, + }, + required: ["pool_address"], + }, + execute: async (params, _context) => { + const { pool_address } = params; + try { + const cacheKey = `poolvol:${pool_address}`; + const cached = sdk.storage.get(cacheKey); + if (cached) return { success: true, data: cached }; + + const res = await fetch( + `https://api.geckoterminal.com/api/v2/networks/ton/pools/${encodeURIComponent(pool_address)}`, + { signal: AbortSignal.timeout(15_000), headers: { Accept: "application/json" } } + ); + + if (!res.ok) { + return { success: false, error: `GeckoTerminal API returned ${res.status}` }; + } + + const json = await res.json(); + const attr = json?.data?.attributes ?? {}; + + const data = { + pool_address, + name: attr.name ?? null, + dex: attr.dex_id ?? null, + reserve_in_usd: parseFloat(attr.reserve_in_usd ?? 0), + volume_usd: { + h1: parseFloat(attr.volume_usd?.h1 ?? 0), + h6: parseFloat(attr.volume_usd?.h6 ?? 0), + h24: parseFloat(attr.volume_usd?.h24 ?? 0), + }, + price_change_percent: { + h1: parseFloat(attr.price_change_percentage?.h1 ?? 0), + h6: parseFloat(attr.price_change_percentage?.h6 ?? 0), + h24: parseFloat(attr.price_change_percentage?.h24 ?? 0), + }, + transactions_24h: { + buys: attr.transactions?.h24?.buys ?? null, + sells: attr.transactions?.h24?.sells ?? null, + }, + fetched_at: Date.now(), + }; + + sdk.storage.set(cacheKey, data, { ttl: 60_000 }); + + return { success: true, data }; + } catch (err) { + sdk.log.error(`ton_trading_get_pool_volume failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, + + // ── P1 Tools: Backtesting ────────────────────────────────────────────────── + + // ── Tool 16: ton_trading_backtest ────────────────────────────────────────── + { + name: "ton_trading_backtest", + description: + "Replay a simple threshold-based strategy against historical trades in the journal. Returns win rate, total PnL, max drawdown, and Sharpe ratio. Use to evaluate a strategy before running it live.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + strategy: { + type: "string", + description: 'Strategy type: "buy_and_hold", "mean_reversion", or "momentum"', + enum: ["buy_and_hold", "mean_reversion", "momentum"], + }, + from_asset: { + type: "string", + description: 'Asset pair — from asset (e.g. "TON")', + }, + to_asset: { + type: "string", + description: "Asset pair — to asset (jetton address)", + }, + entry_threshold_percent: { + type: "number", + description: "For mean_reversion/momentum: price change % that triggers a buy (default 2)", + minimum: 0, + }, + exit_threshold_percent: { + type: "number", + description: "Profit % at which the strategy exits (default 5)", + minimum: 0, + }, + stop_loss_percent: { + type: "number", + description: "Loss % that triggers a stop-loss exit (default 5)", + minimum: 0, + }, + lookback_days: { + type: "integer", + description: "Number of days of journal history to use (default 30, max 365)", + minimum: 1, + maximum: 365, + }, + }, + required: ["strategy", "from_asset", "to_asset"], + }, + execute: async (params, _context) => { + const { + strategy, + from_asset, + to_asset, + entry_threshold_percent = 2, + exit_threshold_percent = 5, + stop_loss_percent = 5, + lookback_days = 30, + } = params; + try { + const since = Date.now() - lookback_days * 24 * 60 * 60 * 1000; + + const trades = sdk.db + .prepare( + `SELECT * FROM trade_journal + WHERE from_asset = ? AND to_asset = ? AND timestamp >= ? AND status = 'closed' + ORDER BY timestamp ASC` + ) + .all(from_asset, to_asset, since); + + if (trades.length < 2) { + return { + success: true, + data: { + strategy, + from_asset, + to_asset, + note: `Not enough closed trades (${trades.length}) to backtest. Need at least 2.`, + simulated_trades: 0, + }, + }; + } + + let capital = 1000; // virtual starting capital + const initialCapital = capital; + let wins = 0; + let losses = 0; + let maxCapital = capital; + let minCapital = capital; + const returns = []; + + for (let i = 1; i < trades.length; i++) { + const prev = trades[i - 1]; + const curr = trades[i]; + + const prevPnlPct = prev.pnl_percent ?? 0; + let shouldBuy = false; + + if (strategy === "buy_and_hold") { + shouldBuy = true; + } else if (strategy === "mean_reversion") { + shouldBuy = prevPnlPct <= -entry_threshold_percent; + } else if (strategy === "momentum") { + shouldBuy = prevPnlPct >= entry_threshold_percent; + } + + if (!shouldBuy) continue; + + const tradeReturn = curr.pnl_percent ?? 0; + const exitedAtProfit = tradeReturn >= exit_threshold_percent; + const exitedAtStop = tradeReturn <= -stop_loss_percent; + const effectiveReturn = exitedAtStop ? -stop_loss_percent : (exitedAtProfit ? exit_threshold_percent : tradeReturn); + + const prevCapital = capital; + capital = capital * (1 + effectiveReturn / 100); + const ret = (capital - prevCapital) / prevCapital; + returns.push(ret); + + if (capital > maxCapital) maxCapital = capital; + if (capital < minCapital) minCapital = capital; + + if (effectiveReturn > 0) wins += 1; + else losses += 1; + } + + const totalTrades = wins + losses; + const totalPnl = capital - initialCapital; + const totalPnlPercent = (totalPnl / initialCapital) * 100; + const winRate = totalTrades > 0 ? wins / totalTrades : 0; + const maxDrawdown = maxCapital > 0 ? ((maxCapital - minCapital) / maxCapital) * 100 : 0; + + // Sharpe ratio (simplified, assuming risk-free rate = 0) + let sharpe = null; + if (returns.length > 1) { + const mean = returns.reduce((s, r) => s + r, 0) / returns.length; + const variance = returns.reduce((s, r) => s + (r - mean) ** 2, 0) / returns.length; + const stddev = Math.sqrt(variance); + sharpe = stddev > 0 ? parseFloat((mean / stddev).toFixed(4)) : null; + } + + return { + success: true, + data: { + strategy, + from_asset, + to_asset, + lookback_days, + journal_trades_analysed: trades.length, + simulated_trades: totalTrades, + wins, + losses, + win_rate: parseFloat(winRate.toFixed(4)), + total_pnl: parseFloat(totalPnl.toFixed(4)), + total_pnl_percent: parseFloat(totalPnlPercent.toFixed(2)), + max_drawdown_percent: parseFloat(maxDrawdown.toFixed(2)), + sharpe_ratio: sharpe, + final_capital: parseFloat(capital.toFixed(4)), + }, + }; + } catch (err) { + sdk.log.error(`ton_trading_backtest failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, + + // ── P2 Tools: Risk Management ────────────────────────────────────────────── + + // ── Tool 17: ton_trading_calculate_risk_metrics ──────────────────────────── + { + name: "ton_trading_calculate_risk_metrics", + description: + "Calculate risk metrics from the trade journal: Value at Risk (VaR), maximum drawdown, Sharpe ratio, and win/loss statistics. Returns a risk summary to guide position sizing.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + mode: { + type: "string", + description: 'Analyse "real" trades, "simulation" trades, or "all" (default "all")', + enum: ["real", "simulation", "all"], + }, + lookback_days: { + type: "integer", + description: "Number of days of history to include (default 30)", + minimum: 1, + maximum: 365, + }, + confidence_level: { + type: "number", + description: "VaR confidence level (0.9–0.99, default 0.95)", + minimum: 0.9, + maximum: 0.99, + }, + }, + }, + execute: async (params, _context) => { + const { mode = "all", lookback_days = 30, confidence_level = 0.95 } = params; + try { + const since = Date.now() - lookback_days * 24 * 60 * 60 * 1000; + + const modeClause = mode === "all" ? "" : `AND mode = '${mode}'`; + const trades = sdk.db + .prepare( + `SELECT pnl_percent FROM trade_journal + WHERE status = 'closed' AND timestamp >= ? ${modeClause} + ORDER BY timestamp ASC` + ) + .all(since); + + if (trades.length === 0) { + return { + success: true, + data: { mode, lookback_days, note: "No closed trades found in this period", trades_analysed: 0 }, + }; + } + + const returns = trades.map((t) => (t.pnl_percent ?? 0) / 100); + + const sorted = [...returns].sort((a, b) => a - b); + const varIndex = Math.floor((1 - confidence_level) * sorted.length); + const var95 = sorted[varIndex] ?? sorted[0]; + + const mean = returns.reduce((s, r) => s + r, 0) / returns.length; + const variance = returns.reduce((s, r) => s + (r - mean) ** 2, 0) / returns.length; + const stddev = Math.sqrt(variance); + const sharpe = stddev > 0 ? parseFloat((mean / stddev).toFixed(4)) : null; + + // Max drawdown + let peak = 1; + let trough = 1; + let capital = 1; + let maxDrawdown = 0; + for (const r of returns) { + capital *= 1 + r; + if (capital > peak) { peak = capital; trough = capital; } + if (capital < trough) { + trough = capital; + const dd = (peak - trough) / peak; + if (dd > maxDrawdown) maxDrawdown = dd; + } + } + + const wins = returns.filter((r) => r > 0).length; + const losses = returns.filter((r) => r < 0).length; + const avgWin = wins > 0 ? returns.filter((r) => r > 0).reduce((s, r) => s + r, 0) / wins : 0; + const avgLoss = losses > 0 ? Math.abs(returns.filter((r) => r < 0).reduce((s, r) => s + r, 0) / losses) : 0; + const profitFactor = avgLoss > 0 ? parseFloat((avgWin / avgLoss).toFixed(4)) : null; + + return { + success: true, + data: { + mode, + lookback_days, + trades_analysed: trades.length, + win_rate: parseFloat((wins / trades.length).toFixed(4)), + avg_win_percent: parseFloat((avgWin * 100).toFixed(2)), + avg_loss_percent: parseFloat((avgLoss * 100).toFixed(2)), + profit_factor: profitFactor, + sharpe_ratio: sharpe, + max_drawdown_percent: parseFloat((maxDrawdown * 100).toFixed(2)), + value_at_risk_percent: parseFloat((Math.abs(var95) * 100).toFixed(2)), + confidence_level, + }, + }; + } catch (err) { + sdk.log.error(`ton_trading_calculate_risk_metrics failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, + + // ── Tool 18: ton_trading_set_stop_loss ───────────────────────────────────── + { + name: "ton_trading_set_stop_loss", + description: + "Register a stop-loss (and optional take-profit) rule for an open trade in the journal. The LLM should check this rule on each market data update and close the trade if triggered.", + category: "action", + parameters: { + type: "object", + properties: { + trade_id: { + type: "integer", + description: "Journal trade ID to protect", + }, + entry_price: { + type: "number", + description: "Price at which the position was opened (in quote asset units)", + }, + stop_loss_percent: { + type: "number", + description: "Percentage loss that triggers the stop-loss (e.g. 5 = close at -5%)", + minimum: 0.1, + maximum: 99, + }, + take_profit_percent: { + type: "number", + description: "Optional profit percentage that triggers the take-profit exit", + minimum: 0.1, + }, + }, + required: ["trade_id", "entry_price", "stop_loss_percent"], + }, + execute: async (params, _context) => { + const { trade_id, entry_price, stop_loss_percent, take_profit_percent } = params; + try { + const entry = sdk.db + .prepare("SELECT id, status FROM trade_journal WHERE id = ?") + .get(trade_id); + + if (!entry) { + return { success: false, error: `Trade ${trade_id} not found` }; + } + if (entry.status === "closed") { + return { success: false, error: `Trade ${trade_id} is already closed` }; + } + + const ruleId = sdk.db + .prepare( + `INSERT INTO stop_loss_rules (trade_id, stop_loss_percent, take_profit_percent, entry_price, created_at) + VALUES (?, ?, ?, ?, ?)` + ) + .run(trade_id, stop_loss_percent, take_profit_percent ?? null, entry_price, Date.now()) + .lastInsertRowid; + + const stopLossPrice = entry_price * (1 - stop_loss_percent / 100); + const takeProfitPrice = take_profit_percent != null + ? entry_price * (1 + take_profit_percent / 100) + : null; + + sdk.log.info(`Stop-loss rule #${ruleId} set for trade #${trade_id}: SL=${stopLossPrice.toFixed(4)}`); + + return { + success: true, + data: { + rule_id: ruleId, + trade_id, + entry_price, + stop_loss_price: parseFloat(stopLossPrice.toFixed(6)), + take_profit_price: takeProfitPrice != null ? parseFloat(takeProfitPrice.toFixed(6)) : null, + stop_loss_percent, + take_profit_percent: take_profit_percent ?? null, + status: "active", + }, + }; + } catch (err) { + sdk.log.error(`ton_trading_set_stop_loss failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, + + // ── Tool 19: ton_trading_get_optimal_position_size ───────────────────────── + { + name: "ton_trading_get_optimal_position_size", + description: + "Calculate the optimal position size for a trade using the Kelly Criterion and fixed-fraction methods, based on historical win rate and risk/reward ratio from the trade journal.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + mode: { + type: "string", + description: 'Use "real" or "simulation" trade history (default "simulation")', + enum: ["real", "simulation"], + }, + risk_percent: { + type: "number", + description: "Maximum percentage of balance to risk per trade for fixed-fraction method (default 2)", + minimum: 0.1, + maximum: 50, + }, + stop_loss_percent: { + type: "number", + description: "Stop-loss percentage for this trade (used for fixed-fraction sizing)", + minimum: 0.1, + maximum: 99, + }, + lookback_days: { + type: "integer", + description: "Days of history to use for win rate calculation (default 30)", + minimum: 1, + maximum: 365, + }, + }, + required: ["stop_loss_percent"], + }, + execute: async (params, _context) => { + const { mode = "simulation", risk_percent = 2, stop_loss_percent, lookback_days = 30 } = params; + try { + const since = Date.now() - lookback_days * 24 * 60 * 60 * 1000; + const trades = sdk.db + .prepare( + `SELECT pnl_percent FROM trade_journal + WHERE status = 'closed' AND mode = ? AND timestamp >= ?` + ) + .all(mode, since); + + const balance = + mode === "simulation" + ? getSimBalance(sdk) + : parseFloat((await sdk.ton.getBalance())?.balance ?? "0"); + + let winRate = 0.5; // default if no history + let avgWinPct = 5; + let avgLossPct = 5; + + if (trades.length >= 5) { + const wins = trades.filter((t) => (t.pnl_percent ?? 0) > 0); + const lossesArr = trades.filter((t) => (t.pnl_percent ?? 0) < 0); + winRate = wins.length / trades.length; + avgWinPct = wins.length > 0 + ? wins.reduce((s, t) => s + (t.pnl_percent ?? 0), 0) / wins.length + : 5; + avgLossPct = lossesArr.length > 0 + ? Math.abs(lossesArr.reduce((s, t) => s + (t.pnl_percent ?? 0), 0) / lossesArr.length) + : 5; + } + + // Kelly Criterion: f* = W/L - (1-W)/W where W=win_rate, L=loss_rate, b=avg_win/avg_loss + const b = avgLossPct > 0 ? avgWinPct / avgLossPct : 1; + const kellyFraction = winRate - (1 - winRate) / b; + const halfKellyFraction = Math.max(0, kellyFraction / 2); // half-Kelly for safety + + // Fixed-fraction: risk a fixed % of capital, sized so stop-loss = that % of capital + const fixedFractionSize = balance * (risk_percent / 100) / (stop_loss_percent / 100); + + return { + success: true, + data: { + mode, + balance, + trades_analysed: trades.length, + win_rate: parseFloat(winRate.toFixed(4)), + avg_win_percent: parseFloat(avgWinPct.toFixed(2)), + avg_loss_percent: parseFloat(avgLossPct.toFixed(2)), + kelly_fraction: parseFloat(kellyFraction.toFixed(4)), + half_kelly_fraction: parseFloat(halfKellyFraction.toFixed(4)), + kelly_position_size: parseFloat((balance * halfKellyFraction).toFixed(4)), + fixed_fraction_position_size: parseFloat(fixedFractionSize.toFixed(4)), + risk_percent, + stop_loss_percent, + recommendation: kellyFraction <= 0 + ? "Kelly suggests no position — unfavorable win/loss ratio based on history" + : `Suggested position: ${Math.min(balance * halfKellyFraction, fixedFractionSize).toFixed(2)} TON (lower of Kelly and fixed-fraction)`, + }, + }; + } catch (err) { + sdk.log.error(`ton_trading_get_optimal_position_size failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, + + // ── P2 Tools: Automation ─────────────────────────────────────────────────── + + // ── Tool 20: ton_trading_schedule_trade ──────────────────────────────────── + { + name: "ton_trading_schedule_trade", + description: + "Store a pending trade to be executed at a future time. The LLM should check scheduled trades on each run and execute any that are due. Returns the scheduled trade ID.", + category: "action", + parameters: { + type: "object", + properties: { + mode: { + type: "string", + description: 'Trading mode: "real" or "simulation"', + enum: ["real", "simulation"], + }, + from_asset: { + type: "string", + description: 'Asset to sell — "TON" or a jetton master address', + }, + to_asset: { + type: "string", + description: 'Asset to buy — "TON" or a jetton master address', + }, + amount: { + type: "number", + description: "Amount of from_asset to trade", + }, + execute_at_iso: { + type: "string", + description: 'ISO 8601 datetime when to execute the trade (e.g. "2025-01-01T12:00:00Z")', + }, + note: { + type: "string", + description: "Optional note describing the reason for scheduling", + }, + }, + required: ["mode", "from_asset", "to_asset", "amount", "execute_at_iso"], + }, + execute: async (params, _context) => { + const { mode, from_asset, to_asset, amount, execute_at_iso, note } = params; + try { + const executeAt = new Date(execute_at_iso).getTime(); + if (isNaN(executeAt)) { + return { success: false, error: `Invalid execute_at_iso: "${execute_at_iso}"` }; + } + if (executeAt <= Date.now()) { + return { success: false, error: "execute_at_iso must be in the future" }; + } + + const schedId = sdk.db + .prepare( + `INSERT INTO scheduled_trades (created_at, execute_at, mode, from_asset, to_asset, amount, note) + VALUES (?, ?, ?, ?, ?, ?, ?)` + ) + .run(Date.now(), executeAt, mode, from_asset, to_asset, amount, note ?? null) + .lastInsertRowid; + + sdk.log.info(`Scheduled trade #${schedId}: ${amount} ${from_asset} → ${to_asset} at ${execute_at_iso}`); + + return { + success: true, + data: { + scheduled_id: schedId, + mode, + from_asset, + to_asset, + amount, + execute_at: execute_at_iso, + status: "pending", + }, + }; + } catch (err) { + sdk.log.error(`ton_trading_schedule_trade failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, + + // ── Tool 21: ton_trading_get_scheduled_trades ────────────────────────────── + { + name: "ton_trading_get_scheduled_trades", + description: + "List pending scheduled trades. Returns all pending trades, highlighting those that are due now (execute_at <= current time). The LLM should execute due trades using ton_trading_execute_swap or ton_trading_simulate_trade.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + status: { + type: "string", + description: 'Filter by status: "pending", "executed", "cancelled", or "all" (default "pending")', + enum: ["pending", "executed", "cancelled", "all"], + }, + limit: { + type: "integer", + description: "Maximum number of records to return (1–50, default 20)", + minimum: 1, + maximum: 50, + }, + }, + }, + execute: async (params, _context) => { + const status = params.status ?? "pending"; + const limit = params.limit ?? 20; + try { + const statusClause = status === "all" ? "" : `WHERE status = '${status}'`; + + const scheduled = sdk.db + .prepare(`SELECT * FROM scheduled_trades ${statusClause} ORDER BY execute_at ASC LIMIT ?`) + .all(limit); + + const now = Date.now(); + const annotated = scheduled.map((s) => ({ + ...s, + is_due: s.execute_at <= now, + due_in_ms: Math.max(0, s.execute_at - now), + })); + + const dueTrades = annotated.filter((s) => s.is_due && s.status === "pending"); + + return { + success: true, + data: { + scheduled_trades: annotated, + due_now: dueTrades.length, + note: dueTrades.length > 0 + ? `${dueTrades.length} trade(s) are due — execute them using ton_trading_execute_swap or ton_trading_simulate_trade` + : null, + }, + }; + } catch (err) { + sdk.log.error(`ton_trading_get_scheduled_trades failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, ]; diff --git a/plugins/ton-trading-bot/manifest.json b/plugins/ton-trading-bot/manifest.json index 3b45a0e..d104aa8 100644 --- a/plugins/ton-trading-bot/manifest.json +++ b/plugins/ton-trading-bot/manifest.json @@ -1,8 +1,8 @@ { "id": "ton-trading-bot", "name": "TON Trading Bot", - "version": "1.0.0", - "description": "Atomic TON trading tools: market data, portfolio, risk validation, simulation, and DEX swap execution. The LLM composes these into trading strategies.", + "version": "2.0.0", + "description": "Atomic TON trading tools: market data, portfolio, risk validation, simulation, DEX swap execution, cross-DEX arbitrage, sniper trading, copy trading, liquidity pools, farming, backtesting, risk management, and automation. The LLM composes these into trading strategies.", "author": { "name": "xlabtg", "url": "https://github.com/xlabtg" @@ -35,6 +35,66 @@ { "name": "ton_trading_record_trade", "description": "Close an open trade in the journal and record final PnL" + }, + { + "name": "ton_trading_get_arbitrage_opportunities", + "description": "Find cross-DEX price differences for a token pair across StonFi, DeDust, TONCO, and swap.coffee" + }, + { + "name": "ton_trading_get_token_listings", + "description": "Fetch recently listed tokens on TON DEXes for sniper trading strategies" + }, + { + "name": "ton_trading_get_token_info", + "description": "Get detailed information about a specific token: price, market cap, holders, volume" + }, + { + "name": "ton_trading_validate_token", + "description": "Safety-check a token before sniping: liquidity, volume, and rug-pull risk signals" + }, + { + "name": "ton_trading_get_top_traders", + "description": "Find top-performing trader wallets on TON ranked by win rate and volume" + }, + { + "name": "ton_trading_get_trader_performance", + "description": "Analyse the on-chain trading performance of a specific wallet for copy trading" + }, + { + "name": "ton_trading_get_active_pools", + "description": "List active liquidity pools on TON DEXes sorted by 24-h volume" + }, + { + "name": "ton_trading_get_farms_with_apy", + "description": "List yield farming opportunities on TON DEXes with estimated APY" + }, + { + "name": "ton_trading_get_pool_volume", + "description": "Get detailed volume statistics for a specific liquidity pool" + }, + { + "name": "ton_trading_backtest", + "description": "Replay a strategy against trade journal history and return win rate, PnL, and Sharpe ratio" + }, + { + "name": "ton_trading_calculate_risk_metrics", + "description": "Calculate VaR, max drawdown, Sharpe ratio, and win/loss statistics from trade history" + }, + { + "name": "ton_trading_set_stop_loss", + "description": "Register a stop-loss and optional take-profit rule for an open trade" + }, + { + "name": "ton_trading_get_optimal_position_size", + "description": "Calculate optimal position size using Kelly Criterion and fixed-fraction methods" + }, + { + "name": "ton_trading_schedule_trade", + "description": "Store a pending trade for future execution at a specified datetime" + }, + { + "name": "ton_trading_get_scheduled_trades", + "description": "List pending scheduled trades and flag which ones are due for execution" } ], "defaultConfig": { @@ -44,7 +104,7 @@ "simulationBalance": 1000 }, "permissions": [], - "tags": ["trading", "ton", "dex", "portfolio", "simulation"], + "tags": ["trading", "ton", "dex", "portfolio", "simulation", "arbitrage", "sniper", "copy-trading", "farming", "backtesting", "risk-management"], "repository": "https://github.com/xlabtg/teleton-plugins", "funding": null } diff --git a/plugins/ton-trading-bot/tests/index.test.js b/plugins/ton-trading-bot/tests/index.test.js index fd0e7aa..8c30177 100644 --- a/plugins/ton-trading-bot/tests/index.test.js +++ b/plugins/ton-trading-bot/tests/index.test.js @@ -148,10 +148,10 @@ describe("ton-trading-bot plugin", () => { assert.ok(Array.isArray(toolList)); }); - it("exports exactly 6 tools", () => { + it("exports exactly 21 tools", () => { const sdk = makeSdk(); const toolList = mod.tools(sdk); - assert.equal(toolList.length, 6); + assert.equal(toolList.length, 21); }); it("all tools have name, description, and execute", () => { @@ -175,6 +175,8 @@ describe("ton-trading-bot plugin", () => { assert.equal(executed.length, 1); assert.ok(executed[0].includes("trade_journal")); assert.ok(executed[0].includes("sim_balance")); + assert.ok(executed[0].includes("stop_loss_rules")); + assert.ok(executed[0].includes("scheduled_trades")); }); }); @@ -594,4 +596,480 @@ describe("ton-trading-bot plugin", () => { assert.ok(tool.parameters?.required?.includes("amount_out")); }); }); + + // ── ton_trading_get_arbitrage_opportunities ───────────────────────────────── + describe("ton_trading_get_arbitrage_opportunities", () => { + it("returns opportunities when DEX prices differ", async () => { + const sdk = makeSdk({ + ton: { + getAddress: () => "EQTestWalletAddress", + getBalance: async () => ({ balance: "100.5", balanceNano: "100500000000" }), + getPrice: async () => ({ usd: 3.5, source: "mock" }), + getJettonBalances: async () => [], + dex: { + quote: async () => ({ + stonfi: { output: "10.0", price: "10.0" }, + dedust: { output: "10.5", price: "10.5" }, + recommended: "dedust", + savings: "0.5", + }), + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_arbitrage_opportunities"); + const result = await tool.execute({ from_asset: "TON", to_asset: "EQCxE6test", amount: "1" }, {}); + assert.equal(result.success, true); + assert.ok(Array.isArray(result.data.opportunities)); + }); + + it("returns empty opportunities when quotes are equal", async () => { + const sdk = makeSdk({ + ton: { + getAddress: () => "EQTestWalletAddress", + getBalance: async () => ({ balance: "100.5", balanceNano: "100500000000" }), + getPrice: async () => ({ usd: 3.5, source: "mock" }), + getJettonBalances: async () => [], + dex: { + quote: async () => ({ + stonfi: { output: "10.0", price: "10.0" }, + dedust: { output: "10.0", price: "10.0" }, + recommended: "stonfi", + }), + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_arbitrage_opportunities"); + const result = await tool.execute( + { from_asset: "TON", to_asset: "EQCxE6test", amount: "1", min_profit_percent: 1 }, + {} + ); + assert.equal(result.success, true); + assert.equal(result.data.opportunities.length, 0); + }); + + it("returns failure when DEX quote fails", async () => { + const sdk = makeSdk({ + ton: { + getAddress: () => "EQTestWalletAddress", + getBalance: async () => null, + getPrice: async () => null, + getJettonBalances: async () => [], + dex: { quote: async () => { throw new Error("DEX down"); } }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_arbitrage_opportunities"); + const result = await tool.execute({ from_asset: "TON", to_asset: "EQCxE6test", amount: "1" }, {}); + assert.equal(result.success, false); + }); + + it("required parameters include from_asset, to_asset, amount", () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_arbitrage_opportunities"); + assert.ok(tool.parameters?.required?.includes("from_asset")); + assert.ok(tool.parameters?.required?.includes("to_asset")); + assert.ok(tool.parameters?.required?.includes("amount")); + }); + }); + + // ── ton_trading_get_token_listings ────────────────────────────────────────── + describe("ton_trading_get_token_listings", () => { + it("has correct name and description", () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_token_listings"); + assert.ok(tool); + assert.ok(tool.description); + assert.equal(tool.category, "data-bearing"); + }); + + it("returns cached data when available", async () => { + const cachedData = { listings: [{ name: "CachedToken" }], fetched_at: Date.now() }; + const sdk = makeSdk({ storage: { get: () => cachedData, set: () => {}, has: () => true } }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_token_listings"); + const result = await tool.execute({}, {}); + assert.equal(result.success, true); + assert.deepEqual(result.data, cachedData); + }); + }); + + // ── ton_trading_get_token_info ────────────────────────────────────────────── + describe("ton_trading_get_token_info", () => { + it("required parameters include token_address", () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_token_info"); + assert.ok(tool.parameters?.required?.includes("token_address")); + }); + + it("returns cached data when available", async () => { + const cachedData = { token_address: "EQCxE6test", name: "TestToken", price_usd: 1.5 }; + const sdk = makeSdk({ storage: { get: () => cachedData, set: () => {}, has: () => true } }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_token_info"); + const result = await tool.execute({ token_address: "EQCxE6test" }, {}); + assert.equal(result.success, true); + assert.deepEqual(result.data, cachedData); + }); + }); + + // ── ton_trading_validate_token ────────────────────────────────────────────── + describe("ton_trading_validate_token", () => { + it("required parameters include token_address", () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_validate_token"); + assert.ok(tool.parameters?.required?.includes("token_address")); + }); + + it("has data-bearing category", () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_validate_token"); + assert.equal(tool.category, "data-bearing"); + }); + }); + + // ── ton_trading_get_top_traders ───────────────────────────────────────────── + describe("ton_trading_get_top_traders", () => { + it("has correct name and category", () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_top_traders"); + assert.ok(tool); + assert.equal(tool.category, "data-bearing"); + }); + + it("returns cached data when available", async () => { + const cachedData = { traders: [{ wallet: "EQTest", win_rate: 0.7 }], fetched_at: Date.now() }; + const sdk = makeSdk({ storage: { get: () => cachedData, set: () => {}, has: () => true } }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_top_traders"); + const result = await tool.execute({}, {}); + assert.equal(result.success, true); + assert.deepEqual(result.data, cachedData); + }); + }); + + // ── ton_trading_get_trader_performance ───────────────────────────────────── + describe("ton_trading_get_trader_performance", () => { + it("required parameters include wallet_address", () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_trader_performance"); + assert.ok(tool.parameters?.required?.includes("wallet_address")); + }); + + it("returns cached data when available", async () => { + const cachedData = { wallet_address: "EQTest", total_swaps: 5, win_rate: 0.6 }; + const sdk = makeSdk({ storage: { get: () => cachedData, set: () => {}, has: () => true } }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_trader_performance"); + const result = await tool.execute({ wallet_address: "EQTest" }, {}); + assert.equal(result.success, true); + assert.deepEqual(result.data, cachedData); + }); + }); + + // ── ton_trading_get_active_pools ──────────────────────────────────────────── + describe("ton_trading_get_active_pools", () => { + it("has correct name and category", () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_active_pools"); + assert.ok(tool); + assert.equal(tool.category, "data-bearing"); + }); + + it("returns cached data when available", async () => { + const cachedData = { pools: [{ name: "TON/USDT", dex: "stonfi" }], fetched_at: Date.now() }; + const sdk = makeSdk({ storage: { get: () => cachedData, set: () => {}, has: () => true } }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_active_pools"); + const result = await tool.execute({}, {}); + assert.equal(result.success, true); + assert.deepEqual(result.data, cachedData); + }); + }); + + // ── ton_trading_get_farms_with_apy ────────────────────────────────────────── + describe("ton_trading_get_farms_with_apy", () => { + it("has correct name and category", () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_farms_with_apy"); + assert.ok(tool); + assert.equal(tool.category, "data-bearing"); + }); + + it("returns cached data when available", async () => { + const cachedData = { farms: [{ name: "TON/USDT", estimated_apy_percent: 15 }], fetched_at: Date.now() }; + const sdk = makeSdk({ storage: { get: () => cachedData, set: () => {}, has: () => true } }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_farms_with_apy"); + const result = await tool.execute({}, {}); + assert.equal(result.success, true); + assert.deepEqual(result.data, cachedData); + }); + }); + + // ── ton_trading_get_pool_volume ───────────────────────────────────────────── + describe("ton_trading_get_pool_volume", () => { + it("required parameters include pool_address", () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_pool_volume"); + assert.ok(tool.parameters?.required?.includes("pool_address")); + }); + + it("returns cached data when available", async () => { + const cachedData = { pool_address: "EQPool", volume_usd: { h24: 50000 } }; + const sdk = makeSdk({ storage: { get: () => cachedData, set: () => {}, has: () => true } }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_pool_volume"); + const result = await tool.execute({ pool_address: "EQPool" }, {}); + assert.equal(result.success, true); + assert.deepEqual(result.data, cachedData); + }); + }); + + // ── ton_trading_backtest ──────────────────────────────────────────────────── + describe("ton_trading_backtest", () => { + it("required parameters include strategy, from_asset, to_asset", () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_backtest"); + assert.ok(tool.parameters?.required?.includes("strategy")); + assert.ok(tool.parameters?.required?.includes("from_asset")); + assert.ok(tool.parameters?.required?.includes("to_asset")); + }); + + it("returns note when not enough trades exist", async () => { + const sdk = makeSdk({ + db: { + exec: () => {}, + prepare: () => ({ get: () => null, all: () => [], run: () => ({ lastInsertRowid: 1 }) }), + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_backtest"); + const result = await tool.execute( + { strategy: "momentum", from_asset: "TON", to_asset: "EQCxE6test" }, + {} + ); + assert.equal(result.success, true); + assert.ok(result.data.note); + }); + + it("backtests buy_and_hold strategy with trade data", async () => { + const trades = [ + { id: 1, from_asset: "TON", to_asset: "EQCxE6test", pnl_percent: 5, status: "closed" }, + { id: 2, from_asset: "TON", to_asset: "EQCxE6test", pnl_percent: -3, status: "closed" }, + { id: 3, from_asset: "TON", to_asset: "EQCxE6test", pnl_percent: 8, status: "closed" }, + ]; + const sdk = makeSdk({ + db: { + exec: () => {}, + prepare: () => ({ get: () => null, all: () => trades, run: () => ({ lastInsertRowid: 1 }) }), + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_backtest"); + const result = await tool.execute( + { strategy: "buy_and_hold", from_asset: "TON", to_asset: "EQCxE6test" }, + {} + ); + assert.equal(result.success, true); + assert.ok(result.data.simulated_trades >= 0); + assert.ok("win_rate" in result.data); + assert.ok("total_pnl_percent" in result.data); + }); + }); + + // ── ton_trading_calculate_risk_metrics ────────────────────────────────────── + describe("ton_trading_calculate_risk_metrics", () => { + it("returns note when no trades found", async () => { + const sdk = makeSdk({ + db: { + exec: () => {}, + prepare: () => ({ get: () => null, all: () => [], run: () => ({ lastInsertRowid: 1 }) }), + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_calculate_risk_metrics"); + const result = await tool.execute({}, {}); + assert.equal(result.success, true); + assert.ok(result.data.note); + }); + + it("computes win rate and sharpe from trade history", async () => { + const trades = [ + { pnl_percent: 10 }, + { pnl_percent: -5 }, + { pnl_percent: 8 }, + { pnl_percent: -3 }, + { pnl_percent: 12 }, + ]; + const sdk = makeSdk({ + db: { + exec: () => {}, + prepare: () => ({ get: () => null, all: () => trades, run: () => ({ lastInsertRowid: 1 }) }), + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_calculate_risk_metrics"); + const result = await tool.execute({ mode: "all" }, {}); + assert.equal(result.success, true); + assert.ok("win_rate" in result.data); + assert.ok("max_drawdown_percent" in result.data); + assert.ok("value_at_risk_percent" in result.data); + assert.ok("sharpe_ratio" in result.data); + }); + }); + + // ── ton_trading_set_stop_loss ─────────────────────────────────────────────── + describe("ton_trading_set_stop_loss", () => { + it("required parameters include trade_id, entry_price, stop_loss_percent", () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_set_stop_loss"); + assert.ok(tool.parameters?.required?.includes("trade_id")); + assert.ok(tool.parameters?.required?.includes("entry_price")); + assert.ok(tool.parameters?.required?.includes("stop_loss_percent")); + }); + + it("registers a stop-loss rule and returns trigger prices", async () => { + const openTrade = { id: 1, status: "open" }; + const sdk = makeSdk({ dbRows: { trade: openTrade, lastInsertRowid: 5 } }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_set_stop_loss"); + const result = await tool.execute( + { trade_id: 1, entry_price: 100, stop_loss_percent: 10, take_profit_percent: 20 }, + {} + ); + assert.equal(result.success, true); + assert.ok("stop_loss_price" in result.data); + assert.ok("take_profit_price" in result.data); + assert.ok(result.data.stop_loss_price < 100, "stop loss price should be below entry"); + assert.ok(result.data.take_profit_price > 100, "take profit price should be above entry"); + }); + + it("returns failure when trade not found", async () => { + const sdk = makeSdk({ dbRows: { trade: null } }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_set_stop_loss"); + const result = await tool.execute({ trade_id: 999, entry_price: 100, stop_loss_percent: 5 }, {}); + assert.equal(result.success, false); + assert.ok(result.error.includes("not found")); + }); + + it("returns failure when trade is already closed", async () => { + const closedTrade = { id: 2, status: "closed" }; + const sdk = makeSdk({ dbRows: { trade: closedTrade } }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_set_stop_loss"); + const result = await tool.execute({ trade_id: 2, entry_price: 100, stop_loss_percent: 5 }, {}); + assert.equal(result.success, false); + assert.ok(result.error.includes("already closed")); + }); + }); + + // ── ton_trading_get_optimal_position_size ────────────────────────────────── + describe("ton_trading_get_optimal_position_size", () => { + it("required parameters include stop_loss_percent", () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_optimal_position_size"); + assert.ok(tool.parameters?.required?.includes("stop_loss_percent")); + }); + + it("returns position sizes for simulation mode with no history", async () => { + const sdk = makeSdk({ + dbRows: { simBalance: { balance: 1000 } }, + db: { + exec: () => {}, + prepare: (sql) => ({ + get: () => sql.includes("sim_balance") ? { balance: 1000 } : null, + all: () => [], + run: () => ({ lastInsertRowid: 1 }), + }), + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_optimal_position_size"); + const result = await tool.execute({ mode: "simulation", stop_loss_percent: 5 }, {}); + assert.equal(result.success, true); + assert.ok("kelly_position_size" in result.data); + assert.ok("fixed_fraction_position_size" in result.data); + assert.ok("recommendation" in result.data); + }); + + it("returns position sizes for real mode", async () => { + const sdk = makeSdk({ + db: { + exec: () => {}, + prepare: () => ({ get: () => null, all: () => [], run: () => ({ lastInsertRowid: 1 }) }), + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_optimal_position_size"); + const result = await tool.execute({ mode: "real", stop_loss_percent: 10, risk_percent: 2 }, {}); + assert.equal(result.success, true); + assert.equal(result.data.mode, "real"); + }); + }); + + // ── ton_trading_schedule_trade ────────────────────────────────────────────── + describe("ton_trading_schedule_trade", () => { + it("required parameters include mode, from_asset, to_asset, amount, execute_at_iso", () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_schedule_trade"); + assert.ok(tool.parameters?.required?.includes("mode")); + assert.ok(tool.parameters?.required?.includes("from_asset")); + assert.ok(tool.parameters?.required?.includes("to_asset")); + assert.ok(tool.parameters?.required?.includes("amount")); + assert.ok(tool.parameters?.required?.includes("execute_at_iso")); + }); + + it("schedules a trade and returns scheduled_id", async () => { + const sdk = makeSdk({ dbRows: { lastInsertRowid: 42 } }); + const futureDate = new Date(Date.now() + 3_600_000).toISOString(); // 1h from now + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_schedule_trade"); + const result = await tool.execute( + { mode: "simulation", from_asset: "TON", to_asset: "EQCxE6test", amount: 5, execute_at_iso: futureDate }, + {} + ); + assert.equal(result.success, true); + assert.equal(result.data.scheduled_id, 42); + assert.equal(result.data.status, "pending"); + }); + + it("returns failure for invalid date", async () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_schedule_trade"); + const result = await tool.execute( + { mode: "simulation", from_asset: "TON", to_asset: "EQCxE6test", amount: 5, execute_at_iso: "not-a-date" }, + {} + ); + assert.equal(result.success, false); + assert.ok(result.error.includes("Invalid execute_at_iso")); + }); + + it("returns failure for past date", async () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_schedule_trade"); + const pastDate = new Date(Date.now() - 3_600_000).toISOString(); + const result = await tool.execute( + { mode: "simulation", from_asset: "TON", to_asset: "EQCxE6test", amount: 5, execute_at_iso: pastDate }, + {} + ); + assert.equal(result.success, false); + assert.ok(result.error.includes("future")); + }); + }); + + // ── ton_trading_get_scheduled_trades ─────────────────────────────────────── + describe("ton_trading_get_scheduled_trades", () => { + it("has correct name and category", () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_scheduled_trades"); + assert.ok(tool); + assert.equal(tool.category, "data-bearing"); + }); + + it("returns scheduled trades list with due flag", async () => { + const now = Date.now(); + const dueTrade = { id: 1, execute_at: now - 1000, status: "pending", from_asset: "TON", to_asset: "EQCxE6test", amount: 5 }; + const pendingTrade = { id: 2, execute_at: now + 3_600_000, status: "pending", from_asset: "TON", to_asset: "EQCxE6test", amount: 3 }; + const sdk = makeSdk({ + db: { + exec: () => {}, + prepare: () => ({ + get: () => null, + all: () => [dueTrade, pendingTrade], + run: () => ({ lastInsertRowid: 1 }), + }), + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_trading_get_scheduled_trades"); + const result = await tool.execute({ status: "pending" }, {}); + assert.equal(result.success, true); + assert.ok(Array.isArray(result.data.scheduled_trades)); + assert.equal(result.data.due_now, 1); + assert.ok(result.data.scheduled_trades[0].is_due === true); + assert.ok(result.data.scheduled_trades[1].is_due === false); + }); + }); });