@openfinclaw/openfinclaw-strategy 2026.4.9 → 2026.4.10

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 CHANGED
@@ -12,7 +12,7 @@ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
12
12
  import { registerStrategyCli } from "./src/cli.js";
13
13
  import { resolvePluginConfig } from "./src/config.js";
14
14
  import { registerDatahubTools } from "./src/datahub/tools.js";
15
- import { closeDb, getDb } from "./src/db/db.js";
15
+ import { getDb } from "./src/db/db.js";
16
16
  import { createOpenFinclawGatewayProxy } from "./src/http/gateway-proxy.js";
17
17
  import { startHttpServer } from "./src/http/server.js";
18
18
  import { OPENFINCLAW_AGENT_GUIDANCE } from "./src/prompt-guidance.js";
@@ -20,10 +20,6 @@ import { setupOpenfinclawCronJobs } from "./src/scheduler/cron-setup.js";
20
20
  import { AggregatedNewsProvider, createNewsProviders } from "./src/scheduler/news-provider.js";
21
21
  import { registerSchedulerTools } from "./src/scheduler/tools.js";
22
22
  import { registerStrategyTools } from "./src/strategy/tools.js";
23
- import { setupTournamentCronJob } from "./src/tournament/cron-setup.js";
24
- import { TournamentDb } from "./src/tournament/db.js";
25
- import { buildOrchestratorPrompt } from "./src/tournament/prompts.js";
26
- import { registerTournamentTools } from "./src/tournament/tools.js";
27
23
 
28
24
  export default definePluginEntry({
29
25
  id: "openfinclaw-strategy",
@@ -72,21 +68,11 @@ export default definePluginEntry({
72
68
  handler: createOpenFinclawGatewayProxy({ port: config.httpPort, logger: api.logger }),
73
69
  });
74
70
 
75
- // Register tournament tools (tournament_pick, tournament_leaderboard, tournament_result)
76
- const getTournamentDb = () => new TournamentDb(getDb());
77
- registerTournamentTools(api.registerTool.bind(api), getTournamentDb);
78
-
79
71
  // Inject agent system prompt: prioritise tool calls so data lands in SQLite
80
- const tournamentPrompt = buildOrchestratorPrompt();
81
72
  api.on("before_prompt_build", async () => ({
82
- prependSystemContext: `${OPENFINCLAW_AGENT_GUIDANCE}\n\n${tournamentPrompt}`,
73
+ prependSystemContext: OPENFINCLAW_AGENT_GUIDANCE,
83
74
  }));
84
75
 
85
- // Graceful shutdown: close SQLite connection
86
- process.once("beforeExit", () => {
87
- closeDb();
88
- });
89
-
90
76
  // ── Gateway Cron registration ──
91
77
  // Write cron jobs directly to ~/.openclaw/cron/jobs.json during register().
92
78
  // This ensures jobs are available immediately on both gateway startup AND
@@ -106,19 +92,6 @@ export default definePluginEntry({
106
92
  `[OpenFinClaw] Cron setup failed: ${err instanceof Error ? err.message : String(err)}`,
107
93
  );
108
94
  });
109
-
110
- // Register tournament cron job
111
- setupTournamentCronJob()
112
- .then((result) => {
113
- if (result.created) {
114
- api.logger.info("[OpenFinClaw] Tournament cron job registered");
115
- }
116
- })
117
- .catch((err) => {
118
- api.logger.info(
119
- `[OpenFinClaw] Tournament cron setup failed: ${err instanceof Error ? err.message : String(err)}`,
120
- );
121
- });
122
95
  }
123
96
  },
124
97
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfinclaw/openfinclaw-strategy",
3
- "version": "2026.4.9",
3
+ "version": "2026.4.10",
4
4
  "description": "OpenFinClaw - Unified financial tools: market data (price/K-line/crypto/compare/search), strategy publishing, fork, and validation. Single API key for Hub and DataHub.",
