horizon-code 0.1.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.
Files changed (54) hide show
  1. package/assets/python/highlights.scm +137 -0
  2. package/assets/python/tree-sitter-python.wasm +0 -0
  3. package/bin/horizon.js +2 -0
  4. package/package.json +40 -0
  5. package/src/ai/client.ts +369 -0
  6. package/src/ai/system-prompt.ts +86 -0
  7. package/src/app.ts +1454 -0
  8. package/src/chat/messages.ts +48 -0
  9. package/src/chat/renderer.ts +243 -0
  10. package/src/chat/types.ts +18 -0
  11. package/src/components/code-panel.ts +329 -0
  12. package/src/components/footer.ts +72 -0
  13. package/src/components/hooks-panel.ts +224 -0
  14. package/src/components/input-bar.ts +193 -0
  15. package/src/components/mode-bar.ts +245 -0
  16. package/src/components/session-panel.ts +294 -0
  17. package/src/components/settings-panel.ts +372 -0
  18. package/src/components/splash.ts +156 -0
  19. package/src/components/strategy-panel.ts +489 -0
  20. package/src/components/tab-bar.ts +112 -0
  21. package/src/components/tutorial-panel.ts +680 -0
  22. package/src/components/widgets/progress-bar.ts +38 -0
  23. package/src/components/widgets/sparkline.ts +57 -0
  24. package/src/hooks/executor.ts +109 -0
  25. package/src/index.ts +22 -0
  26. package/src/keys/handler.ts +198 -0
  27. package/src/platform/auth.ts +36 -0
  28. package/src/platform/client.ts +159 -0
  29. package/src/platform/config.ts +121 -0
  30. package/src/platform/session-sync.ts +158 -0
  31. package/src/platform/supabase.ts +376 -0
  32. package/src/platform/sync.ts +149 -0
  33. package/src/platform/tiers.ts +103 -0
  34. package/src/platform/tools.ts +163 -0
  35. package/src/platform/types.ts +86 -0
  36. package/src/platform/usage.ts +224 -0
  37. package/src/research/apis.ts +367 -0
  38. package/src/research/tools.ts +205 -0
  39. package/src/research/widgets.ts +523 -0
  40. package/src/state/store.ts +256 -0
  41. package/src/state/types.ts +109 -0
  42. package/src/strategy/ascii-chart.ts +74 -0
  43. package/src/strategy/code-stream.ts +146 -0
  44. package/src/strategy/dashboard.ts +140 -0
  45. package/src/strategy/persistence.ts +82 -0
  46. package/src/strategy/prompts.ts +626 -0
  47. package/src/strategy/sandbox.ts +137 -0
  48. package/src/strategy/tools.ts +764 -0
  49. package/src/strategy/validator.ts +216 -0
  50. package/src/strategy/widgets.ts +270 -0
  51. package/src/syntax/setup.ts +54 -0
  52. package/src/theme/colors.ts +107 -0
  53. package/src/theme/icons.ts +27 -0
  54. package/src/util/hyperlink.ts +21 -0
