horizon-code 0.5.0 → 0.6.0
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 +6 -0
- package/src/ai/system-prompt.ts +21 -1
- package/src/app.ts +158 -0
- package/src/components/footer.ts +1 -1
- package/src/keys/handler.ts +8 -0
- package/src/platform/profile.ts +202 -0
- 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 +63 -0
- package/src/strategy/replay.ts +191 -0
- package/src/strategy/tools.ts +495 -1
- package/src/strategy/versioning.ts +168 -0
package/package.json
CHANGED
package/src/ai/client.ts
CHANGED
|
@@ -335,6 +335,12 @@ export async function* chat(
|
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
// If structured output was active but we never extracted a "response" field,
|
|
339
|
+
// the model likely responded with plain text — flush jsonBuf as text
|
|
340
|
+
if (structuredActive && jsonBuf && emittedResponseLen === 0) {
|
|
341
|
+
yield { type: "text-delta", textDelta: jsonBuf };
|
|
342
|
+
}
|
|
343
|
+
|
|
338
344
|
// If no tool calls, we're done
|
|
339
345
|
if (!hasToolCalls || pendingToolCalls.length === 0) {
|
|
340
346
|
if (eventCount === 0) {
|
package/src/ai/system-prompt.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Mode } from "../components/mode-bar.ts";
|
|
2
2
|
import { buildGeneratePrompt } from "../strategy/prompts.ts";
|
|
3
|
+
import { store } from "../state/store.ts";
|
|
3
4
|
|
|
4
5
|
const BASE = `You are Horizon, an AI trading research assistant running in a CLI terminal.
|
|
5
6
|
|
|
6
7
|
Rules:
|
|
7
8
|
- Concise and direct. You're a terminal, not a chatbot.
|
|
9
|
+
- NEVER use emojis in your responses. No emoji characters whatsoever. Use plain text only.
|
|
8
10
|
- Tool results render as rich CLI widgets automatically — do NOT reformat the data as tables. Just add 1-2 sentences of insight after the widget.
|
|
9
11
|
- NEVER suggest switching modes.
|
|
10
12
|
- Format: $102,450 not 102450.
|
|
@@ -122,7 +124,25 @@ const VERBOSITY_PREFIX: Record<string, string> = {
|
|
|
122
124
|
|
|
123
125
|
export function getSystemPrompt(mode: Mode, verbosity: string = "normal"): string {
|
|
124
126
|
// Strategy mode has its own verbosity rules — don't override
|
|
125
|
-
if (mode === "strategy")
|
|
127
|
+
if (mode === "strategy") {
|
|
128
|
+
let prompt = buildGeneratePrompt();
|
|
129
|
+
// Inject user profile context if available
|
|
130
|
+
try {
|
|
131
|
+
const { buildProfileContext } = require("../platform/profile.ts");
|
|
132
|
+
const profileCtx = buildProfileContext();
|
|
133
|
+
if (profileCtx) prompt += "\n\n" + profileCtx;
|
|
134
|
+
} catch {}
|
|
135
|
+
// Inject active strategy context so research tools auto-scope
|
|
136
|
+
const draft = store.getActiveSession()?.strategyDraft;
|
|
137
|
+
if (draft?.code) {
|
|
138
|
+
const marketsMatch = draft.code.match(/markets\s*=\s*\[([\s\S]*?)\]/);
|
|
139
|
+
const slugs = marketsMatch ? [...marketsMatch[1]!.matchAll(/["']([^"']+)["']/g)].map(m => m[1]!) : [];
|
|
140
|
+
if (slugs.length > 0) {
|
|
141
|
+
prompt += `\n\n## Active Strategy Context\nThe user is working on "${draft.name}" trading these markets: ${slugs.join(", ")}. When they ask about "the market" or "what's happening", scope to these markets first. Use these slugs for polymarket_data, scan_correlations, etc.`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return prompt;
|
|
145
|
+
}
|
|
126
146
|
const prefix = VERBOSITY_PREFIX[verbosity] ?? "";
|
|
127
147
|
return MODE_PROMPTS[mode] + prefix;
|
|
128
148
|
}
|
package/src/app.ts
CHANGED
|
@@ -76,6 +76,17 @@ const SLASH_COMMANDS: Record<string, { description: string; usage: string }> = {
|
|
|
76
76
|
"/exchanges": { description: "Show exchange connection status", usage: "/exchanges" },
|
|
77
77
|
"/quit": { description: "Exit Horizon", usage: "/quit" },
|
|
78
78
|
"/exit": { description: "Exit Horizon", usage: "/exit" },
|
|
79
|
+
"/bt": { description: "Backtest current strategy", usage: "/bt" },
|
|
80
|
+
"/backtest": { description: "Backtest current strategy", usage: "/backtest" },
|
|
81
|
+
"/run": { description: "Run current strategy", usage: "/run" },
|
|
82
|
+
"/dash": { description: "Open strategy dashboard", usage: "/dash" },
|
|
83
|
+
"/dashboard": { description: "Open strategy dashboard", usage: "/dashboard" },
|
|
84
|
+
"/save": { description: "Save current strategy", usage: "/save [name]" },
|
|
85
|
+
"/health": { description: "Check strategy health score", usage: "/health" },
|
|
86
|
+
"/versions": { description: "List strategy versions", usage: "/versions [name]" },
|
|
87
|
+
"/export": { description: "Export strategy to file", usage: "/export" },
|
|
88
|
+
"/report": { description: "Generate strategy ledger report", usage: "/report [name]" },
|
|
89
|
+
"/session-export": { description: "Export chat session to markdown", usage: "/session-export" },
|
|
79
90
|
};
|
|
80
91
|
|
|
81
92
|
export class App {
|
|
@@ -304,6 +315,18 @@ export class App {
|
|
|
304
315
|
this.keyHandler.onClear(() => {});
|
|
305
316
|
this.keyHandler.onSessions(() => this.togglePanel("sessions"));
|
|
306
317
|
this.keyHandler.onDeployments(() => this.togglePanel("deployments"));
|
|
318
|
+
this.keyHandler.onOpenDashboard(() => {
|
|
319
|
+
if (dashboard.running) {
|
|
320
|
+
Bun.spawn(["open", dashboard.url]);
|
|
321
|
+
} else {
|
|
322
|
+
// Auto-start built-in dashboard if processes running
|
|
323
|
+
const hasLocal = [...runningProcesses.values()].some(m => m.proc.exitCode === null);
|
|
324
|
+
if (hasLocal) {
|
|
325
|
+
const url = dashboard.start("local", 0, true);
|
|
326
|
+
Bun.spawn(["open", url]);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
});
|
|
307
330
|
this.keyHandler.onTabNext(() => { store.nextTab(); this.switchToActiveTab(); });
|
|
308
331
|
this.keyHandler.onTabPrev(() => { store.prevTab(); this.switchToActiveTab(); });
|
|
309
332
|
this.keyHandler.onTabClose(() => { store.closeTab(store.get().activeSessionId); this.switchToActiveTab(); });
|
|
@@ -527,6 +550,49 @@ export class App {
|
|
|
527
550
|
startedAt: localProcessStartedAt,
|
|
528
551
|
});
|
|
529
552
|
this._hasLocalMetrics = true;
|
|
553
|
+
|
|
554
|
+
// Check alerts against current local metrics (fire-and-forget — setInterval is not async)
|
|
555
|
+
((metrics) => {
|
|
556
|
+
(async () => {
|
|
557
|
+
try {
|
|
558
|
+
const { checkAlerts } = await import("./strategy/alerts.ts");
|
|
559
|
+
const draft = store.getActiveSession()?.strategyDraft;
|
|
560
|
+
const alertResults = await checkAlerts(
|
|
561
|
+
draft?.name ?? "local",
|
|
562
|
+
{
|
|
563
|
+
pnl: metrics.pnl,
|
|
564
|
+
max_dd: metrics.max_dd ?? 0,
|
|
565
|
+
exposure: metrics.exposure ?? 0,
|
|
566
|
+
win_rate: metrics.win_rate ?? 0,
|
|
567
|
+
trades: metrics.trades ?? 0,
|
|
568
|
+
},
|
|
569
|
+
() => {
|
|
570
|
+
// Stop all running processes
|
|
571
|
+
for (const [pid, m] of runningProcesses) {
|
|
572
|
+
if (m.proc.exitCode === null) m.cleanup?.();
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
);
|
|
576
|
+
for (const r of alertResults) {
|
|
577
|
+
if (r.triggered && r.actionTaken) {
|
|
578
|
+
this.codePanel.appendLog(r.actionTaken);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} catch {}
|
|
582
|
+
})();
|
|
583
|
+
})(localMetricsData);
|
|
584
|
+
|
|
585
|
+
// Record replay metrics (fire-and-forget)
|
|
586
|
+
((metrics) => {
|
|
587
|
+
(async () => {
|
|
588
|
+
try {
|
|
589
|
+
const { recordMetrics, isRecording } = await import("./strategy/replay.ts");
|
|
590
|
+
for (const [pid] of runningProcesses) {
|
|
591
|
+
if (isRecording(pid)) recordMetrics(pid, metrics as any);
|
|
592
|
+
}
|
|
593
|
+
} catch {}
|
|
594
|
+
})();
|
|
595
|
+
})(localMetricsData);
|
|
530
596
|
} else if (this._hasLocalMetrics && alive === 0) {
|
|
531
597
|
// Local process stopped — clear local metrics, let platform data take over
|
|
532
598
|
this._hasLocalMetrics = false;
|
|
@@ -845,6 +911,81 @@ export class App {
|
|
|
845
911
|
case "/exit":
|
|
846
912
|
this.quit();
|
|
847
913
|
break;
|
|
914
|
+
case "/bt":
|
|
915
|
+
case "/backtest": {
|
|
916
|
+
const tool = (await import("./strategy/tools.ts")).strategyTools.backtest_strategy;
|
|
917
|
+
const result = await tool.execute({});
|
|
918
|
+
this.codePanel.appendLog(typeof result === "object" ? JSON.stringify(result, null, 2) : String(result));
|
|
919
|
+
this.codePanel.setTab("logs");
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
case "/run": {
|
|
923
|
+
const tool = (await import("./strategy/tools.ts")).strategyTools.run_strategy;
|
|
924
|
+
const result = await tool.execute({});
|
|
925
|
+
this.codePanel.appendLog(typeof result === "object" ? JSON.stringify(result, null, 2) : String(result));
|
|
926
|
+
this.codePanel.setTab("logs");
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
case "/dash":
|
|
930
|
+
case "/dashboard": {
|
|
931
|
+
const tool = (await import("./strategy/tools.ts")).strategyTools.spawn_dashboard;
|
|
932
|
+
const result = await tool.execute({ strategy_id: "local" });
|
|
933
|
+
if (result?.url) Bun.spawn(["open", result.url]);
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
case "/save": {
|
|
937
|
+
const tool = (await import("./strategy/tools.ts")).strategyTools.save_strategy;
|
|
938
|
+
const draft = store.getActiveSession()?.strategyDraft;
|
|
939
|
+
const result = await tool.execute({ name: draft?.name ?? arg ?? "untitled" });
|
|
940
|
+
this.codePanel.appendLog(`Saved: ${JSON.stringify(result)}`);
|
|
941
|
+
break;
|
|
942
|
+
}
|
|
943
|
+
case "/health": {
|
|
944
|
+
const { computeHealthScore } = await import("./strategy/health.ts");
|
|
945
|
+
const draft = store.getActiveSession()?.strategyDraft;
|
|
946
|
+
if (!draft?.code) { this.codePanel.appendLog("No strategy loaded"); break; }
|
|
947
|
+
const report = computeHealthScore(draft.code);
|
|
948
|
+
this.codePanel.appendLog(`Health: ${report.score}/${report.maxScore} (${report.grade}) — ${report.summary}`);
|
|
949
|
+
break;
|
|
950
|
+
}
|
|
951
|
+
case "/versions": {
|
|
952
|
+
const { listVersions } = await import("./strategy/versioning.ts");
|
|
953
|
+
const draft = store.getActiveSession()?.strategyDraft;
|
|
954
|
+
const versions = await listVersions(draft?.name ?? arg ?? "");
|
|
955
|
+
this.codePanel.appendLog(versions.length === 0 ? "No versions found" : versions.map(v => `v${v.version} [${v.hash}] ${v.label} — ${v.timestamp}`).join("\n"));
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
958
|
+
case "/export": {
|
|
959
|
+
const { exportStrategy } = await import("./strategy/export.ts");
|
|
960
|
+
const draft = store.getActiveSession()?.strategyDraft;
|
|
961
|
+
if (!draft?.code) { this.codePanel.appendLog("No strategy loaded"); break; }
|
|
962
|
+
const result = await exportStrategy(draft.name, draft.code, draft.params ?? {}, null);
|
|
963
|
+
this.codePanel.appendLog(`Exported to ${result.path} (${result.size} bytes)`);
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
case "/report": {
|
|
967
|
+
const { generateReport } = await import("./strategy/ledger.ts");
|
|
968
|
+
const draft = store.getActiveSession()?.strategyDraft;
|
|
969
|
+
const report = generateReport(draft?.name ?? arg ?? "");
|
|
970
|
+
this.codePanel.appendLog(report.summary);
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
case "/session-export": {
|
|
974
|
+
const session = store.getActiveSession();
|
|
975
|
+
if (!session) break;
|
|
976
|
+
const { writeWorkspaceFile } = await import("./strategy/workspace.ts");
|
|
977
|
+
const lines: string[] = [];
|
|
978
|
+
lines.push(`# ${session.id} — ${new Date().toISOString()}\n`);
|
|
979
|
+
for (const msg of session.messages) {
|
|
980
|
+
const role = msg.role === "user" ? "**User**" : "**Horizon**";
|
|
981
|
+
const text = msg.content.map((b: any) => b.text ?? b.markdown ?? "").join("\n");
|
|
982
|
+
lines.push(`### ${role}\n${text}\n`);
|
|
983
|
+
}
|
|
984
|
+
const fileName = `session_${Date.now()}.md`;
|
|
985
|
+
await writeWorkspaceFile(`exports/${fileName}`, lines.join("\n"));
|
|
986
|
+
this.codePanel.appendLog(`Session exported to exports/${fileName}`);
|
|
987
|
+
break;
|
|
988
|
+
}
|
|
848
989
|
default: {
|
|
849
990
|
// Unknown command — show as system message
|
|
850
991
|
const msg: Message = {
|
|
@@ -1158,6 +1299,23 @@ export class App {
|
|
|
1158
1299
|
contextParts.push(`Active strategy: ${draft.name} (${draft.phase}, ${draft.validationStatus})`);
|
|
1159
1300
|
}
|
|
1160
1301
|
|
|
1302
|
+
// Preserve strategy context through compaction
|
|
1303
|
+
if (draft?.code) {
|
|
1304
|
+
contextParts.push(`\nActive strategy code hash: ${draft.code.length} chars`);
|
|
1305
|
+
// Preserve active market slugs
|
|
1306
|
+
const marketsMatch = draft.code.match(/markets\s*=\s*\[([\s\S]*?)\]/);
|
|
1307
|
+
if (marketsMatch) {
|
|
1308
|
+
const slugs2 = [...marketsMatch[1]!.matchAll(/["']([^"']+)["']/g)].map(m => m[1]!);
|
|
1309
|
+
contextParts.push(`Active markets: ${slugs2.join(", ")}`);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// Preserve running process info
|
|
1314
|
+
const pids = [...runningProcesses.keys()].filter(pid => runningProcesses.get(pid)?.proc.exitCode === null);
|
|
1315
|
+
if (pids.length > 0) {
|
|
1316
|
+
contextParts.push(`Running processes: PIDs ${pids.join(", ")}`);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1161
1319
|
contextParts.push("");
|
|
1162
1320
|
contextParts.push("Conversation summary:");
|
|
1163
1321
|
contextParts.push(...summaryParts.slice(-15)); // last 15 exchanges max
|
package/src/components/footer.ts
CHANGED
|
@@ -33,7 +33,7 @@ export class Footer {
|
|
|
33
33
|
|
|
34
34
|
this.hintsText = new TextRenderable(renderer, {
|
|
35
35
|
id: "footer-hints",
|
|
36
|
-
content: "esc stop ^N new
|
|
36
|
+
content: "esc stop ^N new ^L ^H switch ^W close ^R mode ^E chats ^D bots ^O dash / cmd",
|
|
37
37
|
fg: COLORS.borderDim,
|
|
38
38
|
});
|
|
39
39
|
this.container.add(this.hintsText);
|
package/src/keys/handler.ts
CHANGED
|
@@ -20,6 +20,7 @@ export class KeyHandler {
|
|
|
20
20
|
private tabCloseCallback: (() => void) | null = null;
|
|
21
21
|
private tabNewCallback: (() => void) | null = null;
|
|
22
22
|
private acNavCallback: ((dir: "up" | "down" | "accept" | "dismiss") => void) | null = null;
|
|
23
|
+
private openDashboardCallback: (() => void) | null = null;
|
|
23
24
|
acActive = false; // set by app when autocomplete is visible
|
|
24
25
|
|
|
25
26
|
// Panel navigation (shared by session & strategy panels)
|
|
@@ -56,6 +57,7 @@ export class KeyHandler {
|
|
|
56
57
|
onTabClose(cb: () => void): void { this.tabCloseCallback = cb; }
|
|
57
58
|
onTabNew(cb: () => void): void { this.tabNewCallback = cb; }
|
|
58
59
|
onAcNav(cb: (dir: "up" | "down" | "accept" | "dismiss") => void): void { this.acNavCallback = cb; }
|
|
60
|
+
onOpenDashboard(cb: () => void): void { this.openDashboardCallback = cb; }
|
|
59
61
|
|
|
60
62
|
onPanelNav(cb: (delta: number) => void): void { this.panelNavCallback = cb; }
|
|
61
63
|
onPanelSelect(cb: () => void): void { this.panelSelectCallback = cb; }
|
|
@@ -123,6 +125,12 @@ export class KeyHandler {
|
|
|
123
125
|
return;
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
// Ctrl+O — open dashboard in browser
|
|
129
|
+
if (key.ctrl && key.name === "o") {
|
|
130
|
+
this.openDashboardCallback?.();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
126
134
|
// Ctrl+L — next tab (was clear, now tab navigation)
|
|
127
135
|
if (key.ctrl && key.name === "l") {
|
|
128
136
|
if (this.panelActive === "sessions") { this.panelNewCallback?.(); return; }
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Cumulative user profile — learns preferences over time
|
|
2
|
+
// Storage: ~/.horizon/profile.json
|
|
3
|
+
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { resolve } from "path";
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
7
|
+
|
|
8
|
+
const PROFILE_PATH = resolve(homedir(), ".horizon", "profile.json");
|
|
9
|
+
|
|
10
|
+
export interface UserProfile {
|
|
11
|
+
// Trading preferences
|
|
12
|
+
preferred_markets: string[]; // most-used market slugs
|
|
13
|
+
preferred_exchanges: string[]; // polymarket, kalshi, etc.
|
|
14
|
+
risk_tolerance: "conservative" | "moderate" | "aggressive";
|
|
15
|
+
typical_spread: number; // average spread parameter used
|
|
16
|
+
typical_position_size: number; // average max_position used
|
|
17
|
+
always_paper_first: boolean; // always tests in paper before live
|
|
18
|
+
|
|
19
|
+
// Workflow patterns
|
|
20
|
+
strategies_created: number;
|
|
21
|
+
strategies_deployed: number;
|
|
22
|
+
backtests_run: number;
|
|
23
|
+
total_sessions: number;
|
|
24
|
+
preferred_strategy_types: string[]; // mm, momentum, arb, etc.
|
|
25
|
+
avg_pipeline_depth: number; // average number of pipeline functions
|
|
26
|
+
|
|
27
|
+
// Usage stats
|
|
28
|
+
first_seen: string;
|
|
29
|
+
last_seen: string;
|
|
30
|
+
total_interactions: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_PROFILE: UserProfile = {
|
|
34
|
+
preferred_markets: [],
|
|
35
|
+
preferred_exchanges: [],
|
|
36
|
+
risk_tolerance: "moderate",
|
|
37
|
+
typical_spread: 0.04,
|
|
38
|
+
typical_position_size: 100,
|
|
39
|
+
always_paper_first: true,
|
|
40
|
+
strategies_created: 0,
|
|
41
|
+
strategies_deployed: 0,
|
|
42
|
+
backtests_run: 0,
|
|
43
|
+
total_sessions: 0,
|
|
44
|
+
preferred_strategy_types: [],
|
|
45
|
+
avg_pipeline_depth: 2,
|
|
46
|
+
first_seen: new Date().toISOString(),
|
|
47
|
+
last_seen: new Date().toISOString(),
|
|
48
|
+
total_interactions: 0,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Load profile from disk */
|
|
52
|
+
export function loadProfile(): UserProfile {
|
|
53
|
+
if (!existsSync(PROFILE_PATH)) return { ...DEFAULT_PROFILE };
|
|
54
|
+
try {
|
|
55
|
+
const data = JSON.parse(readFileSync(PROFILE_PATH, "utf-8"));
|
|
56
|
+
return { ...DEFAULT_PROFILE, ...data };
|
|
57
|
+
} catch {
|
|
58
|
+
return { ...DEFAULT_PROFILE };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Save profile to disk */
|
|
63
|
+
export function saveProfile(profile: UserProfile): void {
|
|
64
|
+
profile.last_seen = new Date().toISOString();
|
|
65
|
+
writeFileSync(PROFILE_PATH, JSON.stringify(profile, null, 2));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Update profile based on strategy code analysis */
|
|
69
|
+
export function learnFromStrategy(code: string): void {
|
|
70
|
+
const profile = loadProfile();
|
|
71
|
+
|
|
72
|
+
// Extract market slugs
|
|
73
|
+
const marketsMatch = code.match(/markets\s*=\s*\[([\s\S]*?)\]/);
|
|
74
|
+
if (marketsMatch) {
|
|
75
|
+
const slugs = [...marketsMatch[1]!.matchAll(/["']([^"']+)["']/g)].map(m => m[1]!);
|
|
76
|
+
for (const slug of slugs) {
|
|
77
|
+
if (!profile.preferred_markets.includes(slug)) {
|
|
78
|
+
profile.preferred_markets.push(slug);
|
|
79
|
+
if (profile.preferred_markets.length > 20) profile.preferred_markets.shift();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Extract exchange
|
|
85
|
+
const exchangeMatch = code.match(/exchange\s*=\s*hz\.(\w+)/);
|
|
86
|
+
if (exchangeMatch) {
|
|
87
|
+
const exchange = exchangeMatch[1]!.toLowerCase();
|
|
88
|
+
if (!profile.preferred_exchanges.includes(exchange)) {
|
|
89
|
+
profile.preferred_exchanges.push(exchange);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Extract spread
|
|
94
|
+
const spreadMatch = code.match(/spread\s*[=:]\s*([\d.]+)/);
|
|
95
|
+
if (spreadMatch) {
|
|
96
|
+
const spread = parseFloat(spreadMatch[1]!);
|
|
97
|
+
if (spread > 0 && spread < 1) {
|
|
98
|
+
profile.typical_spread = (profile.typical_spread + spread) / 2;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Extract max_position
|
|
103
|
+
const posMatch = code.match(/max_position\s*=\s*(\d+)/);
|
|
104
|
+
if (posMatch) {
|
|
105
|
+
const pos = parseInt(posMatch[1]!);
|
|
106
|
+
profile.typical_position_size = Math.round((profile.typical_position_size + pos) / 2);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Detect risk tolerance from risk config
|
|
110
|
+
const ddMatch = code.match(/max_drawdown_pct\s*=\s*([\d.]+)/);
|
|
111
|
+
if (ddMatch) {
|
|
112
|
+
const dd = parseFloat(ddMatch[1]!);
|
|
113
|
+
if (dd <= 5) profile.risk_tolerance = "conservative";
|
|
114
|
+
else if (dd <= 15) profile.risk_tolerance = "moderate";
|
|
115
|
+
else profile.risk_tolerance = "aggressive";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Detect strategy type
|
|
119
|
+
const codeLC = code.toLowerCase();
|
|
120
|
+
const types: string[] = [];
|
|
121
|
+
if (/market.?mak|mm_|spread/.test(codeLC)) types.push("market_making");
|
|
122
|
+
if (/momentum|trend|signal/.test(codeLC)) types.push("momentum");
|
|
123
|
+
if (/arb|arbitrage/.test(codeLC)) types.push("arbitrage");
|
|
124
|
+
if (/mean.?rev|revert/.test(codeLC)) types.push("mean_reversion");
|
|
125
|
+
if (/scalp/.test(codeLC)) types.push("scalper");
|
|
126
|
+
|
|
127
|
+
for (const t of types) {
|
|
128
|
+
if (!profile.preferred_strategy_types.includes(t)) {
|
|
129
|
+
profile.preferred_strategy_types.push(t);
|
|
130
|
+
if (profile.preferred_strategy_types.length > 10) profile.preferred_strategy_types.shift();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Pipeline depth
|
|
135
|
+
const pipelineMatch = code.match(/pipeline\s*=\s*\[([\s\S]*?)\]/);
|
|
136
|
+
if (pipelineMatch) {
|
|
137
|
+
const fns = pipelineMatch[1]!.split(",").filter(s => s.trim() && !s.trim().startsWith("#"));
|
|
138
|
+
if (fns.length > 0) {
|
|
139
|
+
profile.avg_pipeline_depth = Math.round((profile.avg_pipeline_depth + fns.length) / 2);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Paper mode preference
|
|
144
|
+
if (/mode\s*=\s*["']paper["']/.test(code)) {
|
|
145
|
+
profile.always_paper_first = true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
profile.strategies_created++;
|
|
149
|
+
profile.total_interactions++;
|
|
150
|
+
|
|
151
|
+
saveProfile(profile);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Record a backtest run */
|
|
155
|
+
export function recordBacktest(): void {
|
|
156
|
+
const profile = loadProfile();
|
|
157
|
+
profile.backtests_run++;
|
|
158
|
+
profile.total_interactions++;
|
|
159
|
+
saveProfile(profile);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Record a deployment */
|
|
163
|
+
export function recordDeploy(): void {
|
|
164
|
+
const profile = loadProfile();
|
|
165
|
+
profile.strategies_deployed++;
|
|
166
|
+
profile.total_interactions++;
|
|
167
|
+
saveProfile(profile);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Record a session start */
|
|
171
|
+
export function recordSession(): void {
|
|
172
|
+
const profile = loadProfile();
|
|
173
|
+
profile.total_sessions++;
|
|
174
|
+
saveProfile(profile);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Build a context string for the LLM system prompt */
|
|
178
|
+
export function buildProfileContext(): string {
|
|
179
|
+
const p = loadProfile();
|
|
180
|
+
if (p.total_interactions === 0) return "";
|
|
181
|
+
|
|
182
|
+
const parts: string[] = [];
|
|
183
|
+
parts.push("## User Profile (learned from usage)");
|
|
184
|
+
|
|
185
|
+
if (p.preferred_markets.length > 0) {
|
|
186
|
+
parts.push(`- Frequently traded markets: ${p.preferred_markets.slice(-5).join(", ")}`);
|
|
187
|
+
}
|
|
188
|
+
if (p.preferred_exchanges.length > 0) {
|
|
189
|
+
parts.push(`- Preferred exchanges: ${p.preferred_exchanges.join(", ")}`);
|
|
190
|
+
}
|
|
191
|
+
if (p.preferred_strategy_types.length > 0) {
|
|
192
|
+
parts.push(`- Strategy style: ${p.preferred_strategy_types.join(", ")}`);
|
|
193
|
+
}
|
|
194
|
+
parts.push(`- Risk tolerance: ${p.risk_tolerance} (typical spread: ${p.typical_spread.toFixed(3)}, typical max_position: ${p.typical_position_size})`);
|
|
195
|
+
parts.push(`- Experience: ${p.strategies_created} strategies created, ${p.backtests_run} backtests, ${p.strategies_deployed} deployments`);
|
|
196
|
+
|
|
197
|
+
if (p.always_paper_first) {
|
|
198
|
+
parts.push("- Always uses paper mode first (respect this preference)");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return parts.join("\n");
|
|
202
|
+
}
|