5
5
  "keywords": [
6
6
  "backtest",
package/src/db/db.ts CHANGED
@@ -12,7 +12,7 @@ import { mkdirSync, unlinkSync } from "node:fs";
12
12
  import { homedir } from "node:os";
13
13
  import { join } from "node:path";
14
14
  import { DatabaseSync } from "node:sqlite";
15
- import { cleanupOldRows, ensureSchema } from "./schema.js";
15
+ import { ensureSchema } from "./schema.js";
16
16
 
17
17
  /** globalThis key — survives module hot-reloads within the same process. */
18
18
  const GLOBAL_DB_KEY = "__openfinclaw_db__" as const;
@@ -52,8 +52,6 @@ export function getDb(): DatabaseSync {
52
52
  ensureSchema(db);
53
53
  }
54
54
  (globalThis as Record<string, unknown>)[GLOBAL_DB_KEY] = db;
55
- // Best-effort cleanup of old rows on first access each process lifecycle
56
- cleanupOldRows(db);
57
55
  return db;
58
56
  }
59
57
 
@@ -1,12 +1,7 @@
1
1
  /**
2
2
  * Data access helpers for OpenFinClaw plugin SQLite tables.
3
3
  */
4
- import type { DatabaseSync, SQLInputValue } from "node:sqlite";
5
-
6
- /** Cast unknown[] to SQLInputValue[] for db.prepare().run() spread. */
7
- function sqlParams(values: unknown[]): SQLInputValue[] {
8
- return values as SQLInputValue[];
9
- }
4
+ import type { DatabaseSync } from "node:sqlite";
10
5
 
11
6
  // ── Row types ─────────────────────────────────────────────────────────────
12
7
 
@@ -267,7 +262,7 @@ export function updateBacktestResult(
267
262
  }
268
263
  if (sets.length === 0) return;
269
264
  values.push(id);
270
- db.prepare(`UPDATE backtest_results SET ${sets.join(", ")} WHERE id = ?`).run(...sqlParams(values));
265
+ db.prepare(`UPDATE backtest_results SET ${sets.join(", ")} WHERE id = ?`).run(...values);
271
266
  } catch {
272
267
  // Logging must never crash the calling tool
273
268
  }
@@ -352,7 +347,7 @@ export function updateScanHistory(
352
347
  }
353
348
  if (sets.length === 0) return;
354
349
  values.push(id);
355
- db.prepare(`UPDATE scan_history SET ${sets.join(", ")} WHERE id = ?`).run(...sqlParams(values));
350
+ db.prepare(`UPDATE scan_history SET ${sets.join(", ")} WHERE id = ?`).run(...values);
356
351
  } catch {
357
352
  // Logging must never crash the calling tool
358
353
  }
@@ -383,7 +378,7 @@ export function queryScanHistory(
383
378
  params.push(limit, offset);
384
379
  return db
385
380
  .prepare(`SELECT * FROM scan_history ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?`)
386
- .all(...sqlParams(params)) as unknown as ScanHistoryEntry[];
381
+ .all(...params) as ScanHistoryEntry[];
387
382
  }
388
383
 
389
384
  /**
@@ -467,7 +462,7 @@ export function queryPriceAlerts(
467
462
  params.push(limit, offset);
468
463
  return db
469
464
  .prepare(`SELECT * FROM price_alerts ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`)
470
- .all(...sqlParams(params)) as unknown as PriceAlertEntry[];
465
+ .all(...params) as PriceAlertEntry[];
471
466
  }
472
467
 
473
468
  /** Count price_alerts rows since an inclusive time bound (ISO 8601 created_at). */