@@ -0,0 +1,216 @@
1
+ // Strategy code validator — ported from deploy/platform validate-strategy.ts
2
+ // Pure regex/string checks, no external dependencies
3
+
4
+ export interface ValidationError {
5
+ line: number | null;
6
+ message: string;
7
+ }
8
+
9
+ const ALLOWED_IMPORTS = new Set([
10
+ "horizon", "hz", "datetime", "collections", "math",
11
+ "typing", "enum", "statistics", "pydantic", "abc", "json",
12
+ ]);
13
+
14
+ const FORBIDDEN_PATTERNS: { pattern: RegExp; message: string }[] = [
15
+ { pattern: /\bos\.system\b/, message: "os.system() is not allowed" },
16
+ { pattern: /\bos\.popen\b/, message: "os.popen() is not allowed" },
17
+ { pattern: /\bos\.exec\w*\b/, message: "os.exec*() is not allowed" },
18
+ { pattern: /\bos\.spawn\w*\b/, message: "os.spawn*() is not allowed" },
19
+ { pattern: /\bos\.environ\b/, message: "os.environ access is not allowed" },
20
+ { pattern: /\bsubprocess\b/, message: "subprocess module is not allowed" },
21
+ { pattern: /\b__import__\b/, message: "__import__ is not allowed" },
22
+ { pattern: /\beval\s*\(/, message: "eval() is not allowed" },
23
+ { pattern: /\bexec\s*\(/, message: "exec() is not allowed" },
24
+ { pattern: /\bcompile\s*\(/, message: "compile() is not allowed" },
25
+ { pattern: /\bopen\s*\(/, message: "open() is not allowed — use SDK data providers" },
26
+ { pattern: /\bglobals\s*\(/, message: "globals() is not allowed" },
27
+ { pattern: /\blocals\s*\(/, message: "locals() is not allowed" },
28
+ { pattern: /\bvars\s*\(/, message: "vars() is not allowed" },
29
+ { pattern: /\bgetattr\s*\(/, message: "getattr() is not allowed — access attributes directly" },
30
+ { pattern: /\bsetattr\s*\(/, message: "setattr() is not allowed" },
31
+ { pattern: /\bdelattr\s*\(/, message: "delattr() is not allowed" },
32
+ { pattern: /\b__builtins__\b/, message: "__builtins__ access is not allowed" },
33
+ { pattern: /\b__subclasses__\b/, message: "__subclasses__() is not allowed" },
34
+ { pattern: /\b__class__\.__bases__/, message: "__class__.__bases__ is not allowed" },
35
+ { pattern: /\bbreakpoint\s*\(/, message: "breakpoint() is not allowed" },
36
+ { pattern: /\bpickle\b/, message: "pickle module is not allowed" },
37
+ { pattern: /\bshelve\b/, message: "shelve module is not allowed" },
38
+ { pattern: /\bsocket\b/, message: "socket module is not allowed" },
39
+ { pattern: /\bhttp\b/, message: "http module is not allowed — use SDK data providers" },
40
+ { pattern: /\burllib\b/, message: "urllib module is not allowed — use SDK data providers" },
41
+ { pattern: /\brequests\b/, message: "requests module is not allowed — use SDK data providers" },
42
+ { pattern: /\baiohttp\b/, message: "aiohttp module is not allowed — use SDK data providers" },
43
+ { pattern: /\bhttpx\b/, message: "httpx module is not allowed — use SDK data providers" },
44
+ { pattern: /\bctypes\b/, message: "ctypes module is not allowed" },
45
+ { pattern: /\bsys\./, message: "sys module access is not allowed" },
46
+ { pattern: /\bshutil\b/, message: "shutil module is not allowed" },
47
+ { pattern: /\btempfile\b/, message: "tempfile module is not allowed" },
48
+ { pattern: /\bthreading\b/, message: "threading module is not allowed" },
49
+ { pattern: /\bmultiprocessing\b/, message: "multiprocessing module is not allowed" },
50
+ { pattern: /\bimportlib\b/, message: "importlib module is not allowed" },
51
+ { pattern: /\b__loader__\b/, message: "__loader__ access is not allowed" },
52
+ { pattern: /\b__spec__\b/, message: "__spec__ access is not allowed" },
53
+ { pattern: /\b__traceback__\b/, message: "__traceback__ access is not allowed — potential sandbox escape" },
54
+ { pattern: /\btb_frame\b/, message: "tb_frame access is not allowed — potential sandbox escape" },
55
+ { pattern: /\bf_globals\b/, message: "f_globals access is not allowed — potential sandbox escape" },
56
+ { pattern: /\bf_locals\b/, message: "f_locals access is not allowed — potential sandbox escape" },
57
+ { pattern: /\bf_builtins\b/, message: "f_builtins access is not allowed — potential sandbox escape" },
58
+ { pattern: /\bf_code\b/, message: "f_code access is not allowed — potential sandbox escape" },
59
+ { pattern: /\bf_back\b/, message: "f_back access is not allowed — potential sandbox escape" },
60
+ { pattern: /\bgi_frame\b/, message: "gi_frame access is not allowed — potential sandbox escape" },
61
+ { pattern: /\bgi_code\b/, message: "gi_code access is not allowed — potential sandbox escape" },
62
+ { pattern: /\bcr_frame\b/, message: "cr_frame access is not allowed — potential sandbox escape" },
63
+ { pattern: /\bcr_code\b/, message: "cr_code access is not allowed — potential sandbox escape" },
64
+ { pattern: /\bco_consts\b/, message: "co_consts access is not allowed — code object manipulation" },
65
+ { pattern: /\bco_code\b/, message: "co_code access is not allowed — code object manipulation" },
66
+ { pattern: /\bco_names\b/, message: "co_names access is not allowed — code object manipulation" },
67
+ { pattern: /\b__class__\b/, message: "__class__ access is not allowed" },
68
+ { pattern: /\b__globals__\b/, message: "__globals__ access is not allowed" },
69
+ { pattern: /\b__dict__\b/, message: "__dict__ access is not allowed" },
70
+ { pattern: /\b__mro__\b/, message: "__mro__ access is not allowed" },
71
+ { pattern: /\b__bases__\b/, message: "__bases__ access is not allowed" },
72
+ { pattern: /\b__init_subclass__\b/, message: "__init_subclass__ is not allowed" },
73
+ { pattern: /\b__reduce__\b/, message: "__reduce__ is not allowed — prevents pickle exploits" },
74
+ { pattern: /\b__reduce_ex__\b/, message: "__reduce_ex__ is not allowed" },
75
+ { pattern: /\b__code__\b/, message: "__code__ access is not allowed" },
76
+ { pattern: /\b__closure__\b/, message: "__closure__ access is not allowed" },
77
+ ];
78
+
79
+ /**
80
+ * Auto-fix common AI generation mistakes before validation.
81
+ */
82
+ export function autoFixStrategyCode(code: string): string {
83
+ const lines = code.split("\n");
84
+ const fixed = lines.map((line) => {
85
+ const trimmed = line.trim();
86
+ if (trimmed.startsWith("#")) return line;
87
+
88
+ // Fix hz.Quote(bid=X, ask=Y, size=Z) → hz.quotes(fair=(X+Y)/2, spread=Y-X, size=Z)
89
+ line = line.replace(
90
+ /\bhz\.Quote\s*\(\s*bid\s*=\s*([^,]+),\s*ask\s*=\s*([^,]+),\s*size\s*=\s*([^)]+)\)/g,
91
+ (_match, bid, ask, size) => {
92
+ const b = bid.trim();
93
+ const a = ask.trim();
94
+ const s = size.trim();
95
+ const bNum = parseFloat(b);
96
+ const aNum = parseFloat(a);
97
+ if (!isNaN(bNum) && !isNaN(aNum)) {
98
+ const fair = ((bNum + aNum) / 2).toFixed(4).replace(/0+$/, "").replace(/\.$/, ".0");
99
+ const spread = (aNum - bNum).toFixed(4).replace(/0+$/, "").replace(/\.$/, ".0");
100
+ return `hz.quotes(fair=${fair}, spread=${spread}, size=${s})`;
101
+ }
102
+ return `hz.quotes(fair=(${b} + ${a}) / 2, spread=${a} - ${b}, size=${s})`;
103
+ },
104
+ );
105
+
106
+ // Catch remaining hz.Quote( calls
107
+ line = line.replace(/\bhz\.Quote\s*\(/g, "hz.quotes(");
108
+
109
+ // Fix import decimal / from decimal import Decimal → comment out
110
+ if (/^\s*import\s+decimal\s*$/.test(line) || /^\s*from\s+decimal\s+import\s+/.test(line)) {
111
+ return "# " + trimmed + " # REMOVED: SDK uses plain float, not Decimal";
112
+ }
113
+
114
+ // Fix Decimal(...) → float(...)
115
+ line = line.replace(/\bDecimal\s*\(/g, "float(");
116
+
117
+ return line;
118
+ });
119
+ return fixed.join("\n");
120
+ }
121
+
122
+ /**
123
+ * Validate strategy Python code against the SDK sandbox rules.
124
+ */
125
+ export function validateStrategyCode(code: string): ValidationError[] {
126
+ const errors: ValidationError[] = [];
127
+
128
+ if (!code.trim()) {
129
+ errors.push({ line: null, message: "Code is empty" });
130
+ return errors;
131
+ }
132
+
133
+ // Block backslash line continuations
134
+ if (/\\\s*\n/.test(code)) {
135
+ errors.push({ line: null, message: "Backslash line continuations (\\) are not allowed. Use parentheses for multi-line expressions instead." });
136
+ return errors;
137
+ }
138
+
139
+ // Require at least one pipeline function (def ...(ctx...))
140
+ if (!/def\s+\w+\s*\(\s*ctx/.test(code)) {
141
+ errors.push({ line: null, message: "Missing pipeline function — at least one function accepting `ctx` is required" });
142
+ }
143
+
144
+ // Require SDK usage
145
+ if (!/\bhz\.(quotes|run)\b|\bhorizon\b/.test(code)) {
146
+ errors.push({ line: null, message: "Missing SDK usage — code must use hz.quotes(), hz.run(), or import horizon" });
147
+ }
148
+
149
+ const lines = code.split("\n");
150
+ for (let i = 0; i < lines.length; i++) {
151
+ const line = lines[i]!;
152
+ const trimmed = line.trim();
153
+
154
+ if (trimmed.startsWith("#") || !trimmed) continue;
155
+
156
+ // Validate imports
157
+ const importMatch = trimmed.match(/^import\s+([\w.]+)/);
158
+ if (importMatch) {
159
+ const mod = importMatch[1]!;
160
+ if (!ALLOWED_IMPORTS.has(mod)) {
161
+ errors.push({ line: i + 1, message: `Import "${mod}" is not allowed. Allowed: horizon, hz, datetime, collections, math, typing, enum, statistics, pydantic, abc` });
162
+ }
163
+ continue;
164
+ }
165
+
166
+ const fromImportMatch = trimmed.match(/^from\s+([\w.]+)\s+import\b/);
167
+ if (fromImportMatch) {
168
+ const mod = fromImportMatch[1]!;
169
+ if (!ALLOWED_IMPORTS.has(mod)) {
170
+ errors.push({ line: i + 1, message: `Import from "${mod}" is not allowed. Use only allowed modules (horizon, hz, datetime, collections, etc.)` });
171
+ }
172
+ continue;
173
+ }
174
+
175
+ // Check forbidden patterns
176
+ for (const { pattern, message } of FORBIDDEN_PATTERNS) {
177
+ if (pattern.test(trimmed)) {
178
+ errors.push({ line: i + 1, message });
179
+ }
180
+ }
181
+
182
+ // SDK misuse: inventory is InventorySnapshot, not a dict
183
+ if (/\.inventory\.(items|keys|values|get)\s*\(/.test(trimmed) || /for\s+.+\s+in\s+.*\.inventory\s*:/.test(trimmed)) {
184
+ errors.push({ line: i + 1, message: "ctx.inventory is an InventorySnapshot, not a dict. Use .positions for the list, .net for total exposure, or .net_for_market(id) for per-market exposure." });
185
+ }
186
+
187
+ // SDK misuse: Market has no pricing fields
188
+ if (/\.market\.(mid_price|best_bid|best_ask|spread)\b/.test(trimmed)) {
189
+ errors.push({ line: i + 1, message: "ctx.market has no pricing fields (mid_price, best_bid, best_ask, spread). Use ctx.feeds.get(\"mid\").price for pricing data." });
190
+ }
191
+
192
+ // SDK misuse: hz.Quote() constructor
193
+ if (/hz\.Quote\s*\(/.test(trimmed)) {
194
+ errors.push({ line: i + 1, message: "hz.Quote() is not constructible directly. Use hz.quotes(fair, spread=..., size=...) to generate quotes." });
195
+ }
196
+
197
+ // SDK misuse: Decimal passed to SDK functions
198
+ if (/hz\.(quotes|Quote|Risk)\s*\(/.test(trimmed) && /Decimal/.test(trimmed)) {
199
+ errors.push({ line: i + 1, message: "Do not pass Decimal to SDK functions. hz.quotes(), hz.Quote, and hz.Risk expect plain float values." });
200
+ }
201
+ }
202
+
203
+ // exchange and exchanges mutually exclusive in hz.run()
204
+ const usesExchange = /\bhz\.run\b[\s\S]*?\bexchange\s*=/.test(code);
205
+ const usesExchanges = /\bhz\.run\b[\s\S]*?\bexchanges\s*=/.test(code);
206
+ if (usesExchange && usesExchanges) {
207
+ errors.push({ line: null, message: "hz.run() cannot use both exchange= and exchanges= — they are mutually exclusive." });
208
+ }
209
+
210
+ // Block Decimal import entirely
211
+ if (/from decimal import Decimal|import decimal/.test(code)) {
212
+ errors.push({ line: null, message: "Decimal is not allowed. The Horizon SDK uses plain float everywhere. Remove all Decimal usage." });
213
+ }
214
+
215
+ return errors;
216
+ }
@@ -0,0 +1,270 @@
1
+ // CLI widgets for strategy tool results
2
+
3
+ import { BoxRenderable, TextRenderable, type CliRenderer } from "@opentui/core";
4
+ import { COLORS } from "../theme/colors.ts";
5
+ import { renderAsciiChart } from "./ascii-chart.ts";
6
+
7
+ let widgetCounter = 0;
8
+ function uid(): string { return `sw-${Date.now()}-${widgetCounter++}`; }
9
+
10
+ function addRow(parent: BoxRenderable, renderer: CliRenderer, label: string, value: string, valueColor?: string): void {
11
+ const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
12
+ row.add(new TextRenderable(renderer, { id: uid(), content: label.padEnd(18), fg: COLORS.textMuted }));
13
+ row.add(new TextRenderable(renderer, { id: uid(), content: value, fg: valueColor ?? COLORS.text }));
14
+ parent.add(row);
15
+ }
16
+
17
+ function addSeparator(parent: BoxRenderable, renderer: CliRenderer, width = 40): void {
18
+ parent.add(new TextRenderable(renderer, { id: uid(), content: "─".repeat(width), fg: COLORS.borderDim }));
19
+ }
20
+
21
+ // ── Strategy Proposal ──
22
+
23
+ export function renderStrategyProposal(data: any, renderer: CliRenderer): BoxRenderable {
24
+ const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
25
+
26
+ // Header
27
+ box.add(new TextRenderable(renderer, {
28
+ id: uid(), content: `${data.strategy_name ?? "Strategy"}`, fg: COLORS.accent, attributes: 1,
29
+ }));
30
+
31
+ // Explanation
32
+ if (data.explanation) {
33
+ box.add(new TextRenderable(renderer, { id: uid(), content: data.explanation, fg: COLORS.text }));
34
+ }
35
+
36
+ addSeparator(box, renderer);
37
+
38
+ // Strategy breakdown
39
+ const breakdown = data.strategy_breakdown;
40
+ if (breakdown) {
41
+ addRow(box, renderer, "Class", breakdown.strategy_class ?? "—");
42
+ if (breakdown.how_it_makes_money) {
43
+ addRow(box, renderer, "Profit Mechanism", breakdown.how_it_makes_money);
44
+ }
45
+ if (breakdown.entry_rules?.length) {
46
+ box.add(new TextRenderable(renderer, { id: uid(), content: " Entry Rules:", fg: COLORS.textMuted }));
47
+ for (const rule of breakdown.entry_rules.slice(0, 3)) {
48
+ box.add(new TextRenderable(renderer, { id: uid(), content: ` - ${rule}`, fg: COLORS.text }));
49
+ }
50
+ }
51
+ if (breakdown.exit_rules?.length) {
52
+ box.add(new TextRenderable(renderer, { id: uid(), content: " Exit Rules:", fg: COLORS.textMuted }));
53
+ for (const rule of breakdown.exit_rules.slice(0, 3)) {
54
+ box.add(new TextRenderable(renderer, { id: uid(), content: ` - ${rule}`, fg: COLORS.text }));
55
+ }
56
+ }
57
+ addSeparator(box, renderer);
58
+ }
59
+
60
+ // Estimated metrics
61
+ const metrics = data.estimated_metrics;
62
+ if (metrics) {
63
+ box.add(new TextRenderable(renderer, { id: uid(), content: " Estimated Performance", fg: COLORS.textMuted }));
64
+ if (metrics.expected_monthly_return_range) {
65
+ const [lo, hi] = metrics.expected_monthly_return_range;
66
+ const color = lo >= 0 ? COLORS.success : COLORS.warning;
67
+ addRow(box, renderer, "Monthly Return", `${lo}% to ${hi}%`, color);
68
+ }
69
+ if (metrics.expected_win_rate_range) {
70
+ const [lo, hi] = metrics.expected_win_rate_range;
71
+ addRow(box, renderer, "Win Rate", `${lo}% – ${hi}%`);
72
+ }
73
+ if (metrics.expected_max_drawdown_range) {
74
+ const [lo, hi] = metrics.expected_max_drawdown_range;
75
+ addRow(box, renderer, "Max Drawdown", `${lo}% – ${hi}%`, COLORS.error);
76
+ }
77
+ if (metrics.estimated_trades_per_day) {
78
+ addRow(box, renderer, "Trades/Day", `~${metrics.estimated_trades_per_day}`);
79
+ }
80
+ if (metrics.risk_level) {
81
+ const riskColor = metrics.risk_level === "high" ? COLORS.error : metrics.risk_level === "low" ? COLORS.success : COLORS.warning;
82
+ addRow(box, renderer, "Risk Level", metrics.risk_level.toUpperCase(), riskColor);
83
+ }
84
+ addSeparator(box, renderer);
85
+ }
86
+
87
+ // Risk config
88
+ const risk = data.risk_config;
89
+ if (risk) {
90
+ box.add(new TextRenderable(renderer, { id: uid(), content: " Risk Config", fg: COLORS.textMuted }));
91
+ if (risk.max_position) addRow(box, renderer, "Max Position", `${risk.max_position}`);
92
+ if (risk.max_notional) addRow(box, renderer, "Max Notional", `$${risk.max_notional}`);
93
+ if (risk.max_drawdown_pct) addRow(box, renderer, "Max Drawdown", `${risk.max_drawdown_pct}%`);
94
+ }
95
+
96
+ // Target market
97
+ if (data.target_market?.query_hint) {
98
+ addSeparator(box, renderer);
99
+ addRow(box, renderer, "Target Market", data.target_market.query_hint);
100
+ if (data.target_market.reasoning) {
101
+ box.add(new TextRenderable(renderer, { id: uid(), content: ` ${data.target_market.reasoning}`, fg: COLORS.textMuted }));
102
+ }
103
+ }
104
+
105
+ return box;
106
+ }
107
+
108
+ // ── Validation Result ──
109
+
110
+ export function renderValidationResult(data: any, renderer: CliRenderer): BoxRenderable {
111
+ const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
112
+ const errors = data.errors ?? [];
113
+ const valid = errors.length === 0;
114
+
115
+ if (valid) {
116
+ box.add(new TextRenderable(renderer, {
117
+ id: uid(), content: "Validation passed -- code is valid", fg: COLORS.success,
118
+ }));
119
+ } else {
120
+ box.add(new TextRenderable(renderer, {
121
+ id: uid(), content: `${errors.length} validation error${errors.length > 1 ? "s" : ""}`, fg: COLORS.error, attributes: 1,
122
+ }));
123
+ for (const err of errors.slice(0, 10)) {
124
+ const lineStr = err.line ? `L${err.line}: ` : "";
125
+ box.add(new TextRenderable(renderer, {
126
+ id: uid(), content: ` ${lineStr}${err.message}`, fg: COLORS.error,
127
+ }));
128
+ }
129
+ }
130
+
131
+ return box;
132
+ }
133
+
134
+ // ── Backtest Result ──
135
+
136
+ export function renderBacktestResult(data: any, renderer: CliRenderer): BoxRenderable {
137
+ const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
138
+
139
+ // Error state
140
+ if (data.error) {
141
+ box.add(new TextRenderable(renderer, {
142
+ id: uid(), content: `Backtest Error`, fg: COLORS.error, attributes: 1,
143
+ }));
144
+ box.add(new TextRenderable(renderer, {
145
+ id: uid(), content: data.error, fg: COLORS.error,
146
+ }));
147
+ if (data.hint) {
148
+ box.add(new TextRenderable(renderer, {
149
+ id: uid(), content: data.hint, fg: COLORS.textMuted,
150
+ }));
151
+ }
152
+ return box;
153
+ }
154
+
155
+ box.add(new TextRenderable(renderer, {
156
+ id: uid(), content: `Backtest -- ${data.strategy_name ?? "Strategy"}`, fg: COLORS.accent, attributes: 1,
157
+ }));
158
+
159
+ // ASCII dashboard from hz.dashboard() — the real SDK output
160
+ if (data.ascii_dashboard) {
161
+ for (const line of data.ascii_dashboard.split("\n")) {
162
+ box.add(new TextRenderable(renderer, { id: uid(), content: line, fg: COLORS.text }));
163
+ }
164
+ addSeparator(box, renderer);
165
+ }
166
+
167
+ // Equity curve fallback (when SDK dashboard not available)
168
+ const curve = data.equity_curve ?? data.equityCurve ?? [];
169
+ if (!data.ascii_dashboard && curve.length > 2) {
170
+ const chartLines = renderAsciiChart(curve, 44, 6);
171
+ const finalVal = curve[curve.length - 1] ?? 0;
172
+ const startVal = curve[0] ?? 0;
173
+ const chartColor = finalVal >= startVal ? COLORS.success : COLORS.error;
174
+
175
+ for (const line of chartLines) {
176
+ box.add(new TextRenderable(renderer, { id: uid(), content: line, fg: chartColor }));
177
+ }
178
+ addSeparator(box, renderer);
179
+ }
180
+
181
+ // Metrics grid (from hz.backtest().metrics)
182
+ const m = data.metrics ?? data;
183
+ const pnlColor = (m.total_return ?? 0) >= 0 ? COLORS.success : COLORS.error;
184
+
185
+ if (m.total_return !== undefined) addRow(box, renderer, "Total Return", `${(m.total_return * 100).toFixed(1)}%`, pnlColor);
186
+ if (m.max_drawdown !== undefined) addRow(box, renderer, "Max Drawdown", `${(m.max_drawdown * 100).toFixed(1)}%`, COLORS.error);
187
+ if (m.sharpe_ratio !== undefined) addRow(box, renderer, "Sharpe Ratio", m.sharpe_ratio.toFixed(2), m.sharpe_ratio >= 1 ? COLORS.success : COLORS.warning);
188
+ if (m.sortino_ratio !== undefined) addRow(box, renderer, "Sortino Ratio", m.sortino_ratio.toFixed(2));
189
+ if (m.win_rate !== undefined) addRow(box, renderer, "Win Rate", `${(m.win_rate * 100).toFixed(1)}%`);
190
+ if (m.total_trades !== undefined) addRow(box, renderer, "Total Trades", `${m.total_trades}`);
191
+ if (m.profit_factor !== undefined) addRow(box, renderer, "Profit Factor", m.profit_factor.toFixed(2), m.profit_factor >= 1.5 ? COLORS.success : COLORS.warning);
192
+ if (m.expectancy !== undefined) addRow(box, renderer, "Expectancy", `$${m.expectancy.toFixed(4)}`);
193
+ if (m.total_fees !== undefined) addRow(box, renderer, "Total Fees", `$${m.total_fees.toFixed(4)}`);
194
+
195
+ // Per-market P&L
196
+ if (data.pnl_by_market && Object.keys(data.pnl_by_market).length > 0) {
197
+ addSeparator(box, renderer);
198
+ box.add(new TextRenderable(renderer, { id: uid(), content: " P&L by Market", fg: COLORS.textMuted }));
199
+ for (const [market, pnl] of Object.entries(data.pnl_by_market)) {
200
+ const val = pnl as number;
201
+ addRow(box, renderer, ` ${market}`, `$${val >= 0 ? "+" : ""}${val.toFixed(2)}`, val >= 0 ? COLORS.success : COLORS.error);
202
+ }
203
+ }
204
+
205
+ // SDK summary text (from result.summary())
206
+ if (data.summary) {
207
+ addSeparator(box, renderer);
208
+ for (const line of (data.summary as string).split("\n").slice(0, 15)) {
209
+ box.add(new TextRenderable(renderer, { id: uid(), content: line, fg: COLORS.textMuted }));
210
+ }
211
+ }
212
+
213
+ // Duration / trade count
214
+ if (data.duration || data.trade_count !== undefined) {
215
+ addSeparator(box, renderer);
216
+ if (data.duration) addRow(box, renderer, "Period", data.duration);
217
+ if (data.trade_count !== undefined) addRow(box, renderer, "Fills", `${data.trade_count}`);
218
+ }
219
+
220
+ return box;
221
+ }
222
+
223
+ // ── Market Resolution (resolve_markets result) ──
224
+
225
+ export function renderMarketResolution(data: any, renderer: CliRenderer): BoxRenderable {
226
+ const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
227
+ const markets = data.markets ?? [];
228
+
229
+ if (markets.length === 0) {
230
+ box.add(new TextRenderable(renderer, { id: uid(), content: "No matching markets found", fg: COLORS.textMuted }));
231
+ return box;
232
+ }
233
+
234
+ box.add(new TextRenderable(renderer, {
235
+ id: uid(), content: `Found ${markets.length} market${markets.length > 1 ? "s" : ""}`, fg: COLORS.textMuted,
236
+ }));
237
+
238
+ for (let i = 0; i < Math.min(markets.length, 5); i++) {
239
+ const m = markets[i];
240
+ const price = m.yesPrice ? `${(parseFloat(m.yesPrice) * 100).toFixed(0)}¢` : "—";
241
+ const title = (m.title ?? "").length > 50 ? m.title.slice(0, 50) + "…" : m.title;
242
+ const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
243
+ row.add(new TextRenderable(renderer, { id: uid(), content: `${i + 1}. `, fg: COLORS.textMuted }));
244
+ row.add(new TextRenderable(renderer, { id: uid(), content: title, fg: COLORS.text }));
245
+ row.add(new TextRenderable(renderer, { id: uid(), content: ` ${price}`, fg: COLORS.success }));
246
+ box.add(row);
247
+ if (m.slug) {
248
+ box.add(new TextRenderable(renderer, { id: uid(), content: ` ${m.slug}`, fg: COLORS.textMuted }));
249
+ }
250
+ }
251
+
252
+ return box;
253
+ }
254
+
255
+ // ── Public dispatcher ──
256
+
257
+ export function renderStrategyWidget(
258
+ toolName: string,
259
+ data: any,
260
+ renderer: CliRenderer,
261
+ ): BoxRenderable | null {
262
+ switch (toolName) {
263
+ case "edit_strategy": return renderStrategyProposal(data, renderer);
264
+ case "load_saved_strategy": return renderStrategyProposal(data, renderer);
265
+ case "validate_strategy": return renderValidationResult(data, renderer);
266
+ case "backtest_strategy": return renderBacktestResult(data, renderer);
267
+ case "polymarket_data": return renderMarketResolution(data, renderer);
268
+ default: return null;
269
+ }
270
+ }
@@ -0,0 +1,54 @@
1
+ // Python syntax highlighting setup for the code panel
2
+ // Loads tree-sitter-python WASM and registers it for markdown code blocks
3
+
4
+ import { resolve } from "path";
5
+
6
+ const assetsDir = resolve(import.meta.dir, "../../assets/python");
7
+ const wasmPath = resolve(assetsDir, "tree-sitter-python.wasm");
8
+ const highlightsPath = resolve(assetsDir, "highlights.scm");
9
+
10
+ let client: any = null;
11
+
12
+ /**
13
+ * Get or create a shared TreeSitterClient with Python support.
14
+ * Returns null if tree-sitter is not available.
15
+ */
16
+ export async function getTreeSitterClient(): Promise<any> {
17
+ if (client) return client;
18
+
19
+ try {
20
+ const { TreeSitterClient, addDefaultParsers } = await import("@opentui/core");
21
+
22
+ // Register Python parser globally
23
+ addDefaultParsers([{
24
+ filetype: "python",
25
+ wasm: wasmPath,
26
+ queries: { highlights: [highlightsPath] },
27
+ }]);
28
+
29
+ const dataPath = resolve(import.meta.dir, "../../node_modules/@opentui/core/assets");
30
+ client = new TreeSitterClient({ dataPath });
31
+
32
+ // Also register directly on the client instance
33
+ if (client.addFiletypeParser) {
34
+ client.addFiletypeParser({
35
+ filetype: "python",
36
+ wasm: wasmPath,
37
+ queries: { highlights: [highlightsPath] },
38
+ });
39
+ }
40
+
41
+ await client.initialize();
42
+ return client;
43
+ } catch (e) {
44
+ // Tree-sitter init failed — code panel will work without highlighting
45
+ return null;
46
+ }
47
+ }
48
+
49
+ export function destroyTreeSitterClient(): void {
50
+ if (client) {
51
+ try { client.destroy(); } catch {}
52
+ client = null;
53
+ }
54
+ }
@@ -0,0 +1,107 @@
1
+ // Theme system — mutable color object, setTheme overwrites in place
2
+
3
+ export type ThemeName =
4
+ | "dark" | "midnight" | "nord" | "solarized" // Free
5
+ | "light" // Free
6
+ | "poly" | "poly-dark" // Ultra
7
+ | "limitless" // Ultra
8
+ | "apple-1984"; // Ultra
9
+
10
+ export const ULTRA_THEMES = new Set<ThemeName>(["poly", "poly-dark", "limitless", "apple-1984"]);
11
+
12
+ interface Colors {
13
+ bg: string; bgSecondary: string; bgDarker: string; selection: string;
14
+ text: string; textMuted: string; textEmphasized: string;
15
+ border: string; borderDim: string; borderFocus: string;
16
+ primary: string; primaryDim: string; secondary: string; accent: string;
17
+ success: string; error: string; warning: string; info: string;
18
+ }
19
+
20
+ const THEMES: Record<ThemeName, Colors> = {
21
+ dark: {
22
+ bg: "#212121", bgSecondary: "#252525", bgDarker: "#1a1a1a", selection: "#303030",
23
+ text: "#e0e0e0", textMuted: "#6a6a6a", textEmphasized: "#ffffff",
24
+ border: "#4b4c5c", borderDim: "#333333", borderFocus: "#fab283",
25
+ primary: "#fab283", primaryDim: "#c48a62", secondary: "#5c9cf5", accent: "#9d7cd8",
26
+ success: "#7fd88f", error: "#e06c75", warning: "#f5a742", info: "#56b6c2",
27
+ },
28
+ midnight: {
29
+ bg: "#0d1117", bgSecondary: "#161b22", bgDarker: "#080c12", selection: "#1c2333",
30
+ text: "#c9d1d9", textMuted: "#484f58", textEmphasized: "#f0f6fc",
31
+ border: "#30363d", borderDim: "#21262d", borderFocus: "#8be9fd",
32
+ primary: "#ff79c6", primaryDim: "#cc5f9e", secondary: "#8be9fd", accent: "#bd93f9",
33
+ success: "#50fa7b", error: "#ff5555", warning: "#f1fa8c", info: "#8be9fd",
34
+ },
35
+ nord: {
36
+ bg: "#2e3440", bgSecondary: "#3b4252", bgDarker: "#272c36", selection: "#434c5e",
37
+ text: "#d8dee9", textMuted: "#616e88", textEmphasized: "#eceff4",
38
+ border: "#4c566a", borderDim: "#3b4252", borderFocus: "#81a1c1",
39
+ primary: "#d08770", primaryDim: "#a5654f", secondary: "#81a1c1", accent: "#b48ead",
40
+ success: "#a3be8c", error: "#bf616a", warning: "#ebcb8b", info: "#88c0d0",
41
+ },
42
+ solarized: {
43
+ bg: "#002b36", bgSecondary: "#073642", bgDarker: "#001f27", selection: "#0a4050",
44
+ text: "#839496", textMuted: "#586e75", textEmphasized: "#fdf6e3",
45
+ border: "#2aa198", borderDim: "#073642", borderFocus: "#268bd2",
46
+ primary: "#cb4b16", primaryDim: "#a33a0e", secondary: "#268bd2", accent: "#6c71c4",
47
+ success: "#859900", error: "#dc322f", warning: "#b58900", info: "#2aa198",
48
+ },
49
+
50
+ // ── Light ──
51
+ light: {
52
+ bg: "#ffffff", bgSecondary: "#f5f5f5", bgDarker: "#e8e8e8", selection: "#d0d7de",
53
+ text: "#24292f", textMuted: "#656d76", textEmphasized: "#000000",
54
+ border: "#d0d7de", borderDim: "#e1e4e8", borderFocus: "#0969da",
55
+ primary: "#0969da", primaryDim: "#0550ae", secondary: "#8250df", accent: "#bf3989",
56
+ success: "#1a7f37", error: "#cf222e", warning: "#9a6700", info: "#0969da",
57
+ },
58
+
59
+ // ── Ultra: Poly (Polymarket brand — white and blue) ──
60
+ poly: {
61
+ bg: "#f8f9fb", bgSecondary: "#eef1f6", bgDarker: "#e2e7ef", selection: "#d4dbe8",
62
+ text: "#1a1f36", textMuted: "#6b7394", textEmphasized: "#0a0f24",
63
+ border: "#d4dbe8", borderDim: "#e2e7ef", borderFocus: "#3b82f6",
64
+ primary: "#3b82f6", primaryDim: "#2563eb", secondary: "#6366f1", accent: "#8b5cf6",
65
+ success: "#10b981", error: "#ef4444", warning: "#f59e0b", info: "#3b82f6",
66
+ },
67
+
68
+ // ── Ultra: Poly Dark (black and blue) ──
69
+ "poly-dark": {
70
+ bg: "#0f1120", bgSecondary: "#151830", bgDarker: "#0a0c18", selection: "#1e2240",
71
+ text: "#c8cee0", textMuted: "#5b6080", textEmphasized: "#f0f2ff",
72
+ border: "#2a2f50", borderDim: "#1e2240", borderFocus: "#3b82f6",
73
+ primary: "#3b82f6", primaryDim: "#2563eb", secondary: "#6366f1", accent: "#818cf8",
74
+ success: "#34d399", error: "#f87171", warning: "#fbbf24", info: "#60a5fa",
75
+ },
76
+
77
+ // ── Ultra: Limitless (black and neon green) ──
78
+ limitless: {
79
+ bg: "#0a0a0a", bgSecondary: "#111111", bgDarker: "#050505", selection: "#1a2a1a",
80
+ text: "#b0b0b0", textMuted: "#505050", textEmphasized: "#00ff41",
81
+ border: "#1a3a1a", borderDim: "#151515", borderFocus: "#00ff41",
82
+ primary: "#00ff41", primaryDim: "#00cc33", secondary: "#00e5ff", accent: "#00ff41",
83
+ success: "#00ff41", error: "#ff1744", warning: "#ffea00", info: "#00e5ff",
84
+ },
85
+
86
+ // ── Ultra: Apple 1984 (phosphor green on black CRT) ──
87
+ "apple-1984": {
88
+ bg: "#000000", bgSecondary: "#0a0f0a", bgDarker: "#000000", selection: "#0d1f0d",
89
+ text: "#33ff33", textMuted: "#1a8c1a", textEmphasized: "#66ff66",
90
+ border: "#1a5c1a", borderDim: "#0d2e0d", borderFocus: "#33ff33",
91
+ primary: "#33ff33", primaryDim: "#1aaa1a", secondary: "#33ff33", accent: "#66ff66",
92
+ success: "#33ff33", error: "#ff3333", warning: "#ffff33", info: "#33ff33",
93
+ },
94
+ };
95
+
96
+ // Mutable — setTheme overwrites properties so existing references stay valid
97
+ export const COLORS: Colors = { ...THEMES.dark };
98
+
99
+ export function setTheme(name: ThemeName): void {
100
+ const theme = THEMES[name];
101
+ if (!theme) return;
102
+ Object.assign(COLORS, theme);
103
+ }
104
+
105
+ export function getThemeNames(): ThemeName[] {
106
+ return Object.keys(THEMES) as ThemeName[];
107
+ }
@@ -0,0 +1,27 @@
1
+ export const ICONS = {
2
+ // App
3
+ logo: "\u2B2C", // ⬬
4
+
5
+ // Status
6
+ check: "\u2713", // ✓
7
+ error: "\u2716", // ✖
8
+ warning: "\u26A0", // ⚠
9
+ info: "i",
10
+ spinner: "\u27F3", // ⟳
11
+
12
+ // States
13
+ running: "\u25CF", // ●
14
+ paused: "\u25CB", // ○
15
+ stopped: "\u25A0", // ■
16
+
17
+ // Navigation
18
+ pointer: "\u25B8", // ▸
19
+ dot: "\u00B7", // ·
20
+
21
+ // Sparkline blocks
22
+ spark: ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"] as const,
23
+
24
+ // Progress
25
+ barFull: "\u2588", // █
26
+ barEmpty: "\u2591", // ░
27
+ } as const;