horizon-code 0.5.1 → 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/system-prompt.ts +20 -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 +62 -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/system-prompt.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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
|
|
|
@@ -123,7 +124,25 @@ const VERBOSITY_PREFIX: Record<string, string> = {
|
|
|
123
124
|
|
|
124
125
|
export function getSystemPrompt(mode: Mode, verbosity: string = "normal"): string {
|
|
125
126
|
// Strategy mode has its own verbosity rules — don't override
|
|
126
|
-
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
|
+
}
|
|
127
146
|
const prefix = VERBOSITY_PREFIX[verbosity] ?? "";
|
|
128
147
|
return MODE_PROMPTS[mode] + prefix;
|
|
129
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
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// Market scanner + correlation analysis
|
|
2
|
+
// Scans for trading opportunities across Polymarket markets
|
|
3
|
+
|
|
4
|
+
import { gammaEvents } from "./apis.ts";
|
|
5
|
+
|
|
6
|
+
export interface MarketOpportunity {
|
|
7
|
+
slug: string;
|
|
8
|
+
title: string;
|
|
9
|
+
type: "wide_spread" | "high_volume" | "mispricing" | "volatile";
|
|
10
|
+
score: number; // 0-100
|
|
11
|
+
spread?: number;
|
|
12
|
+
volume?: number;
|
|
13
|
+
price?: number;
|
|
14
|
+
detail: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CorrelationResult {
|
|
18
|
+
marketA: string;
|
|
19
|
+
marketB: string;
|
|
20
|
+
correlation: number; // -1 to 1
|
|
21
|
+
direction: string; // "positive", "negative", "neutral"
|
|
22
|
+
strength: string; // "strong", "moderate", "weak"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CorrelationMatrix {
|
|
26
|
+
markets: string[];
|
|
27
|
+
matrix: number[][];
|
|
28
|
+
pairs: CorrelationResult[];
|
|
29
|
+
high_correlation_warning: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Scan top Polymarket markets for trading opportunities.
|
|
34
|
+
* Returns ranked list of opportunities for market making, edge trading, or arbitrage.
|
|
35
|
+
*/
|
|
36
|
+
export async function scanOpportunities(limit: number = 30): Promise<{
|
|
37
|
+
opportunities: MarketOpportunity[];
|
|
38
|
+
total_scanned: number;
|
|
39
|
+
summary: string;
|
|
40
|
+
}> {
|
|
41
|
+
// Fetch active events
|
|
42
|
+
const events = await gammaEvents({ query: "", limit: limit * 2 });
|
|
43
|
+
if (!events || events.length === 0) {
|
|
44
|
+
return { opportunities: [], total_scanned: 0, summary: "No markets available." };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const opportunities: MarketOpportunity[] = [];
|
|
48
|
+
|
|
49
|
+
for (const event of events) {
|
|
50
|
+
const markets = event.markets ?? [event];
|
|
51
|
+
|
|
52
|
+
for (const market of markets) {
|
|
53
|
+
const slug = market.slug ?? market.conditionId ?? "";
|
|
54
|
+
const title = market.question ?? market.title ?? slug;
|
|
55
|
+
const spread = market.spread ?? (market.bestAsk && market.bestBid ? market.bestAsk - market.bestBid : null);
|
|
56
|
+
const volume = market.volume ?? market.volumeNum ?? 0;
|
|
57
|
+
const price = market.lastTradePrice ?? market.outcomePrices?.[0] ?? 0.5;
|
|
58
|
+
const priceNum = typeof price === "string" ? parseFloat(price) : price;
|
|
59
|
+
|
|
60
|
+
// Wide spread opportunity (market making)
|
|
61
|
+
if (spread && spread > 0.04) {
|
|
62
|
+
const score = Math.min(100, Math.round(spread * 500));
|
|
63
|
+
opportunities.push({
|
|
64
|
+
slug, title, type: "wide_spread", score, spread, volume: volume,
|
|
65
|
+
detail: `Spread ${(spread * 100).toFixed(1)}c — room for market making`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// High volume but price near 50% (uncertain market = good for MM)
|
|
70
|
+
if (volume > 50000 && priceNum > 0.3 && priceNum < 0.7) {
|
|
71
|
+
const uncertainty = 1 - Math.abs(priceNum - 0.5) * 2;
|
|
72
|
+
const score = Math.min(100, Math.round(uncertainty * 60 + (volume / 100000) * 40));
|
|
73
|
+
opportunities.push({
|
|
74
|
+
slug, title, type: "high_volume", score, volume, price: priceNum,
|
|
75
|
+
detail: `$${(volume / 1000).toFixed(0)}K vol, price ${(priceNum * 100).toFixed(0)}c — high uncertainty`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Extreme prices that might indicate mispricing
|
|
80
|
+
if (priceNum > 0.02 && priceNum < 0.08) {
|
|
81
|
+
opportunities.push({
|
|
82
|
+
slug, title, type: "mispricing", score: 65, price: priceNum,
|
|
83
|
+
detail: `Low price ${(priceNum * 100).toFixed(1)}c — potential long tail value`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (priceNum > 0.92 && priceNum < 0.98) {
|
|
87
|
+
opportunities.push({
|
|
88
|
+
slug, title, type: "mispricing", score: 55, price: priceNum,
|
|
89
|
+
detail: `High price ${(priceNum * 100).toFixed(1)}c — potential short opportunity`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Sort by score descending and deduplicate by slug
|
|
96
|
+
const seen = new Set<string>();
|
|
97
|
+
const unique = opportunities
|
|
98
|
+
.sort((a, b) => b.score - a.score)
|
|
99
|
+
.filter(o => {
|
|
100
|
+
if (seen.has(o.slug)) return false;
|
|
101
|
+
seen.add(o.slug);
|
|
102
|
+
return true;
|
|
103
|
+
})
|
|
104
|
+
.slice(0, limit);
|
|
105
|
+
|
|
106
|
+
const byType = {
|
|
107
|
+
wide_spread: unique.filter(o => o.type === "wide_spread").length,
|
|
108
|
+
high_volume: unique.filter(o => o.type === "high_volume").length,
|
|
109
|
+
mispricing: unique.filter(o => o.type === "mispricing").length,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const summary = `Scanned ${events.length} markets. Found ${unique.length} opportunities: ` +
|
|
113
|
+
`${byType.wide_spread} wide spreads, ${byType.high_volume} high volume, ${byType.mispricing} mispricings.`;
|
|
114
|
+
|
|
115
|
+
return { opportunities: unique, total_scanned: events.length, summary };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Compute pairwise correlations between a set of markets.
|
|
120
|
+
* Uses simple price-based correlation from available data.
|
|
121
|
+
*/
|
|
122
|
+
export function computeCorrelations(
|
|
123
|
+
marketPrices: { slug: string; prices: number[] }[],
|
|
124
|
+
): CorrelationMatrix {
|
|
125
|
+
const n = marketPrices.length;
|
|
126
|
+
const matrix: number[][] = Array.from({ length: n }, () => Array(n).fill(0));
|
|
127
|
+
const pairs: CorrelationResult[] = [];
|
|
128
|
+
|
|
129
|
+
for (let i = 0; i < n; i++) {
|
|
130
|
+
matrix[i]![i] = 1.0;
|
|
131
|
+
for (let j = i + 1; j < n; j++) {
|
|
132
|
+
const corr = pearsonCorrelation(marketPrices[i]!.prices, marketPrices[j]!.prices);
|
|
133
|
+
matrix[i]![j] = corr;
|
|
134
|
+
matrix[j]![i] = corr;
|
|
135
|
+
|
|
136
|
+
const absCorr = Math.abs(corr);
|
|
137
|
+
pairs.push({
|
|
138
|
+
marketA: marketPrices[i]!.slug,
|
|
139
|
+
marketB: marketPrices[j]!.slug,
|
|
140
|
+
correlation: Math.round(corr * 100) / 100,
|
|
141
|
+
direction: corr > 0.1 ? "positive" : corr < -0.1 ? "negative" : "neutral",
|
|
142
|
+
strength: absCorr > 0.7 ? "strong" : absCorr > 0.4 ? "moderate" : "weak",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Find high correlations that indicate concentration risk
|
|
148
|
+
const highCorr = pairs.filter(p => Math.abs(p.correlation) > 0.7);
|
|
149
|
+
const warning = highCorr.length > 0
|
|
150
|
+
? `WARNING: ${highCorr.length} pair(s) with >0.7 correlation — portfolio is concentrated: ${highCorr.map(p => `${p.marketA}/${p.marketB} (${p.correlation})`).join(", ")}`
|
|
151
|
+
: null;
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
markets: marketPrices.map(m => m.slug),
|
|
155
|
+
matrix,
|
|
156
|
+
pairs: pairs.sort((a, b) => Math.abs(b.correlation) - Math.abs(a.correlation)),
|
|
157
|
+
high_correlation_warning: warning,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Pearson correlation between two arrays */
|
|
162
|
+
function pearsonCorrelation(x: number[], y: number[]): number {
|
|
163
|
+
const n = Math.min(x.length, y.length);
|
|
164
|
+
if (n < 3) return 0;
|
|
165
|
+
|
|
166
|
+
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0, sumY2 = 0;
|
|
167
|
+
for (let i = 0; i < n; i++) {
|
|
168
|
+
sumX += x[i]!;
|
|
169
|
+
sumY += y[i]!;
|
|
170
|
+
sumXY += x[i]! * y[i]!;
|
|
171
|
+
sumX2 += x[i]! * x[i]!;
|
|
172
|
+
sumY2 += y[i]! * y[i]!;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const numerator = n * sumXY - sumX * sumY;
|
|
176
|
+
const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
|
|
177
|
+
|
|
178
|
+
if (denominator === 0) return 0;
|
|
179
|
+
return numerator / denominator;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Build ASCII correlation matrix for terminal display */
|
|
183
|
+
export function formatCorrelationMatrix(result: CorrelationMatrix): string {
|
|
184
|
+
const { markets, matrix } = result;
|
|
185
|
+
const n = markets.length;
|
|
186
|
+
const maxSlugLen = Math.min(12, Math.max(...markets.map(m => m.length)));
|
|
187
|
+
|
|
188
|
+
const lines: string[] = [];
|
|
189
|
+
|
|
190
|
+
// Header
|
|
191
|
+
const header = " ".repeat(maxSlugLen + 2) + markets.map(m => m.slice(0, 6).padStart(7)).join("");
|
|
192
|
+
lines.push(header);
|
|
193
|
+
lines.push("-".repeat(header.length));
|
|
194
|
+
|
|
195
|
+
// Rows
|
|
196
|
+
for (let i = 0; i < n; i++) {
|
|
197
|
+
const slug = markets[i]!.slice(0, maxSlugLen).padEnd(maxSlugLen);
|
|
198
|
+
const vals = matrix[i]!.map((v, j) => {
|
|
199
|
+
if (i === j) return " 1.00";
|
|
200
|
+
const str = v.toFixed(2);
|
|
201
|
+
return (v >= 0 ? " " : "") + str;
|
|
202
|
+
}).map(s => s.padStart(7)).join("");
|
|
203
|
+
lines.push(`${slug} ${vals}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (result.high_correlation_warning) {
|
|
207
|
+
lines.push("");
|
|
208
|
+
lines.push(result.high_correlation_warning);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return lines.join("\n");
|
|
212
|
+
}
|