@openfinclaw/openfinclaw-strategy 2026.3.310 → 2026.4.2
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.ts +3 -25
- package/openclaw.plugin.json +12 -2
- package/package.json +1 -1
- package/src/config.ts +27 -1
- package/src/db/schema.ts +0 -4
- package/src/scheduler/cron-setup.ts +93 -26
- package/src/types.ts +6 -0
- package/src/tournament/cron-setup.ts +0 -102
- package/src/tournament/db.test.ts +0 -222
- package/src/tournament/db.ts +0 -286
- package/src/tournament/orchestrator.test.ts +0 -232
- package/src/tournament/orchestrator.ts +0 -238
- package/src/tournament/prompts.ts +0 -65
- package/src/tournament/tools.test.ts +0 -221
- package/src/tournament/tools.ts +0 -192
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for tournament DB operations.
|
|
3
|
-
* @module openfinclaw/tournament/db.test
|
|
4
|
-
*/
|
|
5
|
-
import { DatabaseSync } from "node:sqlite";
|
|
6
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
7
|
-
import { TournamentDb, ensureTournamentSchema } from "./db.js";
|
|
8
|
-
|
|
9
|
-
describe("TournamentDb", () => {
|
|
10
|
-
let db: DatabaseSync;
|
|
11
|
-
let tdb: TournamentDb;
|
|
12
|
-
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
db = new DatabaseSync(":memory:");
|
|
15
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
16
|
-
ensureTournamentSchema(db);
|
|
17
|
-
tdb = new TournamentDb(db);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
db.close();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe("schema", () => {
|
|
25
|
-
it("creates tables idempotently", () => {
|
|
26
|
-
ensureTournamentSchema(db);
|
|
27
|
-
ensureTournamentSchema(db);
|
|
28
|
-
const tables = db
|
|
29
|
-
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'tournament_%'")
|
|
30
|
-
.all() as Array<{ name: string }>;
|
|
31
|
-
const names = tables.map((t) => t.name).sort();
|
|
32
|
-
expect(names).toEqual([
|
|
33
|
-
"tournament_agents",
|
|
34
|
-
"tournament_picks",
|
|
35
|
-
"tournament_rounds",
|
|
36
|
-
"tournament_strategies",
|
|
37
|
-
]);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe("rounds", () => {
|
|
42
|
-
it("creates a round", () => {
|
|
43
|
-
const created = tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" });
|
|
44
|
-
expect(created).toBe(true);
|
|
45
|
-
const round = tdb.getRound("round-20260331");
|
|
46
|
-
expect(round).toBeDefined();
|
|
47
|
-
expect(round!.ticker).toBe("AAPL");
|
|
48
|
-
expect(round!.status).toBe("running");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("returns false for duplicate round", () => {
|
|
52
|
-
tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" });
|
|
53
|
-
const dup = tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "TSLA" });
|
|
54
|
-
expect(dup).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("updates round status to completed", () => {
|
|
58
|
-
tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" });
|
|
59
|
-
tdb.updateRoundStatus("round-20260331", "completed");
|
|
60
|
-
const round = tdb.getRound("round-20260331")!;
|
|
61
|
-
expect(round.status).toBe("completed");
|
|
62
|
-
expect(round.completed_at).toBeTruthy();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("updates round status to skipped with reason", () => {
|
|
66
|
-
tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" });
|
|
67
|
-
tdb.updateRoundStatus("round-20260331", "skipped", "Less than 2 agents succeeded");
|
|
68
|
-
const round = tdb.getRound("round-20260331")!;
|
|
69
|
-
expect(round.status).toBe("skipped");
|
|
70
|
-
expect(round.skip_reason).toBe("Less than 2 agents succeeded");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("counts consecutive skips", () => {
|
|
74
|
-
tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "A" });
|
|
75
|
-
tdb.updateRoundStatus("round-20260331", "skipped");
|
|
76
|
-
tdb.createRound({ id: "round-20260330", date: "2026-03-30", ticker: "B" });
|
|
77
|
-
tdb.updateRoundStatus("round-20260330", "skipped");
|
|
78
|
-
tdb.createRound({ id: "round-20260329", date: "2026-03-29", ticker: "C" });
|
|
79
|
-
tdb.updateRoundStatus("round-20260329", "completed");
|
|
80
|
-
expect(tdb.countConsecutiveSkips()).toBe(2);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe("strategies", () => {
|
|
85
|
-
it("saves and retrieves strategies", () => {
|
|
86
|
-
tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" });
|
|
87
|
-
tdb.saveStrategy({
|
|
88
|
-
round_id: "round-20260331",
|
|
89
|
-
agent_name: "bull",
|
|
90
|
-
thesis: "EMA crossover bullish",
|
|
91
|
-
entry_price: 150.0,
|
|
92
|
-
exit_price: 165.0,
|
|
93
|
-
stop_loss: 145.0,
|
|
94
|
-
position_pct: 0.25,
|
|
95
|
-
confidence: 85,
|
|
96
|
-
sharpe: 1.5,
|
|
97
|
-
max_drawdown: -0.08,
|
|
98
|
-
total_return: 0.12,
|
|
99
|
-
raw_result: '{"detail":"full analysis"}',
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
const strategies = tdb.getStrategies("round-20260331");
|
|
103
|
-
expect(strategies).toHaveLength(1);
|
|
104
|
-
expect(strategies[0].agent_name).toBe("bull");
|
|
105
|
-
expect(strategies[0].confidence).toBe(85);
|
|
106
|
-
expect(strategies[0].sharpe).toBe(1.5);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("upserts strategy on conflict", () => {
|
|
110
|
-
tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" });
|
|
111
|
-
tdb.saveStrategy({
|
|
112
|
-
round_id: "round-20260331",
|
|
113
|
-
agent_name: "bull",
|
|
114
|
-
thesis: "First",
|
|
115
|
-
confidence: 50,
|
|
116
|
-
entry_price: null,
|
|
117
|
-
exit_price: null,
|
|
118
|
-
stop_loss: null,
|
|
119
|
-
position_pct: null,
|
|
120
|
-
sharpe: null,
|
|
121
|
-
max_drawdown: null,
|
|
122
|
-
total_return: null,
|
|
123
|
-
raw_result: null,
|
|
124
|
-
});
|
|
125
|
-
tdb.saveStrategy({
|
|
126
|
-
round_id: "round-20260331",
|
|
127
|
-
agent_name: "bull",
|
|
128
|
-
thesis: "Updated",
|
|
129
|
-
confidence: 90,
|
|
130
|
-
entry_price: 100,
|
|
131
|
-
exit_price: 110,
|
|
132
|
-
stop_loss: 95,
|
|
133
|
-
position_pct: 0.3,
|
|
134
|
-
sharpe: 2.0,
|
|
135
|
-
max_drawdown: -0.05,
|
|
136
|
-
total_return: 0.2,
|
|
137
|
-
raw_result: null,
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const strategies = tdb.getStrategies("round-20260331");
|
|
141
|
-
expect(strategies).toHaveLength(1);
|
|
142
|
-
expect(strategies[0].thesis).toBe("Updated");
|
|
143
|
-
expect(strategies[0].confidence).toBe(90);
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
describe("agents", () => {
|
|
148
|
-
it("records win and updates avg sharpe", () => {
|
|
149
|
-
tdb.recordWin("bull", 1.5);
|
|
150
|
-
tdb.recordWin("bull", 2.5);
|
|
151
|
-
const agent = tdb.getAgent("bull")!;
|
|
152
|
-
expect(agent.wins).toBe(2);
|
|
153
|
-
expect(agent.rounds_played).toBe(2);
|
|
154
|
-
expect(agent.avg_sharpe).toBe(2.0);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("records loss", () => {
|
|
158
|
-
tdb.recordLoss("bear", 0.5);
|
|
159
|
-
const agent = tdb.getAgent("bear")!;
|
|
160
|
-
expect(agent.losses).toBe(1);
|
|
161
|
-
expect(agent.rounds_played).toBe(1);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it("returns leaderboard sorted by avg_sharpe", () => {
|
|
165
|
-
tdb.recordWin("bull", 2.0);
|
|
166
|
-
tdb.recordWin("bear", 1.0);
|
|
167
|
-
tdb.recordWin("contrarian", 3.0);
|
|
168
|
-
const lb = tdb.getLeaderboard();
|
|
169
|
-
expect(lb[0].name).toBe("contrarian");
|
|
170
|
-
expect(lb[1].name).toBe("bull");
|
|
171
|
-
expect(lb[2].name).toBe("bear");
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
describe("picks", () => {
|
|
176
|
-
it("records a pick", () => {
|
|
177
|
-
tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" });
|
|
178
|
-
const recorded = tdb.recordPick({
|
|
179
|
-
round_id: "round-20260331",
|
|
180
|
-
user_id: "telegram:12345",
|
|
181
|
-
session_key: "agent:main:telegram",
|
|
182
|
-
agent_name: "bull",
|
|
183
|
-
});
|
|
184
|
-
expect(recorded).toBe(true);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("returns false for duplicate pick by same user", () => {
|
|
188
|
-
tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" });
|
|
189
|
-
tdb.recordPick({
|
|
190
|
-
round_id: "round-20260331",
|
|
191
|
-
user_id: "telegram:12345",
|
|
192
|
-
session_key: "agent:main:telegram",
|
|
193
|
-
agent_name: "bull",
|
|
194
|
-
});
|
|
195
|
-
const dup = tdb.recordPick({
|
|
196
|
-
round_id: "round-20260331",
|
|
197
|
-
user_id: "telegram:12345",
|
|
198
|
-
session_key: "agent:main:telegram",
|
|
199
|
-
agent_name: "bear",
|
|
200
|
-
});
|
|
201
|
-
expect(dup).toBe(false);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("allows different users to pick same round", () => {
|
|
205
|
-
tdb.createRound({ id: "round-20260331", date: "2026-03-31", ticker: "AAPL" });
|
|
206
|
-
const pick1 = tdb.recordPick({
|
|
207
|
-
round_id: "round-20260331",
|
|
208
|
-
user_id: "telegram:12345",
|
|
209
|
-
session_key: "agent:main:telegram",
|
|
210
|
-
agent_name: "bull",
|
|
211
|
-
});
|
|
212
|
-
const pick2 = tdb.recordPick({
|
|
213
|
-
round_id: "round-20260331",
|
|
214
|
-
user_id: "telegram:67890",
|
|
215
|
-
session_key: "agent:main:telegram",
|
|
216
|
-
agent_name: "bear",
|
|
217
|
-
});
|
|
218
|
-
expect(pick1).toBe(true);
|
|
219
|
-
expect(pick2).toBe(true);
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
});
|
package/src/tournament/db.ts
DELETED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tournament SQLite persistence layer.
|
|
3
|
-
* Manages rounds, strategies, agent records, and user picks.
|
|
4
|
-
* @module openfinclaw/tournament/db
|
|
5
|
-
*/
|
|
6
|
-
import type { DatabaseSync } from "node:sqlite";
|
|
7
|
-
|
|
8
|
-
// ── Types ─────────────────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
export interface TournamentRound {
|
|
11
|
-
id: string;
|
|
12
|
-
date: string;
|
|
13
|
-
ticker: string;
|
|
14
|
-
status: "pending" | "running" | "completed" | "skipped";
|
|
15
|
-
started_at: string | null;
|
|
16
|
-
completed_at: string | null;
|
|
17
|
-
skip_reason: string | null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface TournamentStrategy {
|
|
21
|
-
round_id: string;
|
|
22
|
-
agent_name: string;
|
|
23
|
-
thesis: string;
|
|
24
|
-
entry_price: number | null;
|
|
25
|
-
exit_price: number | null;
|
|
26
|
-
stop_loss: number | null;
|
|
27
|
-
position_pct: number | null;
|
|
28
|
-
confidence: number;
|
|
29
|
-
sharpe: number | null;
|
|
30
|
-
max_drawdown: number | null;
|
|
31
|
-
total_return: number | null;
|
|
32
|
-
raw_result: string | null;
|
|
33
|
-
created_at: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface TournamentAgent {
|
|
37
|
-
name: string;
|
|
38
|
-
wins: number;
|
|
39
|
-
losses: number;
|
|
40
|
-
avg_sharpe: number;
|
|
41
|
-
rounds_played: number;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface TournamentPick {
|
|
45
|
-
round_id: string;
|
|
46
|
-
user_id: string;
|
|
47
|
-
session_key: string;
|
|
48
|
-
agent_name: string;
|
|
49
|
-
picked_at: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ── Schema ────────────────────────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
/** Create tournament tables. Safe to call multiple times. */
|
|
55
|
-
export function ensureTournamentSchema(db: DatabaseSync): void {
|
|
56
|
-
db.exec(`
|
|
57
|
-
CREATE TABLE IF NOT EXISTS tournament_rounds (
|
|
58
|
-
id TEXT PRIMARY KEY,
|
|
59
|
-
date TEXT NOT NULL,
|
|
60
|
-
ticker TEXT NOT NULL,
|
|
61
|
-
status TEXT NOT NULL DEFAULT 'pending',
|
|
62
|
-
started_at TEXT,
|
|
63
|
-
completed_at TEXT,
|
|
64
|
-
skip_reason TEXT
|
|
65
|
-
);
|
|
66
|
-
`);
|
|
67
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_tournament_rounds_date ON tournament_rounds(date);`);
|
|
68
|
-
|
|
69
|
-
db.exec(`
|
|
70
|
-
CREATE TABLE IF NOT EXISTS tournament_strategies (
|
|
71
|
-
round_id TEXT NOT NULL,
|
|
72
|
-
agent_name TEXT NOT NULL,
|
|
73
|
-
thesis TEXT NOT NULL,
|
|
74
|
-
entry_price REAL,
|
|
75
|
-
exit_price REAL,
|
|
76
|
-
stop_loss REAL,
|
|
77
|
-
position_pct REAL,
|
|
78
|
-
confidence INTEGER NOT NULL DEFAULT 50,
|
|
79
|
-
sharpe REAL,
|
|
80
|
-
max_drawdown REAL,
|
|
81
|
-
total_return REAL,
|
|
82
|
-
raw_result TEXT,
|
|
83
|
-
created_at TEXT NOT NULL,
|
|
84
|
-
PRIMARY KEY (round_id, agent_name)
|
|
85
|
-
);
|
|
86
|
-
`);
|
|
87
|
-
|
|
88
|
-
db.exec(`
|
|
89
|
-
CREATE TABLE IF NOT EXISTS tournament_agents (
|
|
90
|
-
name TEXT PRIMARY KEY,
|
|
91
|
-
wins INTEGER NOT NULL DEFAULT 0,
|
|
92
|
-
losses INTEGER NOT NULL DEFAULT 0,
|
|
93
|
-
avg_sharpe REAL NOT NULL DEFAULT 0,
|
|
94
|
-
rounds_played INTEGER NOT NULL DEFAULT 0
|
|
95
|
-
);
|
|
96
|
-
`);
|
|
97
|
-
|
|
98
|
-
db.exec(`
|
|
99
|
-
CREATE TABLE IF NOT EXISTS tournament_picks (
|
|
100
|
-
round_id TEXT NOT NULL,
|
|
101
|
-
user_id TEXT NOT NULL,
|
|
102
|
-
session_key TEXT NOT NULL,
|
|
103
|
-
agent_name TEXT NOT NULL,
|
|
104
|
-
picked_at TEXT NOT NULL,
|
|
105
|
-
PRIMARY KEY (round_id, user_id)
|
|
106
|
-
);
|
|
107
|
-
`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// ── CRUD Operations ───────────────────────────────────────────────────────
|
|
111
|
-
|
|
112
|
-
export class TournamentDb {
|
|
113
|
-
constructor(private db: DatabaseSync) {}
|
|
114
|
-
|
|
115
|
-
// ── Rounds ──────────────────────────────────────────────────────────
|
|
116
|
-
|
|
117
|
-
/** Create a new tournament round. Returns false if already exists. */
|
|
118
|
-
createRound(round: { id: string; date: string; ticker: string }): boolean {
|
|
119
|
-
const existing = this.getRound(round.id);
|
|
120
|
-
if (existing) return false;
|
|
121
|
-
|
|
122
|
-
const now = new Date().toISOString();
|
|
123
|
-
this.db
|
|
124
|
-
.prepare(
|
|
125
|
-
"INSERT INTO tournament_rounds (id, date, ticker, status, started_at) VALUES (?, ?, ?, 'running', ?)",
|
|
126
|
-
)
|
|
127
|
-
.run(round.id, round.date, round.ticker, now);
|
|
128
|
-
return true;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Get a round by ID. */
|
|
132
|
-
getRound(id: string): TournamentRound | undefined {
|
|
133
|
-
return this.db.prepare("SELECT * FROM tournament_rounds WHERE id = ?").get(id) as
|
|
134
|
-
| TournamentRound
|
|
135
|
-
| undefined;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** Update round status. */
|
|
139
|
-
updateRoundStatus(id: string, status: TournamentRound["status"], skipReason?: string): void {
|
|
140
|
-
const now = new Date().toISOString();
|
|
141
|
-
if (status === "completed" || status === "skipped") {
|
|
142
|
-
this.db
|
|
143
|
-
.prepare(
|
|
144
|
-
"UPDATE tournament_rounds SET status = ?, completed_at = ?, skip_reason = ? WHERE id = ?",
|
|
145
|
-
)
|
|
146
|
-
.run(status, now, skipReason ?? null, id);
|
|
147
|
-
} else {
|
|
148
|
-
this.db.prepare("UPDATE tournament_rounds SET status = ? WHERE id = ?").run(status, id);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** Get the latest N rounds ordered by date desc. */
|
|
153
|
-
getRecentRounds(limit: number): TournamentRound[] {
|
|
154
|
-
return this.db
|
|
155
|
-
.prepare("SELECT * FROM tournament_rounds ORDER BY date DESC LIMIT ?")
|
|
156
|
-
.all(limit) as TournamentRound[];
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Count consecutive skipped rounds from the most recent. */
|
|
160
|
-
countConsecutiveSkips(): number {
|
|
161
|
-
const recent = this.getRecentRounds(10);
|
|
162
|
-
let count = 0;
|
|
163
|
-
for (const r of recent) {
|
|
164
|
-
if (r.status === "skipped") count++;
|
|
165
|
-
else break;
|
|
166
|
-
}
|
|
167
|
-
return count;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ── Strategies ──────────────────────────────────────────────────────
|
|
171
|
-
|
|
172
|
-
/** Save a strategy result from a subagent. Upserts by (round_id, agent_name). */
|
|
173
|
-
saveStrategy(strategy: Omit<TournamentStrategy, "created_at">): void {
|
|
174
|
-
const now = new Date().toISOString();
|
|
175
|
-
this.db
|
|
176
|
-
.prepare(
|
|
177
|
-
`INSERT OR REPLACE INTO tournament_strategies
|
|
178
|
-
(round_id, agent_name, thesis, entry_price, exit_price, stop_loss,
|
|
179
|
-
position_pct, confidence, sharpe, max_drawdown, total_return, raw_result, created_at)
|
|
180
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
181
|
-
)
|
|
182
|
-
.run(
|
|
183
|
-
strategy.round_id,
|
|
184
|
-
strategy.agent_name,
|
|
185
|
-
strategy.thesis,
|
|
186
|
-
strategy.entry_price ?? null,
|
|
187
|
-
strategy.exit_price ?? null,
|
|
188
|
-
strategy.stop_loss ?? null,
|
|
189
|
-
strategy.position_pct ?? null,
|
|
190
|
-
strategy.confidence,
|
|
191
|
-
strategy.sharpe ?? null,
|
|
192
|
-
strategy.max_drawdown ?? null,
|
|
193
|
-
strategy.total_return ?? null,
|
|
194
|
-
strategy.raw_result ?? null,
|
|
195
|
-
now,
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** Get all strategies for a round. */
|
|
200
|
-
getStrategies(roundId: string): TournamentStrategy[] {
|
|
201
|
-
return this.db
|
|
202
|
-
.prepare("SELECT * FROM tournament_strategies WHERE round_id = ? ORDER BY agent_name")
|
|
203
|
-
.all(roundId) as TournamentStrategy[];
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// ── Agents ──────────────────────────────────────────────────────────
|
|
207
|
-
|
|
208
|
-
/** Ensure an agent record exists. */
|
|
209
|
-
ensureAgent(name: string): void {
|
|
210
|
-
this.db.prepare("INSERT OR IGNORE INTO tournament_agents (name) VALUES (?)").run(name);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** Record a win for an agent. Updates avg_sharpe from the strategy's backtest. */
|
|
214
|
-
recordWin(name: string, sharpe: number | null): void {
|
|
215
|
-
this.ensureAgent(name);
|
|
216
|
-
const agent = this.getAgent(name)!;
|
|
217
|
-
const newRounds = agent.rounds_played + 1;
|
|
218
|
-
const newSharpe =
|
|
219
|
-
sharpe != null
|
|
220
|
-
? (agent.avg_sharpe * agent.rounds_played + sharpe) / newRounds
|
|
221
|
-
: agent.avg_sharpe;
|
|
222
|
-
this.db
|
|
223
|
-
.prepare(
|
|
224
|
-
"UPDATE tournament_agents SET wins = wins + 1, rounds_played = ?, avg_sharpe = ? WHERE name = ?",
|
|
225
|
-
)
|
|
226
|
-
.run(newRounds, newSharpe, name);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/** Record a loss for an agent. */
|
|
230
|
-
recordLoss(name: string, sharpe: number | null): void {
|
|
231
|
-
this.ensureAgent(name);
|
|
232
|
-
const agent = this.getAgent(name)!;
|
|
233
|
-
const newRounds = agent.rounds_played + 1;
|
|
234
|
-
const newSharpe =
|
|
235
|
-
sharpe != null
|
|
236
|
-
? (agent.avg_sharpe * agent.rounds_played + sharpe) / newRounds
|
|
237
|
-
: agent.avg_sharpe;
|
|
238
|
-
this.db
|
|
239
|
-
.prepare(
|
|
240
|
-
"UPDATE tournament_agents SET losses = losses + 1, rounds_played = ?, avg_sharpe = ? WHERE name = ?",
|
|
241
|
-
)
|
|
242
|
-
.run(newRounds, newSharpe, name);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/** Get agent stats. */
|
|
246
|
-
getAgent(name: string): TournamentAgent | undefined {
|
|
247
|
-
return this.db.prepare("SELECT * FROM tournament_agents WHERE name = ?").get(name) as
|
|
248
|
-
| TournamentAgent
|
|
249
|
-
| undefined;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/** Get all agents sorted by avg_sharpe desc. */
|
|
253
|
-
getLeaderboard(): TournamentAgent[] {
|
|
254
|
-
return this.db
|
|
255
|
-
.prepare("SELECT * FROM tournament_agents ORDER BY avg_sharpe DESC, wins DESC")
|
|
256
|
-
.all() as TournamentAgent[];
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// ── Picks ───────────────────────────────────────────────────────────
|
|
260
|
-
|
|
261
|
-
/** Record a user's pick. Idempotent per (round_id, user_id). Returns true if new. */
|
|
262
|
-
recordPick(pick: Omit<TournamentPick, "picked_at">): boolean {
|
|
263
|
-
const existing = this.db
|
|
264
|
-
.prepare("SELECT * FROM tournament_picks WHERE round_id = ? AND user_id = ?")
|
|
265
|
-
.get(pick.round_id, pick.user_id) as TournamentPick | undefined;
|
|
266
|
-
if (existing) return false;
|
|
267
|
-
|
|
268
|
-
const now = new Date().toISOString();
|
|
269
|
-
this.db
|
|
270
|
-
.prepare(
|
|
271
|
-
"INSERT INTO tournament_picks (round_id, user_id, session_key, agent_name, picked_at) VALUES (?, ?, ?, ?, ?)",
|
|
272
|
-
)
|
|
273
|
-
.run(pick.round_id, pick.user_id, pick.session_key, pick.agent_name, now);
|
|
274
|
-
return true;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/** Get the most recent completed round ID. */
|
|
278
|
-
getLatestCompletedRoundId(): string | undefined {
|
|
279
|
-
const row = this.db
|
|
280
|
-
.prepare(
|
|
281
|
-
"SELECT id FROM tournament_rounds WHERE status = 'completed' ORDER BY date DESC LIMIT 1",
|
|
282
|
-
)
|
|
283
|
-
.get() as { id: string } | undefined;
|
|
284
|
-
return row?.id;
|
|
285
|
-
}
|
|
286
|
-
}
|