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.
- package/assets/python/highlights.scm +137 -0
- package/assets/python/tree-sitter-python.wasm +0 -0
- package/bin/horizon.js +2 -0
- package/package.json +40 -0
- package/src/ai/client.ts +369 -0
- package/src/ai/system-prompt.ts +86 -0
- package/src/app.ts +1454 -0
- package/src/chat/messages.ts +48 -0
- package/src/chat/renderer.ts +243 -0
- package/src/chat/types.ts +18 -0
- package/src/components/code-panel.ts +329 -0
- package/src/components/footer.ts +72 -0
- package/src/components/hooks-panel.ts +224 -0
- package/src/components/input-bar.ts +193 -0
- package/src/components/mode-bar.ts +245 -0
- package/src/components/session-panel.ts +294 -0
- package/src/components/settings-panel.ts +372 -0
- package/src/components/splash.ts +156 -0
- package/src/components/strategy-panel.ts +489 -0
- package/src/components/tab-bar.ts +112 -0
- package/src/components/tutorial-panel.ts +680 -0
- package/src/components/widgets/progress-bar.ts +38 -0
- package/src/components/widgets/sparkline.ts +57 -0
- package/src/hooks/executor.ts +109 -0
- package/src/index.ts +22 -0
- package/src/keys/handler.ts +198 -0
- package/src/platform/auth.ts +36 -0
- package/src/platform/client.ts +159 -0
- package/src/platform/config.ts +121 -0
- package/src/platform/session-sync.ts +158 -0
- package/src/platform/supabase.ts +376 -0
- package/src/platform/sync.ts +149 -0
- package/src/platform/tiers.ts +103 -0
- package/src/platform/tools.ts +163 -0
- package/src/platform/types.ts +86 -0
- package/src/platform/usage.ts +224 -0
- package/src/research/apis.ts +367 -0
- package/src/research/tools.ts +205 -0
- package/src/research/widgets.ts +523 -0
- package/src/state/store.ts +256 -0
- package/src/state/types.ts +109 -0
- package/src/strategy/ascii-chart.ts +74 -0
- package/src/strategy/code-stream.ts +146 -0
- package/src/strategy/dashboard.ts +140 -0
- package/src/strategy/persistence.ts +82 -0
- package/src/strategy/prompts.ts +626 -0
- package/src/strategy/sandbox.ts +137 -0
- package/src/strategy/tools.ts +764 -0
- package/src/strategy/validator.ts +216 -0
- package/src/strategy/widgets.ts +270 -0
- package/src/syntax/setup.ts +54 -0
- package/src/theme/colors.ts +107 -0
- package/src/theme/icons.ts +27 -0
- package/src/util/hyperlink.ts +21 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { platform } from "./client.ts";
|
|
2
|
+
import { isLoggedIn } from "./supabase.ts";
|
|
3
|
+
import { store } from "../state/store.ts";
|
|
4
|
+
import type { Deployment, DeploymentMetrics, Position, Order, DeploymentStatus } from "../state/types.ts";
|
|
5
|
+
import type { PlatformStrategy, PlatformDeployment, PlatformMetrics } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
const pnlHistories: Map<string, number[]> = new Map();
|
|
8
|
+
|
|
9
|
+
export class PlatformSync {
|
|
10
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
11
|
+
|
|
12
|
+
async start(intervalMs = 30000): Promise<void> {
|
|
13
|
+
const loggedIn = await isLoggedIn();
|
|
14
|
+
if (!loggedIn && !platform.authenticated) return;
|
|
15
|
+
|
|
16
|
+
await this.poll();
|
|
17
|
+
this.timer = setInterval(() => this.poll(), intervalMs);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
stop(): void {
|
|
21
|
+
if (this.timer) { clearInterval(this.timer); this.timer = null; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private async poll(): Promise<void> {
|
|
25
|
+
try {
|
|
26
|
+
const strategies = await platform.listStrategies();
|
|
27
|
+
|
|
28
|
+
// Parallel fetch: deployments for all strategies at once
|
|
29
|
+
const depResults = await Promise.allSettled(
|
|
30
|
+
strategies.map((s) => platform.listDeployments(s.id))
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Identify which strategies need metrics (running/starting only)
|
|
34
|
+
const stratWithDeps = strategies.map((strat, i) => {
|
|
35
|
+
const deps = depResults[i]?.status === "fulfilled" ? depResults[i].value : [];
|
|
36
|
+
const latestDep = deps.find((d) => d.status === "running")
|
|
37
|
+
?? deps.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0]
|
|
38
|
+
?? null;
|
|
39
|
+
|
|
40
|
+
let status: DeploymentStatus = "stopped";
|
|
41
|
+
if (latestDep) status = latestDep.status as DeploymentStatus;
|
|
42
|
+
else if (strat.status === "error") status = "error";
|
|
43
|
+
else if (strat.status === "deployed") status = "running";
|
|
44
|
+
|
|
45
|
+
return { strat, latestDep, status };
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Parallel fetch: metrics only for running strategies
|
|
49
|
+
const running = stratWithDeps.filter((s) => s.status === "running" || s.status === "starting");
|
|
50
|
+
const metricsResults = await Promise.allSettled(
|
|
51
|
+
running.map((s) => platform.getMetrics(s.strat.id, 1))
|
|
52
|
+
);
|
|
53
|
+
const metricsMap = new Map<string, typeof metricsResults[0]>();
|
|
54
|
+
running.forEach((s, i) => metricsMap.set(s.strat.id, metricsResults[i]!));
|
|
55
|
+
|
|
56
|
+
const deployments: Deployment[] = stratWithDeps.map(({ strat, latestDep, status }) => {
|
|
57
|
+
let metrics: DeploymentMetrics = emptyMetrics();
|
|
58
|
+
let positions: Position[] = [];
|
|
59
|
+
let orders: Order[] = [];
|
|
60
|
+
|
|
61
|
+
const mr = metricsMap.get(strat.id);
|
|
62
|
+
if (mr?.status === "fulfilled" && mr.value.latest) {
|
|
63
|
+
const m = mr.value.latest;
|
|
64
|
+
metrics = mapMetrics(m);
|
|
65
|
+
positions = mapPositions(m.positions ?? []);
|
|
66
|
+
orders = mapOrders(m.recent_orders ?? []);
|
|
67
|
+
|
|
68
|
+
let hist = pnlHistories.get(strat.id) ?? [];
|
|
69
|
+
hist.push(m.total_pnl);
|
|
70
|
+
if (hist.length > 60) hist = hist.slice(-60);
|
|
71
|
+
pnlHistories.set(strat.id, hist);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
id: latestDep?.id ?? strat.id,
|
|
76
|
+
strategyId: strat.id,
|
|
77
|
+
name: strat.name,
|
|
78
|
+
strategy_type: inferType(strat),
|
|
79
|
+
status,
|
|
80
|
+
dry_run: latestDep?.dry_run ?? true,
|
|
81
|
+
mode: latestDep?.deployment_mode ?? "manual",
|
|
82
|
+
metrics,
|
|
83
|
+
positions,
|
|
84
|
+
orders,
|
|
85
|
+
pnl_history: pnlHistories.get(strat.id) ?? [],
|
|
86
|
+
started_at: latestDep?.started_at ? new Date(latestDep.started_at).getTime() : new Date(strat.created_at).getTime(),
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
store.update({ deployments, connection: "connected" });
|
|
91
|
+
} catch {
|
|
92
|
+
store.update({ connection: "disconnected" });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function emptyMetrics(): DeploymentMetrics {
|
|
98
|
+
return {
|
|
99
|
+
total_pnl: 0, realized_pnl: 0, unrealized_pnl: 0,
|
|
100
|
+
total_exposure: 0, position_count: 0, open_order_count: 0,
|
|
101
|
+
win_rate: 0, total_trades: 0, max_drawdown_pct: 0,
|
|
102
|
+
sharpe_ratio: 0, profit_factor: 0, avg_return_per_trade: 0,
|
|
103
|
+
gross_profit: 0, gross_loss: 0,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function mapMetrics(m: PlatformMetrics): DeploymentMetrics {
|
|
108
|
+
const gp = m.gross_profit || 0;
|
|
109
|
+
const gl = m.gross_loss || 0;
|
|
110
|
+
return {
|
|
111
|
+
total_pnl: m.total_pnl ?? 0, realized_pnl: m.realized_pnl ?? 0,
|
|
112
|
+
unrealized_pnl: m.unrealized_pnl ?? 0, total_exposure: m.total_exposure ?? 0,
|
|
113
|
+
position_count: m.position_count ?? 0, open_order_count: m.open_order_count ?? 0,
|
|
114
|
+
win_rate: m.win_rate ?? 0, total_trades: m.total_trades ?? 0,
|
|
115
|
+
max_drawdown_pct: m.max_drawdown_pct ?? 0, sharpe_ratio: 0,
|
|
116
|
+
profit_factor: gl > 0 ? gp / gl : 0, avg_return_per_trade: m.avg_return_per_trade ?? 0,
|
|
117
|
+
gross_profit: gp, gross_loss: gl,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function mapPositions(raw: PlatformMetrics["positions"]): Position[] {
|
|
122
|
+
return (raw ?? []).map((p) => ({
|
|
123
|
+
market_id: p.market_id ?? "", slug: p.slug ?? "", question: p.question ?? "",
|
|
124
|
+
side: p.side, size: p.size, avg_entry_price: p.avg_entry_price,
|
|
125
|
+
cost_basis: p.cost_basis, realized_pnl: p.realized_pnl, unrealized_pnl: p.unrealized_pnl,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function mapOrders(raw: PlatformMetrics["recent_orders"]): Order[] {
|
|
130
|
+
return (raw ?? []).map((o) => ({
|
|
131
|
+
market_id: o.market_id ?? "", slug: o.slug ?? "", side: o.side,
|
|
132
|
+
price: o.price, size: o.size, filled_size: o.filled_size,
|
|
133
|
+
status: o.status, order_type: o.order_type,
|
|
134
|
+
created_at: new Date(o.created_at).getTime(),
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function inferType(strat: PlatformStrategy): string {
|
|
139
|
+
const name = strat.name.toLowerCase();
|
|
140
|
+
if (name.includes("mm") || name.includes("market")) return "market_making";
|
|
141
|
+
if (name.includes("arb")) return "arbitrage";
|
|
142
|
+
if (name.includes("direct")) return "directional";
|
|
143
|
+
if (name.includes("event") || name.includes("snip") || name.includes("bond")) return "event_driven";
|
|
144
|
+
if (name.includes("moment") || name.includes("trend") || name.includes("fade")) return "momentum";
|
|
145
|
+
if (name.includes("mean") || name.includes("revert")) return "mean_reversion";
|
|
146
|
+
return "custom";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const platformSync = new PlatformSync();
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Tier configuration — plans, models, budgets, rate limits
|
|
2
|
+
|
|
3
|
+
export type PlanTier = "free" | "pro" | "ultra";
|
|
4
|
+
export type ModelPower = "fast" | "standard" | "pro" | "ultra";
|
|
5
|
+
|
|
6
|
+
export interface TierConfig {
|
|
7
|
+
tokenBudget: number; // weighted tokens per 5-hour window
|
|
8
|
+
ratePerMinute: number;
|
|
9
|
+
allowedModels: ModelPower[];
|
|
10
|
+
defaultModel: ModelPower;
|
|
11
|
+
overflowToFast: boolean; // when budget exhausted, allow unlimited Fast
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// OpenRouter model IDs
|
|
15
|
+
export const MODEL_IDS: Record<ModelPower, string> = {
|
|
16
|
+
fast: "openrouter/hunter-alpha",
|
|
17
|
+
standard: "anthropic/claude-haiku-4.5",
|
|
18
|
+
pro: "anthropic/claude-sonnet-4.6",
|
|
19
|
+
ultra: "anthropic/claude-opus-4.6",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Higher models drain budget faster
|
|
23
|
+
export const MODEL_MULTIPLIER: Record<ModelPower, number> = {
|
|
24
|
+
fast: 0.25,
|
|
25
|
+
standard: 1,
|
|
26
|
+
pro: 2,
|
|
27
|
+
ultra: 4,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const TIER_CONFIG: Record<PlanTier, TierConfig> = {
|
|
31
|
+
free: {
|
|
32
|
+
tokenBudget: 50_000,
|
|
33
|
+
ratePerMinute: 5,
|
|
34
|
+
allowedModels: ["fast"],
|
|
35
|
+
defaultModel: "fast",
|
|
36
|
+
overflowToFast: false, // free users get blocked
|
|
37
|
+
},
|
|
38
|
+
pro: {
|
|
39
|
+
tokenBudget: 500_000,
|
|
40
|
+
ratePerMinute: 30,
|
|
41
|
+
allowedModels: ["fast", "standard", "pro"],
|
|
42
|
+
defaultModel: "standard",
|
|
43
|
+
overflowToFast: true, // overflow to unlimited Fast
|
|
44
|
+
},
|
|
45
|
+
ultra: {
|
|
46
|
+
tokenBudget: 5_000_000,
|
|
47
|
+
ratePerMinute: 120,
|
|
48
|
+
allowedModels: ["fast", "standard", "pro", "ultra"],
|
|
49
|
+
defaultModel: "pro",
|
|
50
|
+
overflowToFast: true, // overflow to unlimited Fast
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const WINDOW_SECONDS = 5 * 60 * 60; // 18000 = 5 hours
|
|
55
|
+
|
|
56
|
+
export function getWindowBounds(now: Date = new Date()): { start: Date; end: Date } {
|
|
57
|
+
const epoch = Math.floor(now.getTime() / 1000);
|
|
58
|
+
const windowStart = Math.floor(epoch / WINDOW_SECONDS) * WINDOW_SECONDS;
|
|
59
|
+
return {
|
|
60
|
+
start: new Date(windowStart * 1000),
|
|
61
|
+
end: new Date((windowStart + WINDOW_SECONDS) * 1000),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function formatTimeRemaining(ms: number): string {
|
|
66
|
+
if (ms <= 0) return "now";
|
|
67
|
+
const h = Math.floor(ms / 3_600_000);
|
|
68
|
+
const m = Math.ceil((ms % 3_600_000) / 60_000);
|
|
69
|
+
if (h > 0) return `${h}h ${m}m`;
|
|
70
|
+
return `${m}m`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function resolveModel(tier: PlanTier, power: ModelPower, budgetExhausted: boolean): {
|
|
74
|
+
modelId: string;
|
|
75
|
+
effectivePower: ModelPower;
|
|
76
|
+
downgraded: boolean;
|
|
77
|
+
} {
|
|
78
|
+
const config = TIER_CONFIG[tier];
|
|
79
|
+
|
|
80
|
+
// Budget exhausted: paid users overflow to fast, free users blocked
|
|
81
|
+
if (budgetExhausted) {
|
|
82
|
+
if (config.overflowToFast) {
|
|
83
|
+
return { modelId: MODEL_IDS.fast, effectivePower: "fast", downgraded: true };
|
|
84
|
+
}
|
|
85
|
+
// Free users: handled by caller (blocked)
|
|
86
|
+
return { modelId: MODEL_IDS.fast, effectivePower: "fast", downgraded: true };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Model not allowed on this tier: downgrade to highest allowed
|
|
90
|
+
if (!config.allowedModels.includes(power)) {
|
|
91
|
+
const best = config.allowedModels[config.allowedModels.length - 1]!;
|
|
92
|
+
return { modelId: MODEL_IDS[best], effectivePower: best, downgraded: true };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { modelId: MODEL_IDS[power], effectivePower: power, downgraded: false };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Tools that require Pro or higher — free users don't see them
|
|
99
|
+
export const PREMIUM_TOOLS: Record<string, PlanTier> = {
|
|
100
|
+
newsSentiment: "pro",
|
|
101
|
+
calaKnowledge: "pro",
|
|
102
|
+
};
|
|
103
|
+
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { platform } from "./client.ts";
|
|
4
|
+
|
|
5
|
+
const t = tool as any;
|
|
6
|
+
|
|
7
|
+
export const portfolioTools: Record<string, any> = {
|
|
8
|
+
list_strategies: t({
|
|
9
|
+
description: "List all the user's strategies with their status (draft, validated, deployed, stopped, error).",
|
|
10
|
+
parameters: z.object({}),
|
|
11
|
+
execute: async () => {
|
|
12
|
+
const strategies = await platform.listStrategies();
|
|
13
|
+
return strategies.map((s) => ({
|
|
14
|
+
id: s.id,
|
|
15
|
+
name: s.name,
|
|
16
|
+
description: s.description,
|
|
17
|
+
status: s.status,
|
|
18
|
+
class_name: s.class_name,
|
|
19
|
+
circuit_breaker: s.circuit_breaker_enabled,
|
|
20
|
+
created_at: s.created_at,
|
|
21
|
+
updated_at: s.updated_at,
|
|
22
|
+
}));
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
|
|
26
|
+
get_strategy: t({
|
|
27
|
+
description: "Get detailed info about a specific strategy including its parameters, risk config, and code.",
|
|
28
|
+
parameters: z.object({
|
|
29
|
+
strategy_id: z.string().describe("Strategy UUID"),
|
|
30
|
+
}),
|
|
31
|
+
execute: async (args: any) => platform.getStrategy(args.strategy_id),
|
|
32
|
+
}),
|
|
33
|
+
|
|
34
|
+
list_deployments: t({
|
|
35
|
+
description: "List all deployments for a strategy — active, stopped, errored. Shows status, dry_run (paper/live), mode, timestamps.",
|
|
36
|
+
parameters: z.object({
|
|
37
|
+
strategy_id: z.string().describe("Strategy UUID"),
|
|
38
|
+
}),
|
|
39
|
+
execute: async (args: any) => {
|
|
40
|
+
const deps = await platform.listDeployments(args.strategy_id);
|
|
41
|
+
return deps.map((d) => ({
|
|
42
|
+
id: d.id,
|
|
43
|
+
strategy_id: d.strategy_id,
|
|
44
|
+
status: d.status,
|
|
45
|
+
dry_run: d.dry_run,
|
|
46
|
+
mode: d.deployment_mode,
|
|
47
|
+
error: d.error_message,
|
|
48
|
+
started_at: d.started_at,
|
|
49
|
+
stopped_at: d.stopped_at,
|
|
50
|
+
}));
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
|
|
54
|
+
get_metrics: t({
|
|
55
|
+
description: "Get the latest performance metrics for a strategy's active deployment: P&L, positions, orders, win rate, drawdown, exposure.",
|
|
56
|
+
parameters: z.object({
|
|
57
|
+
strategy_id: z.string().describe("Strategy UUID"),
|
|
58
|
+
limit: z.number().optional().describe("Number of historical snapshots (default 10)"),
|
|
59
|
+
}),
|
|
60
|
+
execute: async (args: any) => {
|
|
61
|
+
const data = await platform.getMetrics(args.strategy_id, args.limit ?? 10);
|
|
62
|
+
return {
|
|
63
|
+
latest: data.latest ? {
|
|
64
|
+
total_pnl: data.latest.total_pnl,
|
|
65
|
+
realized_pnl: data.latest.realized_pnl,
|
|
66
|
+
unrealized_pnl: data.latest.unrealized_pnl,
|
|
67
|
+
total_exposure: data.latest.total_exposure,
|
|
68
|
+
position_count: data.latest.position_count,
|
|
69
|
+
open_order_count: data.latest.open_order_count,
|
|
70
|
+
win_rate: data.latest.win_rate,
|
|
71
|
+
total_trades: data.latest.total_trades,
|
|
72
|
+
max_drawdown_pct: data.latest.max_drawdown_pct,
|
|
73
|
+
gross_profit: data.latest.gross_profit,
|
|
74
|
+
gross_loss: data.latest.gross_loss,
|
|
75
|
+
positions: data.latest.positions,
|
|
76
|
+
recent_orders: data.latest.recent_orders,
|
|
77
|
+
} : null,
|
|
78
|
+
history_count: data.history.length,
|
|
79
|
+
pnl_history: data.history.map((h) => ({
|
|
80
|
+
total_pnl: h.total_pnl,
|
|
81
|
+
created_at: h.created_at,
|
|
82
|
+
})),
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
|
|
87
|
+
get_logs: t({
|
|
88
|
+
description: "Get recent deployment logs for a strategy — stdout/stderr from the running bot.",
|
|
89
|
+
parameters: z.object({
|
|
90
|
+
strategy_id: z.string().describe("Strategy UUID"),
|
|
91
|
+
limit: z.number().optional().describe("Number of log entries (default 50)"),
|
|
92
|
+
}),
|
|
93
|
+
execute: async (args: any) => platform.getLogs(args.strategy_id, args.limit ?? 50),
|
|
94
|
+
}),
|
|
95
|
+
|
|
96
|
+
deploy_strategy: t({
|
|
97
|
+
description: "Deploy a strategy to the Horizon cloud. Call list_credentials first to get a credential_id. Always start with dry_run=true (paper mode).",
|
|
98
|
+
parameters: z.object({
|
|
99
|
+
strategy_id: z.string().describe("Strategy UUID (from save_strategy result)"),
|
|
100
|
+
credential_id: z.string().describe("Exchange credential UUID (from list_credentials)"),
|
|
101
|
+
dry_run: z.boolean().describe("true for paper trading (recommended), false for live"),
|
|
102
|
+
}),
|
|
103
|
+
execute: async (args: any) => {
|
|
104
|
+
if (!args.dry_run) {
|
|
105
|
+
return { error: "Live mode requires explicit confirmation. Call with dry_run=true first, then ask the user to confirm before switching to live." };
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return await platform.deploy(args.strategy_id, {
|
|
109
|
+
credentialId: args.credential_id,
|
|
110
|
+
dryRun: args.dry_run,
|
|
111
|
+
});
|
|
112
|
+
} catch (e: any) { return { error: e.message }; }
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
|
|
116
|
+
stop_strategy: t({
|
|
117
|
+
description: "Stop an active cloud deployment. Sends SIGTERM to the running process.",
|
|
118
|
+
parameters: z.object({
|
|
119
|
+
strategy_id: z.string().describe("Strategy UUID"),
|
|
120
|
+
}),
|
|
121
|
+
execute: async (args: any) => {
|
|
122
|
+
try { return await platform.stop(args.strategy_id); }
|
|
123
|
+
catch (e: any) { return { error: e.message }; }
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
|
|
127
|
+
list_credentials: t({
|
|
128
|
+
description: "List the user's saved exchange credentials (wallet keys). Needed before deploy — provides the credential_id.",
|
|
129
|
+
parameters: z.object({}),
|
|
130
|
+
execute: async () => {
|
|
131
|
+
try {
|
|
132
|
+
const creds = await platform.listCredentials();
|
|
133
|
+
if (creds.length === 0) {
|
|
134
|
+
return { credentials: [], hint: "No credentials found. The user needs to add one with add_credential or from the platform web UI." };
|
|
135
|
+
}
|
|
136
|
+
return { credentials: creds.map((c) => ({ id: c.id, label: c.label, exchange: c.exchange, wallet: c.wallet_address })) };
|
|
137
|
+
} catch (e: any) { return { error: e.message }; }
|
|
138
|
+
},
|
|
139
|
+
}),
|
|
140
|
+
|
|
141
|
+
add_credential: t({
|
|
142
|
+
description: "Add an exchange credential. Tell the user to run `/credential` in the TUI — private keys must NEVER be typed in the chat (they would be sent to the LLM). This tool only explains the process.",
|
|
143
|
+
parameters: z.object({}),
|
|
144
|
+
execute: async () => {
|
|
145
|
+
return {
|
|
146
|
+
message: "For security, private keys cannot be added through chat. Tell the user to run the /credential command, which prompts for the key directly without sending it to the AI.",
|
|
147
|
+
hint: "Type: /credential add <label> <exchange> — then paste the private key when prompted.",
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
|
|
152
|
+
validate_code: t({
|
|
153
|
+
description: "Validate strategy code on the platform's server-side validator.",
|
|
154
|
+
parameters: z.object({
|
|
155
|
+
code: z.string().describe("Python strategy code to validate"),
|
|
156
|
+
}),
|
|
157
|
+
execute: async (args: any) => {
|
|
158
|
+
try { return await platform.validateCode(args.code); }
|
|
159
|
+
catch (e: any) { return { error: e.message }; }
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
|
|
163
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export interface PlatformStrategy {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
status: "draft" | "validated" | "deployed" | "stopped" | "error";
|
|
6
|
+
class_name: string | null;
|
|
7
|
+
params: Record<string, unknown>;
|
|
8
|
+
sdk_risk_config: SdkRiskConfig | null;
|
|
9
|
+
circuit_breaker_enabled: boolean;
|
|
10
|
+
circuit_breaker_max_drawdown: number | null;
|
|
11
|
+
created_at: string;
|
|
12
|
+
updated_at: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SdkRiskConfig {
|
|
16
|
+
max_position: number;
|
|
17
|
+
max_position_per_market: number;
|
|
18
|
+
max_notional: number;
|
|
19
|
+
max_drawdown_pct: number;
|
|
20
|
+
max_order_size: number;
|
|
21
|
+
rate_limit: number;
|
|
22
|
+
rate_burst: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PlatformDeployment {
|
|
26
|
+
id: string;
|
|
27
|
+
strategy_id: string;
|
|
28
|
+
credential_id: string;
|
|
29
|
+
status: string;
|
|
30
|
+
dry_run: boolean;
|
|
31
|
+
deployment_mode: "manual" | "scanner";
|
|
32
|
+
worker_process_id: string | null;
|
|
33
|
+
error_message: string | null;
|
|
34
|
+
started_at: string | null;
|
|
35
|
+
stopped_at: string | null;
|
|
36
|
+
created_at: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PlatformMetrics {
|
|
40
|
+
total_pnl: number;
|
|
41
|
+
unrealized_pnl: number;
|
|
42
|
+
realized_pnl: number;
|
|
43
|
+
total_exposure: number;
|
|
44
|
+
position_count: number;
|
|
45
|
+
open_order_count: number;
|
|
46
|
+
win_rate: number;
|
|
47
|
+
total_trades: number;
|
|
48
|
+
gross_profit: number;
|
|
49
|
+
gross_loss: number;
|
|
50
|
+
max_drawdown_pct: number;
|
|
51
|
+
avg_return_per_trade: number;
|
|
52
|
+
positions: PlatformPosition[];
|
|
53
|
+
recent_orders: PlatformOrder[];
|
|
54
|
+
created_at: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface PlatformPosition {
|
|
58
|
+
market_id: string;
|
|
59
|
+
slug: string;
|
|
60
|
+
question: string;
|
|
61
|
+
side: "BUY" | "SELL";
|
|
62
|
+
size: number;
|
|
63
|
+
avg_entry_price: number;
|
|
64
|
+
cost_basis: number;
|
|
65
|
+
realized_pnl: number;
|
|
66
|
+
unrealized_pnl: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface PlatformOrder {
|
|
70
|
+
market_id: string;
|
|
71
|
+
slug: string;
|
|
72
|
+
side: "BUY" | "SELL";
|
|
73
|
+
price: number;
|
|
74
|
+
size: number;
|
|
75
|
+
filled_size: number;
|
|
76
|
+
status: "PENDING" | "LIVE" | "PARTIALLY_FILLED" | "FILLED" | "CANCELLED";
|
|
77
|
+
order_type: "GTC" | "IOC";
|
|
78
|
+
created_at: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface PlatformLog {
|
|
82
|
+
level: string;
|
|
83
|
+
message: string;
|
|
84
|
+
source: string;
|
|
85
|
+
logged_at: string;
|
|
86
|
+
}
|