opentradex 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 (70) hide show
  1. package/.env.example +8 -0
  2. package/CLAUDE.md +98 -0
  3. package/README.md +246 -0
  4. package/SOUL.md +79 -0
  5. package/SPEC.md +317 -0
  6. package/SUBMISSION.md +30 -0
  7. package/architecture.excalidraw +170 -0
  8. package/architecture.png +0 -0
  9. package/bin/opentradex.mjs +4 -0
  10. package/data/.gitkeep +0 -0
  11. package/data/strategy_notes.md +158 -0
  12. package/gossip/__init__.py +0 -0
  13. package/gossip/dashboard.py +150 -0
  14. package/gossip/db.py +358 -0
  15. package/gossip/kalshi.py +492 -0
  16. package/gossip/news.py +235 -0
  17. package/gossip/trader.py +646 -0
  18. package/main.py +287 -0
  19. package/package.json +47 -0
  20. package/requirements.txt +7 -0
  21. package/src/cli.mjs +124 -0
  22. package/src/index.mjs +420 -0
  23. package/web/AGENTS.md +5 -0
  24. package/web/CLAUDE.md +1 -0
  25. package/web/README.md +36 -0
  26. package/web/components.json +25 -0
  27. package/web/eslint.config.mjs +18 -0
  28. package/web/next.config.ts +7 -0
  29. package/web/package-lock.json +11626 -0
  30. package/web/package.json +37 -0
  31. package/web/postcss.config.mjs +7 -0
  32. package/web/public/file.svg +1 -0
  33. package/web/public/globe.svg +1 -0
  34. package/web/public/next.svg +1 -0
  35. package/web/public/vercel.svg +1 -0
  36. package/web/public/window.svg +1 -0
  37. package/web/src/app/api/agent/route.ts +77 -0
  38. package/web/src/app/api/agent/stream/route.ts +87 -0
  39. package/web/src/app/api/markets/route.ts +15 -0
  40. package/web/src/app/api/news/live/route.ts +77 -0
  41. package/web/src/app/api/news/reddit/route.ts +118 -0
  42. package/web/src/app/api/news/route.ts +10 -0
  43. package/web/src/app/api/news/tiktok/route.ts +115 -0
  44. package/web/src/app/api/news/truthsocial/route.ts +116 -0
  45. package/web/src/app/api/news/twitter/route.ts +186 -0
  46. package/web/src/app/api/portfolio/route.ts +50 -0
  47. package/web/src/app/api/prices/route.ts +18 -0
  48. package/web/src/app/api/trades/route.ts +10 -0
  49. package/web/src/app/favicon.ico +0 -0
  50. package/web/src/app/globals.css +170 -0
  51. package/web/src/app/layout.tsx +36 -0
  52. package/web/src/app/page.tsx +366 -0
  53. package/web/src/components/AgentLog.tsx +71 -0
  54. package/web/src/components/LiveStream.tsx +394 -0
  55. package/web/src/components/MarketScanner.tsx +111 -0
  56. package/web/src/components/NewsFeed.tsx +561 -0
  57. package/web/src/components/PortfolioStrip.tsx +139 -0
  58. package/web/src/components/PositionsPanel.tsx +219 -0
  59. package/web/src/components/TopBar.tsx +127 -0
  60. package/web/src/components/ui/badge.tsx +52 -0
  61. package/web/src/components/ui/button.tsx +60 -0
  62. package/web/src/components/ui/card.tsx +103 -0
  63. package/web/src/components/ui/scroll-area.tsx +55 -0
  64. package/web/src/components/ui/separator.tsx +25 -0
  65. package/web/src/components/ui/tabs.tsx +82 -0
  66. package/web/src/components/ui/tooltip.tsx +66 -0
  67. package/web/src/lib/db.ts +81 -0
  68. package/web/src/lib/types.ts +130 -0
  69. package/web/src/lib/utils.ts +6 -0
  70. package/web/tsconfig.json +34 -0
