horizon-code 0.5.1 → 0.6.1
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/package.json +1 -1
- package/src/ai/client.ts +13 -6
- package/src/ai/system-prompt.ts +20 -1
- package/src/app.ts +184 -11
- package/src/chat/renderer.ts +106 -24
- package/src/components/footer.ts +1 -1
- package/src/keys/handler.ts +8 -0
- package/src/platform/auth.ts +3 -3
- package/src/platform/profile.ts +202 -0
- package/src/platform/session-sync.ts +3 -3
- package/src/platform/supabase.ts +10 -9
- package/src/platform/sync.ts +9 -1
- package/src/research/apis.ts +13 -13
- package/src/research/scanner.ts +212 -0
- package/src/research/tools.ts +58 -0
- package/src/strategy/alerts.ts +190 -0
- package/src/strategy/export.ts +159 -0
- package/src/strategy/health.ts +127 -0
- package/src/strategy/ledger.ts +185 -0
- package/src/strategy/prompts.ts +136 -551
- package/src/strategy/replay.ts +191 -0
- package/src/strategy/tools.ts +495 -1
- package/src/strategy/versioning.ts +168 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Performance ledger — persistent log of every backtest and run
|
|
2
|
+
// Storage: ~/.horizon/ledger/<strategy-slug>.jsonl (append-only)
|
|
3
|
+
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { resolve, join } from "path";
|
|
6
|
+
import { existsSync, mkdirSync, appendFileSync, readFileSync } from "fs";
|
|
7
|
+
import { createHash } from "crypto";
|
|
8
|
+
|
|
9
|
+
const LEDGER_ROOT = resolve(homedir(), ".horizon", "ledger");
|
|
10
|
+
|
|
11
|
+
export interface LedgerEntry {
|
|
12
|
+
strategy_name: string;
|
|
13
|
+
code_hash: string;
|
|
14
|
+
type: "backtest" | "run";
|
|
15
|
+
timestamp: string;
|
|
16
|
+
duration_secs: number;
|
|
17
|
+
params: Record<string, unknown>;
|
|
18
|
+
metrics: {
|
|
19
|
+
pnl: number;
|
|
20
|
+
rpnl: number;
|
|
21
|
+
upnl: number;
|
|
22
|
+
sharpe: number;
|
|
23
|
+
max_dd: number;
|
|
24
|
+
win_rate: number;
|
|
25
|
+
trades: number;
|
|
26
|
+
exposure: number;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
};
|
|
29
|
+
label?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ensureDir(): void {
|
|
33
|
+
if (!existsSync(LEDGER_ROOT)) mkdirSync(LEDGER_ROOT, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function slugify(name: string): string {
|
|
37
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function codeHash(code: string): string {
|
|
41
|
+
return createHash("sha256").update(code).digest("hex").slice(0, 12);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Append a new entry to the ledger (append-only JSONL) */
|
|
45
|
+
export function logEntry(entry: LedgerEntry): void {
|
|
46
|
+
ensureDir();
|
|
47
|
+
const slug = slugify(entry.strategy_name);
|
|
48
|
+
const path = join(LEDGER_ROOT, `${slug}.jsonl`);
|
|
49
|
+
appendFileSync(path, JSON.stringify(entry) + "\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Get all entries for a strategy, newest first */
|
|
53
|
+
export function getHistory(name: string): LedgerEntry[] {
|
|
54
|
+
ensureDir();
|
|
55
|
+
const slug = slugify(name);
|
|
56
|
+
const path = join(LEDGER_ROOT, `${slug}.jsonl`);
|
|
57
|
+
if (!existsSync(path)) return [];
|
|
58
|
+
|
|
59
|
+
const lines = readFileSync(path, "utf-8").split("\n").filter(Boolean);
|
|
60
|
+
const entries: LedgerEntry[] = [];
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
try { entries.push(JSON.parse(line)); } catch {}
|
|
63
|
+
}
|
|
64
|
+
return entries.reverse();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Find the best run by a specific metric (e.g., "sharpe", "pnl", "win_rate") */
|
|
68
|
+
export function getBestRun(name: string, metric: string = "sharpe"): LedgerEntry | null {
|
|
69
|
+
const history = getHistory(name);
|
|
70
|
+
if (history.length === 0) return null;
|
|
71
|
+
|
|
72
|
+
return history.reduce((best, entry) => {
|
|
73
|
+
const bestVal = (best.metrics as any)[metric] ?? -Infinity;
|
|
74
|
+
const entryVal = (entry.metrics as any)[metric] ?? -Infinity;
|
|
75
|
+
return entryVal > bestVal ? entry : best;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Generate a strategy performance report */
|
|
80
|
+
export function generateReport(name: string): {
|
|
81
|
+
total_runs: number;
|
|
82
|
+
backtests: number;
|
|
83
|
+
live_runs: number;
|
|
84
|
+
best_sharpe: LedgerEntry | null;
|
|
85
|
+
best_pnl: LedgerEntry | null;
|
|
86
|
+
worst_dd: LedgerEntry | null;
|
|
87
|
+
avg_sharpe: number;
|
|
88
|
+
avg_win_rate: number;
|
|
89
|
+
avg_pnl: number;
|
|
90
|
+
trend: string;
|
|
91
|
+
versions_tested: number;
|
|
92
|
+
summary: string;
|
|
93
|
+
} {
|
|
94
|
+
const history = getHistory(name);
|
|
95
|
+
if (history.length === 0) {
|
|
96
|
+
return {
|
|
97
|
+
total_runs: 0, backtests: 0, live_runs: 0,
|
|
98
|
+
best_sharpe: null, best_pnl: null, worst_dd: null,
|
|
99
|
+
avg_sharpe: 0, avg_win_rate: 0, avg_pnl: 0,
|
|
100
|
+
trend: "no data", versions_tested: 0,
|
|
101
|
+
summary: `No performance history for "${name}".`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const backtests = history.filter(e => e.type === "backtest");
|
|
106
|
+
const runs = history.filter(e => e.type === "run");
|
|
107
|
+
const uniqueHashes = new Set(history.map(e => e.code_hash));
|
|
108
|
+
|
|
109
|
+
const sharpes = history.map(e => e.metrics.sharpe).filter(v => isFinite(v));
|
|
110
|
+
const winRates = history.map(e => e.metrics.win_rate).filter(v => isFinite(v));
|
|
111
|
+
const pnls = history.map(e => e.metrics.pnl).filter(v => isFinite(v));
|
|
112
|
+
|
|
113
|
+
const avg = (arr: number[]) => arr.length > 0 ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
|
|
114
|
+
|
|
115
|
+
// Trend: compare recent 3 vs older entries
|
|
116
|
+
let trend = "stable";
|
|
117
|
+
if (sharpes.length >= 6) {
|
|
118
|
+
const recent = avg(sharpes.slice(0, 3));
|
|
119
|
+
const older = avg(sharpes.slice(3, 6));
|
|
120
|
+
if (recent > older * 1.15) trend = "improving";
|
|
121
|
+
else if (recent < older * 0.85) trend = "degrading";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const bestSharpe = getBestRun(name, "sharpe");
|
|
125
|
+
const bestPnl = getBestRun(name, "pnl");
|
|
126
|
+
const worstDd = history.reduce((worst, e) => {
|
|
127
|
+
return (e.metrics.max_dd > (worst?.metrics.max_dd ?? 0)) ? e : worst;
|
|
128
|
+
}, history[0]!);
|
|
129
|
+
|
|
130
|
+
const summary = [
|
|
131
|
+
`${history.length} runs (${backtests.length} backtests, ${runs.length} live) across ${uniqueHashes.size} code versions.`,
|
|
132
|
+
`Avg Sharpe: ${avg(sharpes).toFixed(2)}, Avg Win Rate: ${(avg(winRates) * 100).toFixed(1)}%, Avg P&L: $${avg(pnls).toFixed(2)}.`,
|
|
133
|
+
bestSharpe ? `Best Sharpe: ${bestSharpe.metrics.sharpe.toFixed(2)} (${bestSharpe.code_hash}, ${new Date(bestSharpe.timestamp).toLocaleDateString()})` : "",
|
|
134
|
+
`Trend: ${trend}.`,
|
|
135
|
+
].filter(Boolean).join(" ");
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
total_runs: history.length,
|
|
139
|
+
backtests: backtests.length,
|
|
140
|
+
live_runs: runs.length,
|
|
141
|
+
best_sharpe: bestSharpe,
|
|
142
|
+
best_pnl: bestPnl,
|
|
143
|
+
worst_dd: worstDd ?? null,
|
|
144
|
+
avg_sharpe: avg(sharpes),
|
|
145
|
+
avg_win_rate: avg(winRates),
|
|
146
|
+
avg_pnl: avg(pnls),
|
|
147
|
+
trend,
|
|
148
|
+
versions_tested: uniqueHashes.size,
|
|
149
|
+
summary,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Get performance comparison between code versions */
|
|
154
|
+
export function compareVersions(name: string): { hash: string; runs: number; avg_sharpe: number; avg_pnl: number; best_sharpe: number }[] {
|
|
155
|
+
const history = getHistory(name);
|
|
156
|
+
const byHash = new Map<string, LedgerEntry[]>();
|
|
157
|
+
|
|
158
|
+
for (const entry of history) {
|
|
159
|
+
const existing = byHash.get(entry.code_hash) ?? [];
|
|
160
|
+
existing.push(entry);
|
|
161
|
+
byHash.set(entry.code_hash, existing);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return [...byHash.entries()].map(([hash, entries]) => {
|
|
165
|
+
const sharpes = entries.map(e => e.metrics.sharpe).filter(v => isFinite(v));
|
|
166
|
+
const pnls = entries.map(e => e.metrics.pnl).filter(v => isFinite(v));
|
|
167
|
+
const avg = (arr: number[]) => arr.length > 0 ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
|
|
168
|
+
return {
|
|
169
|
+
hash,
|
|
170
|
+
runs: entries.length,
|
|
171
|
+
avg_sharpe: avg(sharpes),
|
|
172
|
+
avg_pnl: avg(pnls),
|
|
173
|
+
best_sharpe: sharpes.length > 0 ? Math.max(...sharpes) : 0,
|
|
174
|
+
};
|
|
175
|
+
}).sort((a, b) => b.avg_sharpe - a.avg_sharpe);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** List all strategies with ledger entries */
|
|
179
|
+
export function listLedgerStrategies(): string[] {
|
|
180
|
+
ensureDir();
|
|
181
|
+
const { readdirSync } = require("fs");
|
|
182
|
+
return readdirSync(LEDGER_ROOT)
|
|
183
|
+
.filter((f: string) => f.endsWith(".jsonl"))
|
|
184
|
+
.map((f: string) => f.replace(".jsonl", ""));
|
|
185
|
+
}
|