horizon-code 0.1.2 → 0.3.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/bin/horizon.js +18 -1
- package/package.json +1 -1
- package/src/ai/client.ts +33 -8
- package/src/ai/system-prompt.ts +48 -6
- package/src/app.ts +167 -14
- package/src/components/code-panel.ts +223 -17
- package/src/components/footer.ts +3 -0
- package/src/components/settings-panel.ts +14 -0
- package/src/platform/exchanges.ts +154 -0
- package/src/platform/session-sync.ts +1 -1
- package/src/research/apis.ts +208 -11
- package/src/research/stock-apis.ts +117 -0
- package/src/research/tools.ts +929 -17
- package/src/research/widgets.ts +1042 -29
- package/src/state/types.ts +1 -0
- package/src/strategy/code-stream.ts +3 -1
- package/src/strategy/dashboard.ts +189 -18
- package/src/strategy/prompts.ts +426 -6
- package/src/strategy/tools.ts +311 -54
- package/src/strategy/validator.ts +98 -0
- package/src/updater.ts +118 -0
package/bin/horizon.js
CHANGED
|
@@ -1,2 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import "../src/
|
|
2
|
+
import { checkForUpdates } from "../src/updater.ts";
|
|
3
|
+
|
|
4
|
+
// Auto-update before loading the app
|
|
5
|
+
const { updated, from, to } = await checkForUpdates();
|
|
6
|
+
if (updated) {
|
|
7
|
+
// Re-exec so the new version's code loads
|
|
8
|
+
process.stderr.write(`\x1b[2mUpdated ${from} → ${to}. Restarting...\x1b[0m\n`);
|
|
9
|
+
const result = Bun.spawnSync(["bun", "run", import.meta.path], {
|
|
10
|
+
stdin: "inherit",
|
|
11
|
+
stdout: "inherit",
|
|
12
|
+
stderr: "inherit",
|
|
13
|
+
env: { ...process.env, __HORIZON_SKIP_UPDATE: "1" },
|
|
14
|
+
});
|
|
15
|
+
process.exit(result.exitCode ?? 0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Load the app
|
|
19
|
+
import("../src/index.ts");
|
package/package.json
CHANGED
package/src/ai/client.ts
CHANGED
|
@@ -152,6 +152,7 @@ import { zodSchema } from "@ai-sdk/provider-utils";
|
|
|
152
152
|
|
|
153
153
|
function toolsToServerFormat(tools: Record<string, any>): Record<string, any> {
|
|
154
154
|
const result: Record<string, any> = {};
|
|
155
|
+
let failures = 0;
|
|
155
156
|
for (const [name, tool] of Object.entries(tools)) {
|
|
156
157
|
if (!tool) continue;
|
|
157
158
|
let params: any = { type: "object", properties: {} };
|
|
@@ -184,17 +185,26 @@ async function callServer(opts: {
|
|
|
184
185
|
const config = loadConfig();
|
|
185
186
|
if (!config.api_key) throw new Error("Not authenticated. Type /login.");
|
|
186
187
|
|
|
188
|
+
const toolPayload = toolsToServerFormat(opts.tools);
|
|
189
|
+
const body = JSON.stringify({
|
|
190
|
+
messages: opts.messages,
|
|
191
|
+
system: opts.system,
|
|
192
|
+
model_power: opts.modelPower,
|
|
193
|
+
tools: toolPayload,
|
|
194
|
+
response_format: opts.responseFormat,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Use the caller's abort signal, with a fallback 2-min timeout
|
|
198
|
+
const timeoutSignal = AbortSignal.timeout(120000);
|
|
199
|
+
const combinedSignal = opts.signal
|
|
200
|
+
? AbortSignal.any([opts.signal, timeoutSignal])
|
|
201
|
+
: timeoutSignal;
|
|
202
|
+
|
|
187
203
|
const res = await fetch(`${getAuthUrl()}/api/v1/chat`, {
|
|
188
204
|
method: "POST",
|
|
189
|
-
signal:
|
|
205
|
+
signal: combinedSignal,
|
|
190
206
|
headers: { "Authorization": `Bearer ${config.api_key}`, "Content-Type": "application/json" },
|
|
191
|
-
body
|
|
192
|
-
messages: opts.messages,
|
|
193
|
-
system: opts.system,
|
|
194
|
-
model_power: opts.modelPower,
|
|
195
|
-
tools: toolsToServerFormat(opts.tools),
|
|
196
|
-
response_format: opts.responseFormat,
|
|
197
|
-
}),
|
|
207
|
+
body,
|
|
198
208
|
});
|
|
199
209
|
|
|
200
210
|
if (!res.ok) {
|
|
@@ -248,7 +258,12 @@ export async function* chat(
|
|
|
248
258
|
signal,
|
|
249
259
|
});
|
|
250
260
|
} catch (err: any) {
|
|
261
|
+
if (signal.aborted) {
|
|
262
|
+
yield { type: "finish" };
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
251
265
|
yield { type: "error", message: err.message };
|
|
266
|
+
yield { type: "finish" };
|
|
252
267
|
return;
|
|
253
268
|
}
|
|
254
269
|
|
|
@@ -262,6 +277,7 @@ export async function* chat(
|
|
|
262
277
|
let eventCount = 0;
|
|
263
278
|
|
|
264
279
|
for await (const event of consumeSSE(res)) {
|
|
280
|
+
if (signal.aborted) break;
|
|
265
281
|
eventCount++;
|
|
266
282
|
if (event.type === "meta") {
|
|
267
283
|
yield {
|
|
@@ -323,6 +339,9 @@ export async function* chat(
|
|
|
323
339
|
if (!hasToolCalls || pendingToolCalls.length === 0) {
|
|
324
340
|
if (eventCount === 0) {
|
|
325
341
|
yield { type: "error", message: "No response from server. Check your connection or try again." };
|
|
342
|
+
} else if (!hasToolCalls && jsonBuf === "" && emittedResponseLen === 0) {
|
|
343
|
+
// Server sent events (meta, usage) but no actual content
|
|
344
|
+
yield { type: "text-delta", textDelta: "*(Server returned no content. This may happen if the model is overloaded. Try again.)*" };
|
|
326
345
|
}
|
|
327
346
|
yield { type: "finish" };
|
|
328
347
|
return;
|
|
@@ -351,6 +370,12 @@ export async function* chat(
|
|
|
351
370
|
}
|
|
352
371
|
}
|
|
353
372
|
|
|
373
|
+
// If aborted during tool execution, stop
|
|
374
|
+
if (signal.aborted) {
|
|
375
|
+
yield { type: "finish" };
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
354
379
|
// Add as assistant message — the LLM sees it already ran the tools
|
|
355
380
|
serverMessages.push({ role: "assistant", content: assistantParts.join("\n\n") });
|
|
356
381
|
// Prompt the LLM to continue with its response based on the tool results
|
package/src/ai/system-prompt.ts
CHANGED
|
@@ -15,7 +15,7 @@ const MODE_PROMPTS: Record<Mode, string> = {
|
|
|
15
15
|
|
|
16
16
|
## Mode: Research
|
|
17
17
|
|
|
18
|
-
You have
|
|
18
|
+
You have 29 research tools that fetch LIVE data from Polymarket, Kalshi, stocks, and the web. Use them.
|
|
19
19
|
|
|
20
20
|
### CRITICAL RULE
|
|
21
21
|
When the user mentions ANY topic, event, market, or asks about what's happening — ALWAYS call a tool to search for real data. Never answer from your training data alone. Prediction markets change by the minute.
|
|
@@ -33,24 +33,66 @@ When the user mentions ANY topic, event, market, or asks about what's happening
|
|
|
33
33
|
- kalshiEvents(query) — search Kalshi markets
|
|
34
34
|
- webSearch(query) — search the internet for news, facts, context
|
|
35
35
|
- calaKnowledge(query) — deep research on companies, people, industries
|
|
36
|
+
- liquidityScanner(query, limit) — scan top markets ranked by liquidity, spread, volume
|
|
36
37
|
|
|
37
|
-
**Analyze a specific
|
|
38
|
+
**Analyze a specific Polymarket (requires a slug from polymarketEvents):**
|
|
38
39
|
- polymarketEventDetail(slug) — full details, sub-markets, price history, spreads
|
|
39
40
|
- polymarketPriceHistory(slug, interval) — price chart (1h/6h/1d/1w/1m/max)
|
|
40
41
|
- polymarketOrderBook(slug) — bid/ask depth
|
|
41
42
|
- whaleTracker(slug) — large trades, smart money flow
|
|
42
43
|
- newsSentiment(slug) — news sentiment score + articles
|
|
43
|
-
- historicalVolatility(slug, interval) —
|
|
44
|
+
- historicalVolatility(slug, interval) — multi-estimator vol (close-to-close, Parkinson, Garman-Klass, EWMA), regime detection
|
|
45
|
+
- marketMicrostructure(slug) — depth imbalance, effective spread, Kyle's lambda, liquidity score
|
|
46
|
+
|
|
47
|
+
**Analyze a specific Kalshi market (requires a ticker from kalshiEvents):**
|
|
48
|
+
- kalshiEventDetail(ticker) — full details, all sub-markets, spreads, volumes
|
|
49
|
+
- kalshiOrderBook(ticker) — bid/ask depth for a Kalshi market
|
|
50
|
+
- kalshiPriceHistory(ticker, period) — price chart for a Kalshi market
|
|
51
|
+
|
|
52
|
+
**Whale & wallet analysis (requires wallet address from whaleTracker results):**
|
|
53
|
+
- walletProfiler(address) — score a wallet: win rate, PnL, Sharpe, edge category (smart_money/neutral/weak_hand/bot)
|
|
54
|
+
- botDetector(slug) — detect bots in a market: classify top traders, identify strategy types (market_maker, grid_bot, momentum, sniper)
|
|
55
|
+
- marketFlow(slug, limit) — deep flow analysis: buy/sell volume, wallet concentration (Herfindahl), top movers
|
|
44
56
|
|
|
45
57
|
**Quantitative analysis:**
|
|
46
58
|
- evCalculator(slug, side, estimatedEdge, bankroll) — expected value + Kelly sizing
|
|
47
59
|
- probabilityCalculator(slugA, slugB) — joint/conditional probability
|
|
48
60
|
- compareMarkets(topic) — cross-platform price comparison (Polymarket vs Kalshi)
|
|
61
|
+
- correlationAnalysis(slugA, slugB, interval) — price correlation, beta, dual sparklines between two markets
|
|
62
|
+
- riskMetrics(slug, interval, confidenceLevel) — VaR, CVaR, max drawdown, Sharpe, Sortino, Calmar, skewness, kurtosis
|
|
63
|
+
- marketRegime(slug, interval) — Hurst exponent, variance ratio, entropy — detect if market is trending, mean-reverting, or efficient
|
|
49
64
|
|
|
50
|
-
|
|
51
|
-
|
|
65
|
+
**Stock market (Yahoo Finance — free, no API key needed):**
|
|
66
|
+
- stockQuote(symbols) — real-time quotes: price, change, volume, market cap, P/E, 52-week range, moving averages. Comma-separated: "AAPL,MSFT,TSLA"
|
|
67
|
+
- stockChart(symbol, range, interval) — price chart with sparkline. Ranges: 1d, 5d, 1mo, 3mo, 6mo, 1y, 5y, max
|
|
68
|
+
- stockSearch(query) — search tickers by name/keyword: "electric vehicles", "gold ETF"
|
|
69
|
+
- stockScreener(symbols) — compare stocks side-by-side: P/E, market cap, 52-week, moving averages
|
|
52
70
|
|
|
53
|
-
|
|
71
|
+
**Visualization:**
|
|
72
|
+
- openChart(source, identifier, range) — open interactive Chart.js chart in browser. source: "polymarket" or "stock". Great for detailed analysis.
|
|
73
|
+
|
|
74
|
+
### Tool Chaining
|
|
75
|
+
When the user refers to a market from a previous result, reuse the SLUG (Polymarket) or TICKER (Kalshi) from that result. Don't search again — go straight to the detail tool.
|
|
76
|
+
|
|
77
|
+
Use your intelligence to understand what the user wants:
|
|
78
|
+
- "dig deeper" → polymarketEventDetail or kalshiEventDetail
|
|
79
|
+
- "what do whales think" → whaleTracker
|
|
80
|
+
- "show me the order book" → polymarketOrderBook or kalshiOrderBook
|
|
81
|
+
- "how liquid is this" → marketMicrostructure
|
|
82
|
+
- "do these markets correlate" → correlationAnalysis
|
|
83
|
+
- "what's the best market to trade" → liquidityScanner
|
|
84
|
+
- "compare Polymarket and Kalshi" → compareMarkets
|
|
85
|
+
- "what's the volatility" → historicalVolatility (now shows 4 estimators)
|
|
86
|
+
- "show me the Kalshi chart" → kalshiPriceHistory
|
|
87
|
+
- "who are the bots" → botDetector
|
|
88
|
+
- "analyze this wallet" → walletProfiler (needs 0x address from whaleTracker)
|
|
89
|
+
- "where is the money flowing" → marketFlow
|
|
90
|
+
- "what's the risk" → riskMetrics (VaR, CVaR, Sharpe, drawdown)
|
|
91
|
+
- "is this market trending" → marketRegime (Hurst exponent, variance ratio)
|
|
92
|
+
- "show me AAPL" → stockQuote("AAPL")
|
|
93
|
+
- "compare big tech" → stockScreener("AAPL,MSFT,GOOG,AMZN,META")
|
|
94
|
+
- "chart this" → openChart (opens interactive chart in browser)
|
|
95
|
+
- "search for AI stocks" → stockSearch("artificial intelligence")`,
|
|
54
96
|
|
|
55
97
|
strategy: "", // dynamically generated
|
|
56
98
|
|
package/src/app.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BoxRenderable, ScrollBoxRenderable, type CliRenderer } from "@opentui/core";
|
|
1
|
+
import { BoxRenderable, TextRenderable, ScrollBoxRenderable, type CliRenderer } from "@opentui/core";
|
|
2
2
|
import { COLORS, setTheme, ULTRA_THEMES } from "./theme/colors.ts";
|
|
3
3
|
import { Footer } from "./components/footer.ts";
|
|
4
4
|
import { Splash } from "./components/splash.ts";
|
|
@@ -13,12 +13,14 @@ import { ModeBar } from "./components/mode-bar.ts";
|
|
|
13
13
|
import { TabBar } from "./components/tab-bar.ts";
|
|
14
14
|
import { InputBar } from "./components/input-bar.ts";
|
|
15
15
|
import { ChatRenderer } from "./chat/renderer.ts";
|
|
16
|
+
import { renderToolWidget } from "./research/widgets.ts";
|
|
17
|
+
import { renderStrategyWidget } from "./strategy/widgets.ts";
|
|
16
18
|
import { KeyHandler } from "./keys/handler.ts";
|
|
17
19
|
import { store } from "./state/store.ts";
|
|
18
20
|
import { createUserMessage } from "./chat/messages.ts";
|
|
19
21
|
import { chat } from "./ai/client.ts";
|
|
20
22
|
import { dashboard } from "./strategy/dashboard.ts";
|
|
21
|
-
import { cleanupStrategyProcesses, runningProcesses } from "./strategy/tools.ts";
|
|
23
|
+
import { cleanupStrategyProcesses, runningProcesses, parseLocalMetrics } from "./strategy/tools.ts";
|
|
22
24
|
import { abortChat } from "./ai/client.ts";
|
|
23
25
|
import type { ModelPower } from "./platform/tiers.ts";
|
|
24
26
|
import { listSavedStrategies, loadStrategy } from "./strategy/persistence.ts";
|
|
@@ -26,6 +28,7 @@ import { autoFixStrategyCode } from "./strategy/validator.ts";
|
|
|
26
28
|
import { CodeFenceDetector, finalizeStrategy, isStrategyCode, extractStrategyName } from "./strategy/code-stream.ts";
|
|
27
29
|
import { loginWithBrowser, loginWithPassword, signOut, isLoggedIn, getUser } from "./platform/supabase.ts";
|
|
28
30
|
import { loadConfig, saveConfig, setEncryptedEnv, removeEncryptedEnv, listEncryptedEnvNames } from "./platform/config.ts";
|
|
31
|
+
import { EXCHANGE_PROFILES, getExchangeStatuses, getExchangeStatus } from "./platform/exchanges.ts";
|
|
29
32
|
import { getTreeSitterClient, destroyTreeSitterClient } from "./syntax/setup.ts";
|
|
30
33
|
import type { Message } from "./chat/types.ts";
|
|
31
34
|
|
|
@@ -33,7 +36,11 @@ import type { Message } from "./chat/types.ts";
|
|
|
33
36
|
const WIDGET_TOOLS = new Set([
|
|
34
37
|
"polymarketEvents", "polymarketEventDetail", "polymarketPriceHistory",
|
|
35
38
|
"polymarketOrderBook", "whaleTracker", "newsSentiment", "evCalculator",
|
|
36
|
-
"kalshiEvents", "
|
|
39
|
+
"kalshiEvents", "kalshiEventDetail", "kalshiOrderBook", "kalshiPriceHistory",
|
|
40
|
+
"compareMarkets", "historicalVolatility",
|
|
41
|
+
"marketMicrostructure", "liquidityScanner", "correlationAnalysis",
|
|
42
|
+
"walletProfiler", "botDetector", "marketFlow", "riskMetrics", "marketRegime",
|
|
43
|
+
"stockQuote", "stockChart", "stockSearch", "stockScreener",
|
|
37
44
|
"webSearch", "calaKnowledge", "probabilityCalculator",
|
|
38
45
|
"edit_strategy", "validate_strategy",
|
|
39
46
|
"backtest_strategy", "polymarket_data",
|
|
@@ -65,6 +72,8 @@ const SLASH_COMMANDS: Record<string, { description: string; usage: string }> = {
|
|
|
65
72
|
"/strategies": { description: "List saved strategies", usage: "/strategies" },
|
|
66
73
|
"/load": { description: "Load a saved strategy", usage: "/load <name>" },
|
|
67
74
|
"/env": { description: "Manage encrypted API keys", usage: "/env [list|add|remove]" },
|
|
75
|
+
"/connect": { description: "Connect an exchange", usage: "/connect [polymarket|kalshi|alpaca|...]" },
|
|
76
|
+
"/exchanges": { description: "Show exchange connection status", usage: "/exchanges" },
|
|
68
77
|
"/quit": { description: "Exit Horizon", usage: "/quit" },
|
|
69
78
|
"/exit": { description: "Exit Horizon", usage: "/exit" },
|
|
70
79
|
};
|
|
@@ -92,6 +101,7 @@ export class App {
|
|
|
92
101
|
private inChatMode = false;
|
|
93
102
|
private authenticated = false;
|
|
94
103
|
private _openIdsSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
104
|
+
private _hasLocalMetrics = false;
|
|
95
105
|
|
|
96
106
|
// Per-tab stream state
|
|
97
107
|
private tabStreams: Map<string, {
|
|
@@ -403,6 +413,27 @@ export class App {
|
|
|
403
413
|
this.codePanel.setStrategy(draft.name, draft.phase);
|
|
404
414
|
}
|
|
405
415
|
|
|
416
|
+
// Feed active deployment metrics into the dashboard tab
|
|
417
|
+
// Skip if local process metrics are active (they take priority)
|
|
418
|
+
if (!this._hasLocalMetrics) {
|
|
419
|
+
const state = store.get();
|
|
420
|
+
const running = state.deployments.find((d) => d.status === "running" || d.status === "starting");
|
|
421
|
+
if (running) {
|
|
422
|
+
this.codePanel.setMetrics({
|
|
423
|
+
name: running.name,
|
|
424
|
+
status: running.status,
|
|
425
|
+
dryRun: running.dry_run,
|
|
426
|
+
metrics: running.metrics,
|
|
427
|
+
positions: running.positions,
|
|
428
|
+
orders: running.orders,
|
|
429
|
+
pnlHistory: running.pnl_history,
|
|
430
|
+
startedAt: running.started_at,
|
|
431
|
+
});
|
|
432
|
+
} else {
|
|
433
|
+
this.codePanel.setMetrics(null);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
406
437
|
// Sync code panel visibility to key handler for Tab cycling
|
|
407
438
|
this.keyHandler.codePanelVisible = this.codePanel.visible;
|
|
408
439
|
|
|
@@ -412,28 +443,82 @@ export class App {
|
|
|
412
443
|
|
|
413
444
|
renderer.on("resize", () => renderer.requestRender());
|
|
414
445
|
|
|
415
|
-
// Poll background processes — update count, live logs, clean dead
|
|
446
|
+
// Poll background processes — update count, live logs, parse metrics, clean dead
|
|
416
447
|
setInterval(() => {
|
|
417
448
|
let alive = 0;
|
|
418
449
|
let latestLogs = "";
|
|
419
450
|
const deadPids: number[] = [];
|
|
420
451
|
const now = Date.now();
|
|
452
|
+
let localMetricsData: ReturnType<typeof parseLocalMetrics> = null;
|
|
453
|
+
let localProcessStartedAt = 0;
|
|
421
454
|
|
|
422
455
|
for (const [pid, m] of runningProcesses) {
|
|
423
456
|
if (m.proc.exitCode === null) {
|
|
424
457
|
alive++;
|
|
425
|
-
|
|
426
|
-
const
|
|
427
|
-
|
|
458
|
+
// Filter out __HZ_METRICS__ lines from visible logs
|
|
459
|
+
const stdoutLines = m.stdout.slice(-30);
|
|
460
|
+
const stderrLines = m.stderr.slice(-10).filter((l: string) => !l.startsWith("__HZ_METRICS__"));
|
|
461
|
+
latestLogs = stdoutLines.join("\n") + (stderrLines.length > 0 ? "\n--- stderr ---\n" + stderrLines.join("\n") : "");
|
|
462
|
+
|
|
463
|
+
// Parse latest metrics from this process
|
|
464
|
+
const metrics = parseLocalMetrics(m);
|
|
465
|
+
if (metrics) {
|
|
466
|
+
localMetricsData = metrics;
|
|
467
|
+
localProcessStartedAt = m.startedAt;
|
|
468
|
+
}
|
|
428
469
|
} else if (now - m.startedAt > 300000) {
|
|
429
|
-
// Dead for 5+ minutes — clean up
|
|
430
470
|
deadPids.push(pid);
|
|
431
471
|
}
|
|
432
472
|
}
|
|
433
473
|
for (const pid of deadPids) runningProcesses.delete(pid);
|
|
434
474
|
|
|
435
475
|
this.modeBar.setBgProcessCount(alive);
|
|
436
|
-
|
|
476
|
+
|
|
477
|
+
// Feed local metrics to the Metrics tab (takes priority over platform deployments)
|
|
478
|
+
if (localMetricsData) {
|
|
479
|
+
const draft = store.getActiveSession()?.strategyDraft;
|
|
480
|
+
this.codePanel.setMetrics({
|
|
481
|
+
name: draft?.name ?? "Local Strategy",
|
|
482
|
+
status: "running",
|
|
483
|
+
dryRun: true,
|
|
484
|
+
metrics: {
|
|
485
|
+
total_pnl: localMetricsData.pnl,
|
|
486
|
+
realized_pnl: localMetricsData.rpnl,
|
|
487
|
+
unrealized_pnl: localMetricsData.upnl,
|
|
488
|
+
total_exposure: 0,
|
|
489
|
+
position_count: localMetricsData.positions,
|
|
490
|
+
open_order_count: localMetricsData.orders,
|
|
491
|
+
win_rate: 0,
|
|
492
|
+
total_trades: 0,
|
|
493
|
+
max_drawdown_pct: 0,
|
|
494
|
+
sharpe_ratio: 0,
|
|
495
|
+
profit_factor: 0,
|
|
496
|
+
avg_return_per_trade: 0,
|
|
497
|
+
gross_profit: 0,
|
|
498
|
+
gross_loss: 0,
|
|
499
|
+
},
|
|
500
|
+
positions: (localMetricsData.pos ?? []).map((p) => ({
|
|
501
|
+
market_id: p.id,
|
|
502
|
+
slug: p.id,
|
|
503
|
+
question: "",
|
|
504
|
+
side: p.side === "Yes" || p.side === "Buy" ? "BUY" as const : "SELL" as const,
|
|
505
|
+
size: p.sz,
|
|
506
|
+
avg_entry_price: p.entry,
|
|
507
|
+
cost_basis: p.sz * p.entry,
|
|
508
|
+
realized_pnl: p.rpnl,
|
|
509
|
+
unrealized_pnl: p.upnl,
|
|
510
|
+
})),
|
|
511
|
+
orders: [],
|
|
512
|
+
pnlHistory: localMetricsData.hist ?? [],
|
|
513
|
+
startedAt: localProcessStartedAt,
|
|
514
|
+
});
|
|
515
|
+
this._hasLocalMetrics = true;
|
|
516
|
+
} else if (this._hasLocalMetrics && alive === 0) {
|
|
517
|
+
// Local process stopped — clear local metrics, let platform data take over
|
|
518
|
+
this._hasLocalMetrics = false;
|
|
519
|
+
store.update({});
|
|
520
|
+
}
|
|
521
|
+
|
|
437
522
|
if (alive > 0 && latestLogs && this.codePanel.visible && this.codePanel.activeTab === "logs") {
|
|
438
523
|
this.codePanel.setLogs(latestLogs);
|
|
439
524
|
}
|
|
@@ -655,7 +740,7 @@ export class App {
|
|
|
655
740
|
const code = autoFixStrategyCode(loaded.code);
|
|
656
741
|
store.setStrategyDraft({
|
|
657
742
|
name: arg, code, params: {}, explanation: "", riskConfig: null,
|
|
658
|
-
validationStatus: "none", validationErrors: [], phase: "generated",
|
|
743
|
+
validationStatus: "none", validationErrors: [], validationWarnings: [], phase: "generated",
|
|
659
744
|
});
|
|
660
745
|
this.codePanel.setCode(code, "pending");
|
|
661
746
|
this.codePanel.setStrategy(arg, "loaded");
|
|
@@ -693,6 +778,50 @@ export class App {
|
|
|
693
778
|
}
|
|
694
779
|
break;
|
|
695
780
|
}
|
|
781
|
+
case "/connect": {
|
|
782
|
+
const exchangeId = arg.trim().toLowerCase();
|
|
783
|
+
if (!exchangeId) {
|
|
784
|
+
const statuses = getExchangeStatuses();
|
|
785
|
+
const lines = statuses.map(s => {
|
|
786
|
+
const icon = s.status === "connected" ? "●" : s.status === "partial" ? "◐" : "○";
|
|
787
|
+
const color = s.status === "connected" ? "green" : s.status === "partial" ? "yellow" : "dim";
|
|
788
|
+
return ` ${icon} ${s.profile.name.padEnd(20)} ${s.status.padEnd(14)} ${s.profile.category}`;
|
|
789
|
+
});
|
|
790
|
+
this.showSystemMsg(`Exchange Connections:\n${lines.join("\n")}\n\nUse /connect <exchange> to set up keys.`);
|
|
791
|
+
} else {
|
|
792
|
+
const profile = EXCHANGE_PROFILES.find(p => p.id === exchangeId || p.name.toLowerCase() === exchangeId);
|
|
793
|
+
if (!profile) {
|
|
794
|
+
const ids = EXCHANGE_PROFILES.map(p => p.id).join(", ");
|
|
795
|
+
this.showSystemMsg(`Unknown exchange: ${exchangeId}. Available: ${ids}`);
|
|
796
|
+
} else {
|
|
797
|
+
const status = getExchangeStatus(profile.id);
|
|
798
|
+
const lines = profile.keys.map(k => {
|
|
799
|
+
const isSet = status?.keysSet.includes(k.env);
|
|
800
|
+
return ` ${isSet ? "●" : "○"} ${k.label.padEnd(18)} ${k.env.padEnd(28)} ${isSet ? "set" : k.required ? "REQUIRED" : "optional"}`;
|
|
801
|
+
});
|
|
802
|
+
this.showSystemMsg(
|
|
803
|
+
`${profile.name} — ${profile.description}\n${lines.join("\n")}\n\n` +
|
|
804
|
+
`Set keys with: /env add <ENV_NAME> <value>\n` +
|
|
805
|
+
`Example: /env add ${profile.keys[0].env} <your-key>`
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
case "/exchanges": {
|
|
812
|
+
const statuses = getExchangeStatuses();
|
|
813
|
+
const connected = statuses.filter(s => s.status === "connected");
|
|
814
|
+
const partial = statuses.filter(s => s.status === "partial");
|
|
815
|
+
const disconnected = statuses.filter(s => s.status === "disconnected");
|
|
816
|
+
|
|
817
|
+
let msg = "Exchange Status:\n";
|
|
818
|
+
if (connected.length > 0) msg += `\nConnected:\n${connected.map(s => ` ● ${s.profile.name} (${s.profile.category})`).join("\n")}`;
|
|
819
|
+
if (partial.length > 0) msg += `\nPartial:\n${partial.map(s => ` ◐ ${s.profile.name} — missing: ${s.keysMissing.join(", ")}`).join("\n")}`;
|
|
820
|
+
if (disconnected.length > 0) msg += `\nNot Connected:\n${disconnected.map(s => ` ○ ${s.profile.name}`).join("\n")}`;
|
|
821
|
+
msg += "\n\nUse /connect <exchange> to set up.";
|
|
822
|
+
this.showSystemMsg(msg);
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
696
825
|
case "/quit":
|
|
697
826
|
case "/exit":
|
|
698
827
|
this.quit();
|
|
@@ -1198,7 +1327,33 @@ export class App {
|
|
|
1198
1327
|
}
|
|
1199
1328
|
|
|
1200
1329
|
if (WIDGET_TOOLS.has(part.toolName)) {
|
|
1201
|
-
|
|
1330
|
+
const { showWidgets, widgetsInTab } = this.settingsPanel.settings;
|
|
1331
|
+
if (showWidgets && !widgetsInTab) {
|
|
1332
|
+
// Render inline in chat (default)
|
|
1333
|
+
currentBlocks.push({ type: "tool-widget", toolName: part.toolName, widgetData: part.result });
|
|
1334
|
+
} else if (showWidgets && widgetsInTab) {
|
|
1335
|
+
// Render in the widgets tab
|
|
1336
|
+
try {
|
|
1337
|
+
const result = part.result;
|
|
1338
|
+
// Skip error results — they'd render as empty widgets
|
|
1339
|
+
if (result && typeof result === "object" && "error" in (result as any)) {
|
|
1340
|
+
const errBox = new BoxRenderable(this.renderer, { id: `wt-err-${Date.now()}`, flexDirection: "column" });
|
|
1341
|
+
errBox.add(new TextRenderable(this.renderer, { id: `wt-err-t-${Date.now()}`, content: `Error: ${(result as any).error}`, fg: COLORS.error }));
|
|
1342
|
+
this.codePanel.addWidget(part.toolName, errBox);
|
|
1343
|
+
} else {
|
|
1344
|
+
const widget = renderToolWidget(part.toolName, result, this.renderer)
|
|
1345
|
+
?? renderStrategyWidget(part.toolName, result, this.renderer);
|
|
1346
|
+
if (widget) {
|
|
1347
|
+
this.codePanel.addWidget(part.toolName, widget);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
if (!this.codePanel.visible) this.codePanel.show();
|
|
1351
|
+
this.codePanel.setTab("widgets");
|
|
1352
|
+
} catch {
|
|
1353
|
+
// Widget rendering failed silently — don't break the stream
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
// else showWidgets=false: skip rendering, LLM still got the data
|
|
1202
1357
|
}
|
|
1203
1358
|
|
|
1204
1359
|
// Update code panel from edit_strategy/load_saved_strategy results
|
|
@@ -1226,12 +1381,10 @@ export class App {
|
|
|
1226
1381
|
}
|
|
1227
1382
|
}
|
|
1228
1383
|
|
|
1229
|
-
//
|
|
1384
|
+
// Switch to dashboard/metrics tab when dashboard is spawned
|
|
1230
1385
|
if (part.toolName === "spawn_dashboard") {
|
|
1231
1386
|
const result = part.result as any;
|
|
1232
1387
|
if (result?.success) {
|
|
1233
|
-
const html = (part as any).args?.custom_html;
|
|
1234
|
-
if (html) this.codePanel.setDashboardHtml(html);
|
|
1235
1388
|
this.codePanel.setTab("dashboard");
|
|
1236
1389
|
if (!this.codePanel.visible) this.codePanel.show();
|
|
1237
1390
|
}
|