@@ -0,0 +1,394 @@
1
+ "use client";
2
+
3
+ import { useRef, useEffect, useState } from "react";
4
+ import { ScrollArea } from "@/components/ui/scroll-area";
5
+ import ReactMarkdown from "react-markdown";
6
+ import remarkGfm from "remark-gfm";
7
+ import {
8
+ Send,
9
+ Terminal,
10
+ ChevronRight,
11
+ ChevronDown,
12
+ CheckCircle2,
13
+ } from "lucide-react";
14
+ import type { StreamLine } from "@/lib/types";
15
+
16
+ interface LiveStreamProps {
17
+ lines: StreamLine[];
18
+ liveStatus: string;
19
+ customPrompt: string;
20
+ onCustomPromptChange: (v: string) => void;
21
+ onSendCommand: (prompt: string) => void;
22
+ }
23
+
24
+ export function LiveStream({
25
+ lines,
26
+ liveStatus,
27
+ customPrompt,
28
+ onCustomPromptChange,
29
+ onSendCommand,
30
+ }: LiveStreamProps) {
31
+ const endRef = useRef<HTMLDivElement>(null);
32
+ const prevLinesCount = useRef(0);
33
+
34
+ useEffect(() => {
35
+ if (lines.length > 0 && lines.length !== prevLinesCount.current) {
36
+ endRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
37
+ }
38
+ prevLinesCount.current = lines.length;
39
+ }, [lines.length]);
40
+
41
+ const grouped = groupLines(lines);
42
+
43
+ return (
44
+ <div className="flex flex-col h-full">
45
+ <div className="h-9 flex items-center justify-between px-3 border-b border-border bg-card shrink-0">
46
+ <div className="flex items-center gap-2">
47
+ <Terminal className="h-3.5 w-3.5 text-muted-foreground" />
48
+ <span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
49
+ Live Stream
50
+ </span>
51
+ </div>
52
+ {liveStatus === "running" && (
53
+ <span className="text-[10px] text-primary font-medium flex items-center gap-1.5">
54
+ <span className="w-1.5 h-1.5 rounded-full bg-primary pulse-glow" />
55
+ Streaming
56
+ </span>
57
+ )}
58
+ </div>
59
+
60
+ <ScrollArea className="flex-1 min-h-0">
61
+ <div className="p-3 space-y-0.5">
62
+ {lines.length === 0 && liveStatus !== "running" && (
63
+ <p className="text-muted-foreground/40 text-center py-12 text-xs">
64
+ Run a cycle to see live agent output
65
+ </p>
66
+ )}
67
+ {lines.length === 0 && liveStatus === "running" && (
68
+ <div className="flex items-center gap-2 justify-center py-12">
69
+ <div className="h-1.5 w-1.5 rounded-full bg-primary animate-bounce [animation-delay:0ms]" />
70
+ <div className="h-1.5 w-1.5 rounded-full bg-primary animate-bounce [animation-delay:150ms]" />
71
+ <div className="h-1.5 w-1.5 rounded-full bg-primary animate-bounce [animation-delay:300ms]" />
72
+ </div>
73
+ )}
74
+ {grouped.map((group, i) => (
75
+ <GroupedBlock key={i} group={group} />
76
+ ))}
77
+ <div ref={endRef} />
78
+ </div>
79
+ </ScrollArea>
80
+
81
+ <div className="h-11 flex items-center gap-2 px-3 border-t border-border bg-card shrink-0">
82
+ <span className="text-muted-foreground/30 text-xs font-mono">
83
+ {">"}
84
+ </span>
85
+ <input
86
+ type="text"
87
+ value={customPrompt}
88
+ onChange={(e) => onCustomPromptChange(e.target.value)}
89
+ onKeyDown={(e) => {
90
+ if (e.key === "Enter" && customPrompt.trim()) {
91
+ onSendCommand(customPrompt);
92
+ onCustomPromptChange("");
93
+ }
94
+ }}
95
+ placeholder="Send a command to the agent..."
96
+ className="flex-1 bg-transparent text-xs focus:outline-none placeholder:text-muted-foreground/30"
97
+ />
98
+ <button
99
+ onClick={() => {
100
+ if (customPrompt.trim()) {
101
+ onSendCommand(customPrompt);
102
+ onCustomPromptChange("");
103
+ }
104
+ }}
105
+ className="text-muted-foreground/40 hover:text-primary transition-colors"
106
+ >
107
+ <Send className="h-3.5 w-3.5" />
108
+ </button>
109
+ </div>
110
+ </div>
111
+ );
112
+ }
113
+
114
+ type GroupedLine =
115
+ | { kind: "text"; text: string }
116
+ | { kind: "tools"; tools: { tool: StreamLine; result?: StreamLine }[] }
117
+ | { kind: "result"; result: string };
118
+
119
+ function groupLines(lines: StreamLine[]): GroupedLine[] {
120
+ const out: GroupedLine[] = [];
121
+ let textBuffer = "";
122
+ let toolBuffer: { tool: StreamLine; result?: StreamLine }[] = [];
123
+
124
+ const flushText = () => {
125
+ if (textBuffer.trim()) {
126
+ out.push({ kind: "text", text: textBuffer.trim() });
127
+ }
128
+ textBuffer = "";
129
+ };
130
+
131
+ const flushTools = () => {
132
+ if (toolBuffer.length > 0) {
133
+ out.push({ kind: "tools", tools: [...toolBuffer] });
134
+ toolBuffer = [];
135
+ }
136
+ };
137
+
138
+ for (let i = 0; i < lines.length; i++) {
139
+ const line = lines[i];
140
+ if (line.type === "text") {
141
+ flushTools();
142
+ textBuffer += (textBuffer ? "\n" : "") + (line.text || "");
143
+ } else if (line.type === "tool_use") {
144
+ flushText();
145
+ const next = lines[i + 1];
146
+ const result = next?.type === "tool_result" ? next : undefined;
147
+ toolBuffer.push({ tool: line, result });
148
+ if (result) i++;
149
+ } else if (line.type === "tool_result") {
150
+ // orphan
151
+ } else if (line.type === "result") {
152
+ flushText();
153
+ flushTools();
154
+ out.push({ kind: "result", result: line.result || "" });
155
+ }
156
+ }
157
+ flushText();
158
+ flushTools();
159
+ return out;
160
+ }
161
+
162
+ function GroupedBlock({ group }: { group: GroupedLine }) {
163
+ if (group.kind === "text") return <TextBlock text={group.text} />;
164
+ if (group.kind === "tools") return <ToolGroup tools={group.tools} />;
165
+ if (group.kind === "result") return <ResultBlock text={group.result} />;
166
+ return null;
167
+ }
168
+
169
+ function TextBlock({ text }: { text: string }) {
170
+ return (
171
+ <div className="py-1 px-1">
172
+ <ReactMarkdown
173
+ remarkPlugins={[remarkGfm]}
174
+ components={markdownComponents}
175
+ >
176
+ {text}
177
+ </ReactMarkdown>
178
+ </div>
179
+ );
180
+ }
181
+
182
+ function ResultBlock({ text }: { text: string }) {
183
+ return (
184
+ <div className="my-2 rounded-lg border border-primary/20 bg-primary/5 overflow-hidden">
185
+ <div className="flex items-center gap-2 px-3 py-1.5 border-b border-primary/10">
186
+ <CheckCircle2 className="h-3 w-3 text-primary" />
187
+ <span className="text-[10px] text-primary font-semibold uppercase tracking-wider">
188
+ Cycle Complete
189
+ </span>
190
+ </div>
191
+ <div className="p-3">
192
+ <ReactMarkdown
193
+ remarkPlugins={[remarkGfm]}
194
+ components={markdownComponents}
195
+ >
196
+ {text}
197
+ </ReactMarkdown>
198
+ </div>
199
+ </div>
200
+ );
201
+ }
202
+
203
+ function ToolGroup({
204
+ tools,
205
+ }: {
206
+ tools: { tool: StreamLine; result?: StreamLine }[];
207
+ }) {
208
+ const [expanded, setExpanded] = useState(false);
209
+
210
+ const summary = tools
211
+ .map((t) => {
212
+ const name = t.tool.tool || "tool";
213
+ const short = formatToolInput(t.tool.input || "");
214
+ return { name, short };
215
+ });
216
+
217
+ return (
218
+ <div className="my-0.5">
219
+ <button
220
+ onClick={() => setExpanded(!expanded)}
221
+ className="w-full flex items-start gap-1.5 px-1 py-0.5 text-left hover:bg-secondary/20 rounded transition-colors group"
222
+ >
223
+ <span className="text-muted-foreground/30 mt-px shrink-0">
224
+ {expanded ? (
225
+ <ChevronDown className="h-3 w-3" />
226
+ ) : (
227
+ <ChevronRight className="h-3 w-3" />
228
+ )}
229
+ </span>
230
+ <div className="flex flex-wrap gap-x-3 gap-y-0 flex-1 min-w-0">
231
+ {summary.map((s, i) => (
232
+ <span key={i} className="flex items-center gap-1 text-[10px] text-muted-foreground/50 font-mono shrink-0">
233
+ <span className="text-primary/50 font-medium">{s.name}</span>
234
+ <span className="truncate max-w-[200px]">{s.short}</span>
235
+ {tools[i].result && (
236
+ <CheckCircle2 className="h-2.5 w-2.5 text-primary/30 shrink-0" />
237
+ )}
238
+ </span>
239
+ ))}
240
+ </div>
241
+ </button>
242
+
243
+ {expanded && (
244
+ <div className="ml-4 mt-1 space-y-1 mb-1">
245
+ {tools.map((t, i) => (
246
+ <ToolDetail key={i} tool={t.tool} result={t.result} />
247
+ ))}
248
+ </div>
249
+ )}
250
+ </div>
251
+ );
252
+ }
253
+
254
+ function ToolDetail({
255
+ tool,
256
+ result,
257
+ }: {
258
+ tool: StreamLine;
259
+ result?: StreamLine;
260
+ }) {
261
+ const resultText = result?.text;
262
+ const isNoOutput =
263
+ resultText === "(Bash completed with no output)" || !resultText;
264
+
265
+ return (
266
+ <div className="rounded border border-border/30 bg-background/30 text-[10px]">
267
+ {tool.input && (
268
+ <pre className="px-2.5 py-1.5 font-mono text-muted-foreground/50 whitespace-pre-wrap break-all leading-relaxed">
269
+ {tool.input}
270
+ </pre>
271
+ )}
272
+ {resultText && !isNoOutput && (
273
+ <pre className="px-2.5 py-1.5 font-mono text-muted-foreground/40 whitespace-pre-wrap break-all max-h-32 overflow-y-auto leading-relaxed border-t border-border/20">
274
+ {resultText}
275
+ </pre>
276
+ )}
277
+ </div>
278
+ );
279
+ }
280
+
281
+ const markdownComponents = {
282
+ h1: ({ children }: { children?: React.ReactNode }) => (
283
+ <h1 className="text-[13px] font-bold text-foreground mt-2.5 mb-1 first:mt-0">
284
+ {children}
285
+ </h1>
286
+ ),
287
+ h2: ({ children }: { children?: React.ReactNode }) => (
288
+ <h2 className="text-[12px] font-bold text-foreground mt-2 mb-1 first:mt-0">
289
+ {children}
290
+ </h2>
291
+ ),
292
+ h3: ({ children }: { children?: React.ReactNode }) => (
293
+ <h3 className="text-[11px] font-semibold text-foreground/90 mt-2 mb-0.5 first:mt-0">
294
+ {children}
295
+ </h3>
296
+ ),
297
+ p: ({ children }: { children?: React.ReactNode }) => (
298
+ <p className="text-[12px] text-foreground/80 leading-relaxed mb-1 last:mb-0">
299
+ {children}
300
+ </p>
301
+ ),
302
+ strong: ({ children }: { children?: React.ReactNode }) => (
303
+ <strong className="font-semibold text-foreground">{children}</strong>
304
+ ),
305
+ em: ({ children }: { children?: React.ReactNode }) => (
306
+ <em className="text-foreground/60">{children}</em>
307
+ ),
308
+ ul: ({ children }: { children?: React.ReactNode }) => (
309
+ <ul className="text-[12px] text-foreground/80 ml-3 mb-1 space-y-0 list-disc list-outside">
310
+ {children}
311
+ </ul>
312
+ ),
313
+ ol: ({ children }: { children?: React.ReactNode }) => (
314
+ <ol className="text-[12px] text-foreground/80 ml-3 mb-1 space-y-0 list-decimal list-outside">
315
+ {children}
316
+ </ol>
317
+ ),
318
+ li: ({ children }: { children?: React.ReactNode }) => (
319
+ <li className="leading-relaxed">{children}</li>
320
+ ),
321
+ a: ({ href, children }: { href?: string; children?: React.ReactNode }) => (
322
+ <a
323
+ href={href}
324
+ target="_blank"
325
+ rel="noopener noreferrer"
326
+ className="text-primary/80 hover:text-primary underline underline-offset-2 transition-colors"
327
+ >
328
+ {children}
329
+ </a>
330
+ ),
331
+ code: ({ children, className }: { children?: React.ReactNode; className?: string }) => {
332
+ if (className?.includes("language-")) {
333
+ return (
334
+ <code className="block bg-background/50 border border-border/30 rounded px-2.5 py-1.5 text-[10px] font-mono text-foreground/60 overflow-x-auto my-1 whitespace-pre">
335
+ {children}
336
+ </code>
337
+ );
338
+ }
339
+ return (
340
+ <code className="bg-background/50 rounded px-1 py-0.5 text-[10px] font-mono text-primary/70">
341
+ {children}
342
+ </code>
343
+ );
344
+ },
345
+ pre: ({ children }: { children?: React.ReactNode }) => (
346
+ <pre className="my-1">{children}</pre>
347
+ ),
348
+ blockquote: ({ children }: { children?: React.ReactNode }) => (
349
+ <blockquote className="border-l-2 border-primary/20 pl-2.5 my-1 text-foreground/50 italic">
350
+ {children}
351
+ </blockquote>
352
+ ),
353
+ table: ({ children }: { children?: React.ReactNode }) => (
354
+ <div className="overflow-x-auto my-1.5 rounded border border-border/30">
355
+ <table className="w-full text-[10px]">{children}</table>
356
+ </div>
357
+ ),
358
+ thead: ({ children }: { children?: React.ReactNode }) => (
359
+ <thead className="bg-secondary/30 border-b border-border/30">
360
+ {children}
361
+ </thead>
362
+ ),
363
+ th: ({ children }: { children?: React.ReactNode }) => (
364
+ <th className="text-left px-2 py-1 font-semibold text-foreground/70 text-[10px]">
365
+ {children}
366
+ </th>
367
+ ),
368
+ td: ({ children }: { children?: React.ReactNode }) => (
369
+ <td className="px-2 py-1 text-foreground/60 border-t border-border/15 font-mono">
370
+ {children}
371
+ </td>
372
+ ),
373
+ hr: () => <hr className="border-border/20 my-2" />,
374
+ };
375
+
376
+ function formatToolInput(raw: string): string {
377
+ try {
378
+ const parsed = JSON.parse(raw);
379
+ if (parsed.command) {
380
+ const cmd = parsed.command as string;
381
+ return cmd.length > 60 ? cmd.slice(0, 60) + "..." : cmd;
382
+ }
383
+ if (parsed.query) return parsed.query;
384
+ if (parsed.file_path) {
385
+ const fp = parsed.file_path as string;
386
+ const parts = fp.split("/");
387
+ return parts.length > 2 ? ".../" + parts.slice(-2).join("/") : fp;
388
+ }
389
+ if (parsed.pattern) return parsed.pattern;
390
+ return raw.length > 60 ? raw.slice(0, 60) + "..." : raw;
391
+ } catch {
392
+ return raw.length > 60 ? raw.slice(0, 60) + "..." : raw;
393
+ }
394
+ }
@@ -0,0 +1,111 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { ScrollArea } from "@/components/ui/scroll-area";
5
+ import { ArrowUpDown } from "lucide-react";
6
+ import type { Market } from "@/lib/types";
7
+ import { kalshiUrl } from "@/lib/types";
8
+
9
+ interface MarketScannerProps {
10
+ markets: Market[];
11
+ }
12
+
13
+ type SortKey = "volume" | "mid" | "ticker";
14
+
15
+ export function MarketScanner({ markets }: MarketScannerProps) {
16
+ const [sortBy, setSortBy] = useState<SortKey>("volume");
17
+
18
+ const sorted = [...markets].sort((a, b) => {
19
+ if (sortBy === "volume") return b.volume - a.volume;
20
+ if (sortBy === "mid") return b.mid - a.mid;
21
+ return a.ticker.localeCompare(b.ticker);
22
+ });
23
+
24
+ return (
25
+ <div className="flex flex-col h-full overflow-hidden">
26
+ <div className="h-9 flex items-center justify-between px-3 border-b border-border bg-card shrink-0">
27
+ <span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
28
+ Markets
29
+ </span>
30
+ <button
31
+ onClick={() =>
32
+ setSortBy((p) =>
33
+ p === "volume" ? "mid" : p === "mid" ? "ticker" : "volume"
34
+ )
35
+ }
36
+ className="flex items-center gap-1 text-[10px] text-muted-foreground/50 hover:text-muted-foreground transition-colors"
37
+ >
38
+ <ArrowUpDown className="h-2.5 w-2.5" />
39
+ {sortBy}
40
+ </button>
41
+ </div>
42
+
43
+ <div className="flex-1 min-h-0 overflow-hidden">
44
+ <ScrollArea className="h-full">
45
+ <table className="w-full text-[10px]">
46
+ <thead className="sticky top-0 bg-card z-10">
47
+ <tr className="text-muted-foreground/40 border-b border-border/50">
48
+ <th className="text-left px-3 py-1.5 font-medium">Ticker</th>
49
+ <th className="text-right px-2 py-1.5 font-medium w-16">Bid/Ask</th>
50
+ <th className="text-right px-2 py-1.5 font-medium w-12">Mid</th>
51
+ <th className="text-right px-3 py-1.5 font-medium w-14">Vol</th>
52
+ </tr>
53
+ </thead>
54
+ <tbody>
55
+ {sorted.map((m) => (
56
+ <tr
57
+ key={m.id}
58
+ className="border-b border-border/20 hover:bg-secondary/20 transition-colors"
59
+ >
60
+ <td className="px-3 py-1.5">
61
+ <a
62
+ href={kalshiUrl(m.ticker, m.title)}
63
+ target="_blank"
64
+ rel="noopener noreferrer"
65
+ className="block hover:text-primary transition-colors"
66
+ >
67
+ <div className="font-mono font-medium truncate max-w-[140px]">
68
+ {m.ticker}
69
+ </div>
70
+ <div className="text-muted-foreground/30 truncate max-w-[140px] text-[9px]">
71
+ {m.title}
72
+ </div>
73
+ </a>
74
+ </td>
75
+ <td className="text-right px-2 py-1.5 font-mono text-muted-foreground/50">
76
+ {m.yes_bid.toFixed(0)}
77
+ <span className="text-muted-foreground/20">/</span>
78
+ {m.yes_ask.toFixed(0)}
79
+ </td>
80
+ <td className="text-right px-2 py-1.5 font-mono font-medium">
81
+ {(m.mid * 100).toFixed(0)}
82
+ <span className="text-muted-foreground/30">%</span>
83
+ </td>
84
+ <td className="text-right px-3 py-1.5 font-mono text-muted-foreground/50">
85
+ {formatVolume(m.volume)}
86
+ </td>
87
+ </tr>
88
+ ))}
89
+ {markets.length === 0 && (
90
+ <tr>
91
+ <td
92
+ colSpan={4}
93
+ className="text-center py-8 text-muted-foreground/30"
94
+ >
95
+ No market data
96
+ </td>
97
+ </tr>
98
+ )}
99
+ </tbody>
100
+ </table>
101
+ </ScrollArea>
102
+ </div>
103
+ </div>
104
+ );
105
+ }
106
+
107
+ function formatVolume(v: number): string {
108
+ if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
109
+ if (v >= 1_000) return `${(v / 1_000).toFixed(1)}K`;
110
+ return String(v);
111
+ }