@@ -493,19 +488,19 @@ export function acknowledgePriceAlert(db: DatabaseSync, id: string): void {
493
488
  export function queryActivityLog(db: DatabaseSync, limit = 50, offset = 0): ActivityLogEntry[] {
494
489
  return db
495
490
  .prepare(`SELECT * FROM agent_activity_log ORDER BY timestamp DESC LIMIT ? OFFSET ?`)
496
- .all(limit, offset) as unknown as ActivityLogEntry[];
491
+ .all(limit, offset) as ActivityLogEntry[];
497
492
  }
498
493
 
499
494
  /** Query agent_events, newest first. */
500
495
  export function queryAgentEvents(db: DatabaseSync, limit = 50, offset = 0): AgentEventEntry[] {
501
496
  return db
502
497
  .prepare(`SELECT * FROM agent_events ORDER BY timestamp DESC LIMIT ? OFFSET ?`)
503
- .all(limit, offset) as unknown as AgentEventEntry[];
498
+ .all(limit, offset) as AgentEventEntry[];
504
499
  }
505
500
 
506
501
  /** Query all strategies, newest first. */
507
502
  export function queryStrategies(db: DatabaseSync): StrategyRow[] {
508
- return db.prepare(`SELECT * FROM strategies ORDER BY updated_at DESC`).all() as unknown as StrategyRow[];
503
+ return db.prepare(`SELECT * FROM strategies ORDER BY updated_at DESC`).all() as StrategyRow[];
509
504
  }
510
505
 
511
506
  /** Query backtest results, optionally filtered by strategy_id. */
@@ -513,9 +508,9 @@ export function queryBacktestResults(db: DatabaseSync, strategyId?: string): Bac
513
508
  if (strategyId) {
514
509
  return db
515
510
  .prepare(`SELECT * FROM backtest_results WHERE strategy_id = ? ORDER BY created_at DESC`)
516
- .all(strategyId) as unknown as BacktestRow[];
511
+ .all(strategyId) as BacktestRow[];
517
512
  }
518
513
  return db
519
514
  .prepare(`SELECT * FROM backtest_results ORDER BY created_at DESC`)
520
- .all() as unknown as BacktestRow[];
515
+ .all() as BacktestRow[];
521
516
  }
package/src/db/schema.ts CHANGED
@@ -3,7 +3,6 @@
3
3
  * Based on ER diagram v0.3.
4
4
  */
5
5
  import type { DatabaseSync } from "node:sqlite";
6
- import { ensureTournamentSchema } from "../tournament/db.js";
7
6
 
8
7
  /**
9
8
  * Create (or migrate) all MVP tables.
@@ -135,29 +134,9 @@ export function ensureSchema(db: DatabaseSync): void {
135
134
  db.exec(`CREATE INDEX IF NOT EXISTS idx_price_alerts_created ON price_alerts(created_at);`);
136
135
  db.exec(`CREATE INDEX IF NOT EXISTS idx_price_alerts_strategy ON price_alerts(strategy_id);`);
137
136
 
138
- // ── tournament tables ──────────────────────────────────────────────────
139
- ensureTournamentSchema(db);
140
-
141
137
  // ── migrations (add columns to existing databases) ──────────────────────
142
138
  }
143
139
 
144
- /**
145
- * Purge old rows from accumulating tables.
146
- * Safe to call periodically (e.g. on plugin register or via cron).
147
- * Keeps 90 days of activity logs, scan history, and acknowledged price alerts.
148
- */
149
- export function cleanupOldRows(db: DatabaseSync): void {
150
- const cutoff = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString();
151
- try {
152
- db.exec(`DELETE FROM agent_activity_log WHERE timestamp < '${cutoff}'`);
153
- db.exec(`DELETE FROM scan_history WHERE started_at < '${cutoff}'`);
154
- db.exec(`DELETE FROM price_alerts WHERE acknowledged = 1 AND created_at < '${cutoff}'`);
155
- db.exec(`DELETE FROM agent_events WHERE timestamp < '${cutoff}'`);
156
- } catch {
157
- // Cleanup is best-effort — never crash the plugin
158
- }
159
- }
160
-
161
140
  /** Add a column if it doesn't already exist (safe for repeated calls). */
