/** * Tournament tools: pick, leaderboard, and result submission. * @module openfinclaw/tournament/tools */ import { Type } from "@sinclair/typebox"; import type { TournamentDb } from "./db.js"; const VALID_AGENTS = ["bull", "bear", "contrarian"] as const; type AgentName = (typeof VALID_AGENTS)[number]; /** JSON helper for tool results. */ function json(data: unknown): { content: Array<{ type: "text"; text: string }>; details: unknown } { const text = typeof data === "string" ? data : JSON.stringify(data, null, 2); return { content: [{ type: "text" as const, text }], details: data }; } /** * Register tournament tools on the plugin API. * @param registerTool - api.registerTool from plugin entry * @param getDb - lazy getter for TournamentDb (DB may not be ready at registration time) */ export function registerTournamentTools( registerTool: (tool: unknown, opts?: unknown) => void, getDb: () => TournamentDb, ): void { // ── tournament_pick ───────────────────────────────────────────────── registerTool( (_ctx: unknown) => ({ name: "tournament_pick", label: "Tournament Pick", description: "Record a user's strategy pick for the current tournament round. Use when user says '/pick bull', '/pick bear', or '/pick contrarian'.", parameters: Type.Object({ agent_name: Type.String({ description: "Agent to pick: bull, bear, or contrarian", }), user_id: Type.Optional( Type.String({ description: "User identifier from the channel (auto-resolved if omitted)", }), ), session_key: Type.Optional( Type.String({ description: "OpenClaw session key (auto-resolved if omitted)" }), ), }), async execute(_toolCallId: string, params: Record) { const db = getDb(); const agentName = String(params.agent_name).toLowerCase().trim(); if (!VALID_AGENTS.includes(agentName as AgentName)) { return json({ error: true, message: `无效选择。请用 /pick bull|bear|contrarian,可选值: ${VALID_AGENTS.join(", ")}`, }); } const roundId = db.getLatestCompletedRoundId(); if (!roundId) { return json({ error: true, message: "当前没有活跃的锦标赛轮次。" }); } const userId = String(params.user_id ?? "unknown"); const sessionKey = String(params.session_key ?? "unknown"); const isNew = db.recordPick({ round_id: roundId, user_id: userId, session_key: sessionKey, agent_name: agentName, }); if (!isNew) { return json({ message: `你已经在本轮选择过了。你的选择: ${agentName}` }); } // Update agent W/L based on the pick const strategies = db.getStrategies(roundId); const picked = strategies.find((s) => s.agent_name === agentName); if (picked) { db.recordWin(agentName, picked.sharpe); for (const s of strategies) { if (s.agent_name !== agentName) { db.recordLoss(s.agent_name, s.sharpe); } } } return json({ message: `已记录你的选择: 🎯 ${agentName.toUpperCase()}`, round_id: roundId, agent_name: agentName, }); }, }), { names: ["tournament_pick"] }, ); // ── tournament_leaderboard ────────────────────────────────────────── registerTool( (_ctx: unknown) => ({ name: "tournament_leaderboard", label: "Tournament Leaderboard", description: "Show the strategy tournament agent leaderboard with win/loss records and Sharpe ratios.", parameters: Type.Object({}), async execute() { const db = getDb(); const agents = db.getLeaderboard(); if (agents.length === 0) { return json({ message: "还没有锦标赛记录。等待第一轮锦标赛完成后再查看。" }); } const medals = ["🥇", "🥈", "🥉"]; const lines = agents.map((a, i) => { const medal = medals[i] ?? " "; const winRate = a.rounds_played > 0 ? ((a.wins / a.rounds_played) * 100).toFixed(0) : "0"; return `${medal} ${a.name.toUpperCase().padEnd(12)} W:${String(a.wins).padStart(3)} L:${String(a.losses).padStart(3)} | Sharpe: ${a.avg_sharpe.toFixed(2)} | Win Rate: ${winRate}% | Rounds: ${a.rounds_played}`; }); return json({ message: `📊 策略锦标赛排行榜\n${"─".repeat(60)}\n${lines.join("\n")}\n${"─".repeat(60)}`, agents, }); }, }), { names: ["tournament_leaderboard"] }, ); // ── tournament_result ─────────────────────────────────────────────── registerTool( (_ctx: unknown) => ({ name: "tournament_result", label: "Tournament Result", description: "Submit analysis result from a tournament subagent. Called by Bull/Bear/Contrarian subagents after completing their analysis.", parameters: Type.Object({ round_id: Type.String({ description: "Tournament round ID" }), agent_name: Type.String({ description: "Agent role: bull, bear, or contrarian" }), thesis: Type.String({ description: "Analysis thesis (1 paragraph)" }), entry_price: Type.Optional(Type.Number({ description: "Entry price" })), exit_price: Type.Optional(Type.Number({ description: "Exit/target price" })), stop_loss: Type.Optional(Type.Number({ description: "Stop loss price" })), position_pct: Type.Optional( Type.Number({ description: "Position size as fraction (0-1)" }), ), confidence: Type.Number({ description: "Confidence score (0-100)" }), sharpe: Type.Optional(Type.Number({ description: "Sharpe ratio from backtest" })), max_drawdown: Type.Optional(Type.Number({ description: "Max drawdown from backtest" })), total_return: Type.Optional(Type.Number({ description: "Total return from backtest" })), }), async execute(_toolCallId: string, params: Record) { const db = getDb(); const agentName = String(params.agent_name).toLowerCase().trim(); const confidence = Number(params.confidence); if (!VALID_AGENTS.includes(agentName as AgentName)) { return json({ error: true, message: `Invalid agent name: ${agentName}` }); } if (confidence < 0 || confidence > 100 || !Number.isFinite(confidence)) { return json({ error: true, message: `Confidence must be 0-100, got: ${confidence}` }); } db.saveStrategy({ round_id: String(params.round_id), agent_name: agentName, thesis: String(params.thesis), entry_price: params.entry_price != null ? Number(params.entry_price) : null, exit_price: params.exit_price != null ? Number(params.exit_price) : null, stop_loss: params.stop_loss != null ? Number(params.stop_loss) : null, position_pct: params.position_pct != null ? Number(params.position_pct) : null, confidence, sharpe: params.sharpe != null ? Number(params.sharpe) : null, max_drawdown: params.max_drawdown != null ? Number(params.max_drawdown) : null, total_return: params.total_return != null ? Number(params.total_return) : null, raw_result: JSON.stringify(params), }); return json({ message: `Strategy submitted: ${agentName} (confidence: ${confidence})`, round_id: params.round_id, agent_name: agentName, }); }, }), { names: ["tournament_result"] }, ); }