@openfinclaw/openfinclaw-strategy 2026.3.275 → 2026.3.310
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.test.ts +4 -2
- package/index.ts +37 -16
- package/package.json +1 -1
- package/src/config.ts +3 -1
- package/src/db/repositories.ts +79 -17
- package/src/db/schema.ts +5 -1
- package/src/scheduler/news-provider.ts +1 -1
- package/src/scheduler/periodic-report-builder.ts +71 -0
- package/src/scheduler/scan-report-builder.ts +6 -2
- package/src/scheduler/tools.ts +362 -42
- package/src/strategy/tools.ts +114 -105
- package/src/tournament/cron-setup.ts +102 -0
- package/src/tournament/db.test.ts +222 -0
- package/src/tournament/db.ts +286 -0
- package/src/tournament/orchestrator.test.ts +232 -0
- package/src/tournament/orchestrator.ts +238 -0
- package/src/tournament/prompts.ts +65 -0
- package/src/tournament/tools.test.ts +221 -0
- package/src/tournament/tools.ts +192 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tournament tools: pick, leaderboard, and result submission.
|
|
3
|
+
* @module openfinclaw/tournament/tools
|
|
4
|
+
*/
|
|
5
|
+
import { Type } from "@sinclair/typebox";
|
|
6
|
+
import type { TournamentDb } from "./db.js";
|
|
7
|
+
|
|
8
|
+
const VALID_AGENTS = ["bull", "bear", "contrarian"] as const;
|
|
9
|
+
type AgentName = (typeof VALID_AGENTS)[number];
|
|
10
|
+
|
|
11
|
+
/** JSON helper for tool results. */
|
|
12
|
+
function json(data: unknown): { content: Array<{ type: "text"; text: string }>; details: unknown } {
|
|
13
|
+
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
14
|
+
return { content: [{ type: "text" as const, text }], details: data };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Register tournament tools on the plugin API.
|
|
19
|
+
* @param registerTool - api.registerTool from plugin entry
|
|
20
|
+
* @param getDb - lazy getter for TournamentDb (DB may not be ready at registration time)
|
|
21
|
+
*/
|
|
22
|
+
export function registerTournamentTools(
|
|
23
|
+
registerTool: (tool: unknown, opts?: unknown) => void,
|
|
24
|
+
getDb: () => TournamentDb,
|
|
25
|
+
): void {
|
|
26
|
+
// ── tournament_pick ─────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
registerTool(
|
|
29
|
+
(_ctx: unknown) => ({
|
|
30
|
+
name: "tournament_pick",
|
|
31
|
+
label: "Tournament Pick",
|
|
32
|
+
description:
|
|
33
|
+
"Record a user's strategy pick for the current tournament round. Use when user says '/pick bull', '/pick bear', or '/pick contrarian'.",
|
|
34
|
+
parameters: Type.Object({
|
|
35
|
+
agent_name: Type.String({
|
|
36
|
+
description: "Agent to pick: bull, bear, or contrarian",
|
|
37
|
+
}),
|
|
38
|
+
user_id: Type.Optional(
|
|
39
|
+
Type.String({
|
|
40
|
+
description: "User identifier from the channel (auto-resolved if omitted)",
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
session_key: Type.Optional(
|
|
44
|
+
Type.String({ description: "OpenClaw session key (auto-resolved if omitted)" }),
|
|
45
|
+
),
|
|
46
|
+
}),
|
|
47
|
+
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
|
48
|
+
const db = getDb();
|
|
49
|
+
const agentName = String(params.agent_name).toLowerCase().trim();
|
|
50
|
+
|
|
51
|
+
if (!VALID_AGENTS.includes(agentName as AgentName)) {
|
|
52
|
+
return json({
|
|
53
|
+
error: true,
|
|
54
|
+
message: `无效选择。请用 /pick bull|bear|contrarian,可选值: ${VALID_AGENTS.join(", ")}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const roundId = db.getLatestCompletedRoundId();
|
|
59
|
+
if (!roundId) {
|
|
60
|
+
return json({ error: true, message: "当前没有活跃的锦标赛轮次。" });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const userId = String(params.user_id ?? "unknown");
|
|
64
|
+
const sessionKey = String(params.session_key ?? "unknown");
|
|
65
|
+
|
|
66
|
+
const isNew = db.recordPick({
|
|
67
|
+
round_id: roundId,
|
|
68
|
+
user_id: userId,
|
|
69
|
+
session_key: sessionKey,
|
|
70
|
+
agent_name: agentName,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (!isNew) {
|
|
74
|
+
return json({ message: `你已经在本轮选择过了。你的选择: ${agentName}` });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Update agent W/L based on the pick
|
|
78
|
+
const strategies = db.getStrategies(roundId);
|
|
79
|
+
const picked = strategies.find((s) => s.agent_name === agentName);
|
|
80
|
+
if (picked) {
|
|
81
|
+
db.recordWin(agentName, picked.sharpe);
|
|
82
|
+
for (const s of strategies) {
|
|
83
|
+
if (s.agent_name !== agentName) {
|
|
84
|
+
db.recordLoss(s.agent_name, s.sharpe);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return json({
|
|
90
|
+
message: `已记录你的选择: 🎯 ${agentName.toUpperCase()}`,
|
|
91
|
+
round_id: roundId,
|
|
92
|
+
agent_name: agentName,
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
{ names: ["tournament_pick"] },
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// ── tournament_leaderboard ──────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
registerTool(
|
|
102
|
+
(_ctx: unknown) => ({
|
|
103
|
+
name: "tournament_leaderboard",
|
|
104
|
+
label: "Tournament Leaderboard",
|
|
105
|
+
description:
|
|
106
|
+
"Show the strategy tournament agent leaderboard with win/loss records and Sharpe ratios.",
|
|
107
|
+
parameters: Type.Object({}),
|
|
108
|
+
async execute() {
|
|
109
|
+
const db = getDb();
|
|
110
|
+
const agents = db.getLeaderboard();
|
|
111
|
+
|
|
112
|
+
if (agents.length === 0) {
|
|
113
|
+
return json({ message: "还没有锦标赛记录。等待第一轮锦标赛完成后再查看。" });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const medals = ["🥇", "🥈", "🥉"];
|
|
117
|
+
const lines = agents.map((a, i) => {
|
|
118
|
+
const medal = medals[i] ?? " ";
|
|
119
|
+
const winRate = a.rounds_played > 0 ? ((a.wins / a.rounds_played) * 100).toFixed(0) : "0";
|
|
120
|
+
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}`;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return json({
|
|
124
|
+
message: `📊 策略锦标赛排行榜\n${"─".repeat(60)}\n${lines.join("\n")}\n${"─".repeat(60)}`,
|
|
125
|
+
agents,
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
{ names: ["tournament_leaderboard"] },
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// ── tournament_result ───────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
registerTool(
|
|
135
|
+
(_ctx: unknown) => ({
|
|
136
|
+
name: "tournament_result",
|
|
137
|
+
label: "Tournament Result",
|
|
138
|
+
description:
|
|
139
|
+
"Submit analysis result from a tournament subagent. Called by Bull/Bear/Contrarian subagents after completing their analysis.",
|
|
140
|
+
parameters: Type.Object({
|
|
141
|
+
round_id: Type.String({ description: "Tournament round ID" }),
|
|
142
|
+
agent_name: Type.String({ description: "Agent role: bull, bear, or contrarian" }),
|
|
143
|
+
thesis: Type.String({ description: "Analysis thesis (1 paragraph)" }),
|
|
144
|
+
entry_price: Type.Optional(Type.Number({ description: "Entry price" })),
|
|
145
|
+
exit_price: Type.Optional(Type.Number({ description: "Exit/target price" })),
|
|
146
|
+
stop_loss: Type.Optional(Type.Number({ description: "Stop loss price" })),
|
|
147
|
+
position_pct: Type.Optional(
|
|
148
|
+
Type.Number({ description: "Position size as fraction (0-1)" }),
|
|
149
|
+
),
|
|
150
|
+
confidence: Type.Number({ description: "Confidence score (0-100)" }),
|
|
151
|
+
sharpe: Type.Optional(Type.Number({ description: "Sharpe ratio from backtest" })),
|
|
152
|
+
max_drawdown: Type.Optional(Type.Number({ description: "Max drawdown from backtest" })),
|
|
153
|
+
total_return: Type.Optional(Type.Number({ description: "Total return from backtest" })),
|
|
154
|
+
}),
|
|
155
|
+
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
|
156
|
+
const db = getDb();
|
|
157
|
+
const agentName = String(params.agent_name).toLowerCase().trim();
|
|
158
|
+
const confidence = Number(params.confidence);
|
|
159
|
+
|
|
160
|
+
if (!VALID_AGENTS.includes(agentName as AgentName)) {
|
|
161
|
+
return json({ error: true, message: `Invalid agent name: ${agentName}` });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (confidence < 0 || confidence > 100 || !Number.isFinite(confidence)) {
|
|
165
|
+
return json({ error: true, message: `Confidence must be 0-100, got: ${confidence}` });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
db.saveStrategy({
|
|
169
|
+
round_id: String(params.round_id),
|
|
170
|
+
agent_name: agentName,
|
|
171
|
+
thesis: String(params.thesis),
|
|
172
|
+
entry_price: params.entry_price != null ? Number(params.entry_price) : null,
|
|
173
|
+
exit_price: params.exit_price != null ? Number(params.exit_price) : null,
|
|
174
|
+
stop_loss: params.stop_loss != null ? Number(params.stop_loss) : null,
|
|
175
|
+
position_pct: params.position_pct != null ? Number(params.position_pct) : null,
|
|
176
|
+
confidence,
|
|
177
|
+
sharpe: params.sharpe != null ? Number(params.sharpe) : null,
|
|
178
|
+
max_drawdown: params.max_drawdown != null ? Number(params.max_drawdown) : null,
|
|
179
|
+
total_return: params.total_return != null ? Number(params.total_return) : null,
|
|
180
|
+
raw_result: JSON.stringify(params),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return json({
|
|
184
|
+
message: `Strategy submitted: ${agentName} (confidence: ${confidence})`,
|
|
185
|
+
round_id: params.round_id,
|
|
186
|
+
agent_name: agentName,
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
{ names: ["tournament_result"] },
|
|
191
|
+
);
|
|
192
|
+
}
|