162
141
  function ensureColumn(db: DatabaseSync, table: string, column: string, definition: string): void {
163
142
  const rows = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
@@ -23,6 +23,9 @@ import type { AggregatedNewsProvider } from "./news-provider.js";
23
23
  import { formatPeriodicReportMarkdown } from "./periodic-report-builder.js";
24
24
  import { buildScanReport, formatScanReportMarkdown } from "./scan-report-builder.js";
25
25
 
26
+ const NO_API_KEY =
27
+ "API key not configured. Set apiKey in plugin config or OPENFINCLAW_API_KEY env var.";
28
+
26
29
  /** JSON tool result helper. */
27
30
  function json(payload: unknown) {
28
31
  return {
@@ -421,6 +424,7 @@ export function registerSchedulerTools(
421
424
  "strategy_scan_history",
422
425
  "scheduler",
423
426
  async (_toolCallId, params) => {
427
+ if (!config.apiKey) return json({ success: false, error: NO_API_KEY });
424
428
  try {
425
429
  const db = getDb();
426
430
  const entries = queryScanHistory(db, {
@@ -489,6 +493,7 @@ export function registerSchedulerTools(
489
493
  "strategy_periodic_report",
490
494
  "scheduler",
491
495
  async (_toolCallId, params) => {
496
+ if (!config.apiKey) return json({ success: false, error: NO_API_KEY });
492
497
  const period = params.period === "monthly" ? "monthly" : "weekly";
493
498
  const scanId = randomUUID();
494
499
  const now = new Date();
@@ -21,7 +21,9 @@ export async function hubApiRequest(
21
21
  }
22
22
 
23
23
  const headers: Record<string, string> = { "Content-Type": "application/json" };
24
- if (config.apiKey) headers["Authorization"] = `Bearer ${config.apiKey}`;
24
+ if (config.apiKey) {
25
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
26
+ }
25
27
 
26
28
  const response = await fetch(url.toString(), {
27
29
  method,
@@ -346,6 +346,7 @@ export function registerStrategyTools(
346
346
  }),
347
347
  }),
348
348
  execute: withLogging(getDb, "skill_validate", "strategy", async (_toolCallId, params) => {
349
+ if (!config.apiKey) return json({ success: false, error: NO_API_KEY });
349
350
  try {
350
351
  const dirPath = String(params.dirPath ?? "").trim();
351
352
  if (!dirPath)
@@ -375,7 +376,7 @@ export function registerStrategyTools(
375
376
  {
376
377
  name: "skill_leaderboard",
377
378
  label: "Get Hub leaderboard",
378
- description: "Query strategy leaderboard from hub.openfinclaw.ai. No API key required.",
379
+ description: "Query strategy leaderboard from hub.openfinclaw.ai.",
379
380
  parameters: Type.Object({
380
381
  boardType: Type.Optional(
381
382
  Type.Unsafe<BoardType>({
@@ -390,6 +391,7 @@ export function registerStrategyTools(
390
391
  offset: Type.Optional(Type.Number({ description: "Offset for pagination" })),
391
392
  }),
392
393
  execute: withLogging(getDb, "skill_leaderboard", "strategy", async (_toolCallId, params) => {
394
+ if (!config.apiKey) return json({ success: false, error: NO_API_KEY });
393
395
  try {
394
396
  const boardType = (params.boardType as BoardType) || "composite";
395
397
  const limit = Math.min(Math.max(Number(params.limit) || 20, 1), 100);
@@ -401,7 +403,7 @@ export function registerStrategyTools(
401
403
 
402
404
  const response = await fetch(url.toString(), {
403
405
  method: "GET",
404
- headers: { Accept: "application/json" },
406
+ headers: { Accept: "application/json", Authorization: `Bearer ${config.apiKey}` },
405
407
  signal: AbortSignal.timeout(config.requestTimeoutMs),
406
408
  });
407
409
 
@@ -573,6 +575,7 @@ export function registerStrategyTools(
573
575
  description: "List all strategies downloaded or created locally, organized by date.",
574
576
  parameters: Type.Object({}),
575
577
  execute: withLogging(getDb, "skill_list_local", "strategy", async () => {
578
+ if (!config.apiKey) return json({ success: false, error: NO_API_KEY });
576
579
  try {
577
580
  const strategies = await listLocalStrategies();
578
581
 
@@ -622,7 +625,7 @@ export function registerStrategyTools(
622
625
  name: "skill_get_info",
623
626
  label: "Get strategy info from Hub",
624
627
  description:
625
- "Fetch detailed information about a strategy from hub.openfinclaw.ai. No API key required.",
628
+ "Fetch detailed information about a strategy from hub.openfinclaw.ai.",
626
629
  parameters: Type.Object({
627
630
  strategyId: Type.String({ description: "Strategy ID from Hub (UUID or Hub URL)" }),
628
631
  }),
@@ -631,6 +634,7 @@ export function registerStrategyTools(
631
634
  "skill_get_info",
632
635
  "strategy",
633
636
  async (_toolCallId, params) => {
637
+ if (!config.apiKey) return json({ success: false, error: NO_API_KEY });
634
638
  try {
635
639
  const strategyId = String(params.strategyId ?? "").trim();
636
640
  if (!strategyId) return json({ success: false, error: "strategyId is required" });
@@ -1,102 +0,0 @@
1
- /**
2
- * Tournament cron job registration.
3
- * Follows the same file-based pattern as scheduler/cron-setup.ts.
4
- * @module openfinclaw/tournament/cron-setup
5
- */
6
- import { randomUUID } from "node:crypto";
7
- import fs from "node:fs";
8
- import path from "node:path";
9
-
10
- const DEFAULT_TOURNAMENT_CRON = "0 9 * * 1-5"; // 9 AM weekdays
11
- const DEFAULT_TOURNAMENT_TZ = "Asia/Shanghai";
12
-
13
- interface StoredCronJob {
14
- id: string;
15
- name: string;
16
- enabled: boolean;
17
- schedule: { kind: "cron"; expr: string; tz?: string };
18
- payload: { kind: "systemEvent"; text: string };
19
- sessionTarget: string;
20
- wakeMode: string;
21
- delivery: { mode: string };
22
- createdAtMs: number;
23
- updatedAtMs: number;
24
- state: Record<string, unknown>;
25
- }
26
-
27
- interface CronStoreFile {
28
- version: 1;
29
- jobs: StoredCronJob[];
30
- }
31
-
32
- function defaultStorePath(): string {
33
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
34
- return path.join(home, ".openclaw", "cron", "jobs.json");
35
- }
36
-
37
- async function loadStore(storePath: string): Promise<CronStoreFile> {
38
- try {
39
- const raw = await fs.promises.readFile(storePath, "utf-8");
40
- const parsed = JSON.parse(raw) as Record<string, unknown>;
41
- const jobs = Array.isArray(parsed.jobs) ? (parsed.jobs as StoredCronJob[]) : [];
42
- return { version: 1, jobs };
43
- } catch (err) {
44
- if ((err as { code?: string }).code === "ENOENT") {
45
- return { version: 1, jobs: [] };
46
- }
47
- throw err;
48
- }
49
- }
50
-
51
- async function saveStore(storePath: string, store: CronStoreFile): Promise<void> {
52
- const dir = path.dirname(storePath);
53
- await fs.promises.mkdir(dir, { recursive: true });
54
- const json = JSON.stringify(store, null, 2);
55
- const tmp = `${storePath}.${process.pid}.${Date.now()}.tmp`;
56
- await fs.promises.writeFile(tmp, json, "utf-8");
57
- await fs.promises.rename(tmp, storePath);
58
- }
59
-
60
- const TOURNAMENT_JOB_NAME = "openfinclaw:tournament";
61
-
62
- /**
63
- * Register the daily tournament cron job.
64
- * Idempotent: skips if job already exists by name.
65
- */
66
- export async function setupTournamentCronJob(config?: {
67
- cronExpr?: string;
68
- timezone?: string;
69
- }): Promise<{ ok: boolean; created: boolean }> {
70
- const storePath = defaultStorePath();
71
- const store = await loadStore(storePath);
72
-
73
- const existingNames = new Set(store.jobs.map((j) => j.name));
74
- if (existingNames.has(TOURNAMENT_JOB_NAME)) {
75
- return { ok: true, created: false };
76
- }
77
-
78
- const cronExpr =
79
- config?.cronExpr ?? process.env.OPENFINCLAW_TOURNAMENT_CRON ?? DEFAULT_TOURNAMENT_CRON;
80
- const timezone = config?.timezone ?? DEFAULT_TOURNAMENT_TZ;
81
-
82
- const now = Date.now();
83
- store.jobs.push({
84
- id: randomUUID(),
85
- name: TOURNAMENT_JOB_NAME,
86
- enabled: true,
87
- schedule: { kind: "cron", expr: cronExpr, tz: timezone },
88
- payload: {
89
- kind: "systemEvent",
90
- text: "[openfinclaw:tournament] 每日策略锦标赛触发。请执行今日锦标赛流程。",
91
- },
92
- sessionTarget: "main",
93
- wakeMode: "now",
94
- delivery: { mode: "none" },
95
- createdAtMs: now,
96
- updatedAtMs: now,
97
- state: {},
98
- });
99
-
100
- await saveStore(storePath, store);
101
- return { ok: true, created: true };
102
- }
@@ -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
- });