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,186 @@
1
+ import { NextResponse } from "next/server";
2
+ import { readFileSync, writeFileSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+
5
+ export const dynamic = "force-dynamic";
6
+
7
+ function getToken() {
8
+ return process.env.APIFY_API_TOKEN || "";
9
+ }
10
+
11
+ interface Tweet {
12
+ id: string;
13
+ text: string;
14
+ author: string;
15
+ authorName: string;
16
+ authorImage?: string;
17
+ likes: number;
18
+ reposts: number;
19
+ replies?: number;
20
+ url: string;
21
+ timestamp: string;
22
+ platform: "twitter";
23
+ images?: string[];
24
+ }
25
+
26
+ const SEARCH_QUERY =
27
+ "(kalshi OR polymarket) OR (tariffs OR trade war) OR (trump executive order) OR (federal reserve rates)";
28
+
29
+ function getCachePaths() {
30
+ // Try multiple possible locations
31
+ const candidates = [
32
+ join(process.cwd(), "..", "data"), // if cwd is web/
33
+ join(process.cwd(), "data"), // if cwd is project root
34
+ ];
35
+ for (const dir of candidates) {
36
+ if (existsSync(join(dir, "twitter_cache.json"))) {
37
+ return {
38
+ raw: join(dir, "twitter_cache.json"),
39
+ parsed: join(dir, "twitter_parsed.json"),
40
+ };
41
+ }
42
+ }
43
+ // Default
44
+ return {
45
+ raw: join(process.cwd(), "..", "data", "twitter_cache.json"),
46
+ parsed: join(process.cwd(), "..", "data", "twitter_parsed.json"),
47
+ };
48
+ }
49
+ const CACHE_TTL = 3 * 60 * 60_000; // 3 hours
50
+
51
+ let memCache: { items: Tweet[]; fetchedAt: number } = { items: [], fetchedAt: 0 };
52
+ let refreshing = false;
53
+
54
+ function parseRawTweets(raw: Record<string, unknown>[]): Tweet[] {
55
+ return raw
56
+ .filter((item) => item.text)
57
+ .map((item) => {
58
+ const userInfo = (item.user_info as Record<string, unknown>) || {};
59
+ const rawMedia = item.media;
60
+ let photos: string[] = [];
61
+ if (Array.isArray(rawMedia)) {
62
+ photos = rawMedia
63
+ .filter((m: Record<string, unknown>) => m.type === "photo")
64
+ .map((m: Record<string, unknown>) => (m.media_url_https as string) || "")
65
+ .filter(Boolean);
66
+ } else if (rawMedia && typeof rawMedia === "object") {
67
+ const photoArr = (rawMedia as Record<string, unknown>).photo;
68
+ if (Array.isArray(photoArr)) {
69
+ photos = photoArr
70
+ .map((m: Record<string, unknown>) => (m.media_url_https as string) || "")
71
+ .filter(Boolean);
72
+ }
73
+ }
74
+
75
+ return {
76
+ id: (item.tweet_id as string) || "",
77
+ text: (item.text as string) || "",
78
+ author: (item.screen_name as string) || "",
79
+ authorName:
80
+ (userInfo.name as string) || (item.screen_name as string) || "",
81
+ authorImage:
82
+ (userInfo.profile_image_url as string)?.replace("_normal", "_bigger") ||
83
+ undefined,
84
+ likes: (item.favorites as number) || 0,
85
+ reposts: (item.retweets as number) || 0,
86
+ replies: (item.replies as number) || 0,
87
+ url: `https://x.com/${item.screen_name}/status/${item.tweet_id}`,
88
+ timestamp: (item.created_at as string) || new Date().toISOString(),
89
+ platform: "twitter" as const,
90
+ images: photos.length > 0 ? photos : undefined,
91
+ };
92
+ })
93
+ .sort(
94
+ (a, b) =>
95
+ new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
96
+ )
97
+ .slice(0, 40);
98
+ }
99
+
100
+ function loadFromDisk(): Tweet[] {
101
+ const paths = getCachePaths();
102
+ console.log("[twitter] looking for cache at:", paths.raw);
103
+ // Try parsed cache first
104
+ if (existsSync(paths.parsed)) {
105
+ try {
106
+ const data = JSON.parse(readFileSync(paths.parsed, "utf-8"));
107
+ if (Array.isArray(data) && data.length > 0) return data;
108
+ } catch { /* ignore */ }
109
+ }
110
+ // Fall back to raw cache file (written by CLI prefetch)
111
+ if (existsSync(paths.raw)) {
112
+ console.log("[twitter] found raw cache file");
113
+ try {
114
+ const raw = JSON.parse(readFileSync(paths.raw, "utf-8"));
115
+ if (Array.isArray(raw) && raw.length > 0) {
116
+ console.log("[twitter] parsing", raw.length, "raw tweets");
117
+ const parsed = parseRawTweets(raw);
118
+ console.log("[twitter] parsed", parsed.length, "tweets");
119
+ try { writeFileSync(paths.parsed, JSON.stringify(parsed)); } catch (e) { console.error("[twitter] write parsed failed:", e); }
120
+ return parsed;
121
+ }
122
+ } catch (e) { console.error("[twitter] parse raw failed:", e); }
123
+ }
124
+ console.log("[twitter] no cache file found");
125
+ return [];
126
+ }
127
+
128
+ async function refreshFromApify(): Promise<void> {
129
+ if (refreshing || !getToken()) return;
130
+ refreshing = true;
131
+
132
+ try {
133
+ const res = await fetch(
134
+ `https://api.apify.com/v2/acts/data-slayer~twitter-search/run-sync-get-dataset-items?token=${getToken()}&timeout=120`,
135
+ {
136
+ method: "POST",
137
+ headers: { "Content-Type": "application/json" },
138
+ body: JSON.stringify({ query: SEARCH_QUERY, maxResults: 40 }),
139
+ cache: "no-store",
140
+ signal: AbortSignal.timeout(130_000),
141
+ }
142
+ );
143
+
144
+ if (!res.ok) return;
145
+ const data = await res.json();
146
+ if (!Array.isArray(data) || data.length === 0) return;
147
+
148
+ const tweets = parseRawTweets(data);
149
+ memCache = { items: tweets, fetchedAt: Date.now() };
150
+
151
+ // Persist to disk
152
+ try {
153
+ const paths = getCachePaths();
154
+ writeFileSync(paths.raw, JSON.stringify(data));
155
+ writeFileSync(paths.parsed, JSON.stringify(tweets));
156
+ } catch { /* ignore */ }
157
+ } catch {
158
+ // Apify call failed (likely concurrent run limit)
159
+ } finally {
160
+ refreshing = false;
161
+ }
162
+ }
163
+
164
+ export async function GET() {
165
+ const now = Date.now();
166
+
167
+ // Return mem cache if fresh
168
+ if (memCache.items.length > 0 && now - memCache.fetchedAt < CACHE_TTL) {
169
+ return NextResponse.json(memCache.items);
170
+ }
171
+
172
+ // Load from disk cache (prefetched or previously saved)
173
+ if (memCache.items.length === 0) {
174
+ const diskData = loadFromDisk();
175
+ if (diskData.length > 0) {
176
+ memCache = { items: diskData, fetchedAt: now };
177
+ }
178
+ }
179
+
180
+ // Trigger background refresh (don't await — return cached data immediately)
181
+ if (!refreshing) {
182
+ refreshFromApify();
183
+ }
184
+
185
+ return NextResponse.json(memCache.items);
186
+ }
@@ -0,0 +1,50 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getDb } from "@/lib/db";
3
+
4
+ function tableExists(db: ReturnType<typeof getDb>, name: string): boolean {
5
+ const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(name) as Record<string, unknown> | undefined;
6
+ return !!row;
7
+ }
8
+
9
+ export async function GET() {
10
+ const db = getDb();
11
+
12
+ const hasPortfolio = tableExists(db, "portfolio");
13
+ const hasTrades = tableExists(db, "trades");
14
+ const hasNews = tableExists(db, "news");
15
+ const hasSnapshots = tableExists(db, "market_snapshots");
16
+ const hasLogs = tableExists(db, "agent_logs");
17
+
18
+ const portfolio = hasPortfolio
19
+ ? db.prepare("SELECT * FROM portfolio WHERE id=1").get() as Record<string, unknown> | undefined
20
+ : undefined;
21
+ const openPositions = hasTrades
22
+ ? db.prepare("SELECT * FROM trades WHERE settled=0 AND action='buy' ORDER BY timestamp DESC").all()
23
+ : [];
24
+ const totalNews = hasNews
25
+ ? (db.prepare("SELECT COUNT(*) as count FROM news").get() as { count: number })?.count ?? 0
26
+ : 0;
27
+ const totalSnapshots = hasSnapshots
28
+ ? (db.prepare("SELECT COUNT(*) as count FROM market_snapshots").get() as { count: number })?.count ?? 0
29
+ : 0;
30
+ const totalCycles = hasLogs
31
+ ? (db.prepare("SELECT COUNT(*) as count FROM agent_logs").get() as { count: number })?.count ?? 0
32
+ : 0;
33
+
34
+ const defaults = {
35
+ bankroll: 15,
36
+ total_pnl: 0,
37
+ total_trades: 0,
38
+ wins: 0,
39
+ losses: 0,
40
+ };
41
+
42
+ return NextResponse.json({
43
+ ...defaults,
44
+ ...(portfolio ?? {}),
45
+ open_positions: openPositions,
46
+ total_news: totalNews,
47
+ total_snapshots: totalSnapshots,
48
+ total_cycles: totalCycles,
49
+ });
50
+ }
@@ -0,0 +1,18 @@
1
+ import { NextResponse } from "next/server";
2
+ import { execSync } from "child_process";
3
+ import path from "path";
4
+
5
+ const PROJECT_DIR = path.join(process.cwd(), "..");
6
+
7
+ export async function GET() {
8
+ try {
9
+ const output = execSync("python3 gossip/trader.py prices", {
10
+ cwd: PROJECT_DIR,
11
+ timeout: 15000,
12
+ env: { ...process.env, PYTHONPATH: PROJECT_DIR },
13
+ }).toString();
14
+ return NextResponse.json(JSON.parse(output));
15
+ } catch {
16
+ return NextResponse.json({ positions: [], total_unrealized_pnl: 0 });
17
+ }
18
+ }
@@ -0,0 +1,10 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getDb } from "@/lib/db";
3
+
4
+ export async function GET() {
5
+ const db = getDb();
6
+ const trades = db
7
+ .prepare("SELECT * FROM trades ORDER BY timestamp DESC LIMIT 50")
8
+ .all();
9
+ return NextResponse.json(trades);
10
+ }
Binary file
@@ -0,0 +1,170 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @import "shadcn/tailwind.css";
4
+
5
+ @custom-variant dark (&:is(.dark *));
6
+
7
+ @theme inline {
8
+ --color-background: var(--background);
9
+ --color-foreground: var(--foreground);
10
+ --font-sans: var(--font-sans);
11
+ --font-mono: var(--font-geist-mono);
12
+ --font-heading: var(--font-sans);
13
+ --color-sidebar-ring: var(--sidebar-ring);
14
+ --color-sidebar-border: var(--sidebar-border);
15
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
16
+ --color-sidebar-accent: var(--sidebar-accent);
17
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
18
+ --color-sidebar-primary: var(--sidebar-primary);
19
+ --color-sidebar-foreground: var(--sidebar-foreground);
20
+ --color-sidebar: var(--sidebar);
21
+ --color-chart-5: var(--chart-5);
22
+ --color-chart-4: var(--chart-4);
23
+ --color-chart-3: var(--chart-3);
24
+ --color-chart-2: var(--chart-2);
25
+ --color-chart-1: var(--chart-1);
26
+ --color-ring: var(--ring);
27
+ --color-input: var(--input);
28
+ --color-border: var(--border);
29
+ --color-destructive: var(--destructive);
30
+ --color-accent-foreground: var(--accent-foreground);
31
+ --color-accent: var(--accent);
32
+ --color-muted-foreground: var(--muted-foreground);
33
+ --color-muted: var(--muted);
34
+ --color-secondary-foreground: var(--secondary-foreground);
35
+ --color-secondary: var(--secondary);
36
+ --color-primary-foreground: var(--primary-foreground);
37
+ --color-primary: var(--primary);
38
+ --color-popover-foreground: var(--popover-foreground);
39
+ --color-popover: var(--popover);
40
+ --color-card-foreground: var(--card-foreground);
41
+ --color-card: var(--card);
42
+ --color-success: var(--success);
43
+ --color-success-foreground: var(--success-foreground);
44
+ --radius-sm: calc(var(--radius) * 0.6);
45
+ --radius-md: calc(var(--radius) * 0.8);
46
+ --radius-lg: var(--radius);
47
+ --radius-xl: calc(var(--radius) * 1.4);
48
+ --radius-2xl: calc(var(--radius) * 1.8);
49
+ --radius-3xl: calc(var(--radius) * 2.2);
50
+ --radius-4xl: calc(var(--radius) * 2.6);
51
+ }
52
+
53
+ :root {
54
+ --background: oklch(1 0 0);
55
+ --foreground: oklch(0.145 0 0);
56
+ --card: oklch(1 0 0);
57
+ --card-foreground: oklch(0.145 0 0);
58
+ --popover: oklch(1 0 0);
59
+ --popover-foreground: oklch(0.145 0 0);
60
+ --primary: oklch(0.205 0 0);
61
+ --primary-foreground: oklch(0.985 0 0);
62
+ --secondary: oklch(0.97 0 0);
63
+ --secondary-foreground: oklch(0.205 0 0);
64
+ --muted: oklch(0.97 0 0);
65
+ --muted-foreground: oklch(0.556 0 0);
66
+ --accent: oklch(0.97 0 0);
67
+ --accent-foreground: oklch(0.205 0 0);
68
+ --destructive: oklch(0.577 0.245 27.325);
69
+ --success: oklch(0.723 0.191 149.579);
70
+ --success-foreground: oklch(0.985 0 0);
71
+ --border: oklch(0.922 0 0);
72
+ --input: oklch(0.922 0 0);
73
+ --ring: oklch(0.708 0 0);
74
+ --chart-1: oklch(0.723 0.191 149.579);
75
+ --chart-2: oklch(0.577 0.245 27.325);
76
+ --chart-3: oklch(0.556 0 0);
77
+ --chart-4: oklch(0.439 0 0);
78
+ --chart-5: oklch(0.371 0 0);
79
+ --radius: 0.5rem;
80
+ --sidebar: oklch(0.985 0 0);
81
+ --sidebar-foreground: oklch(0.145 0 0);
82
+ --sidebar-primary: oklch(0.205 0 0);
83
+ --sidebar-primary-foreground: oklch(0.985 0 0);
84
+ --sidebar-accent: oklch(0.97 0 0);
85
+ --sidebar-accent-foreground: oklch(0.205 0 0);
86
+ --sidebar-border: oklch(0.922 0 0);
87
+ --sidebar-ring: oklch(0.708 0 0);
88
+ }
89
+
90
+ .dark {
91
+ --background: oklch(0.1 0 0);
92
+ --foreground: oklch(0.95 0 0);
93
+ --card: oklch(0.14 0 0);
94
+ --card-foreground: oklch(0.95 0 0);
95
+ --popover: oklch(0.14 0 0);
96
+ --popover-foreground: oklch(0.95 0 0);
97
+ --primary: oklch(0.723 0.191 149.579);
98
+ --primary-foreground: oklch(0.1 0 0);
99
+ --secondary: oklch(0.18 0 0);
100
+ --secondary-foreground: oklch(0.9 0 0);
101
+ --muted: oklch(0.18 0 0);
102
+ --muted-foreground: oklch(0.6 0 0);
103
+ --accent: oklch(0.18 0 0);
104
+ --accent-foreground: oklch(0.95 0 0);
105
+ --destructive: oklch(0.704 0.191 22.216);
106
+ --success: oklch(0.723 0.191 149.579);
107
+ --success-foreground: oklch(0.1 0 0);
108
+ --border: oklch(1 0 0 / 8%);
109
+ --input: oklch(1 0 0 / 12%);
110
+ --ring: oklch(0.723 0.191 149.579 / 30%);
111
+ --chart-1: oklch(0.723 0.191 149.579);
112
+ --chart-2: oklch(0.704 0.191 22.216);
113
+ --chart-3: oklch(0.6 0 0);
114
+ --chart-4: oklch(0.45 0 0);
115
+ --chart-5: oklch(0.3 0 0);
116
+ --sidebar: oklch(0.14 0 0);
117
+ --sidebar-foreground: oklch(0.95 0 0);
118
+ --sidebar-primary: oklch(0.723 0.191 149.579);
119
+ --sidebar-primary-foreground: oklch(0.1 0 0);
120
+ --sidebar-accent: oklch(0.18 0 0);
121
+ --sidebar-accent-foreground: oklch(0.95 0 0);
122
+ --sidebar-border: oklch(1 0 0 / 8%);
123
+ --sidebar-ring: oklch(0.723 0.191 149.579 / 30%);
124
+ }
125
+
126
+ @layer base {
127
+ * {
128
+ @apply border-border outline-ring/50;
129
+ }
130
+ body {
131
+ @apply bg-background text-foreground;
132
+ }
133
+ html {
134
+ @apply font-sans;
135
+ }
136
+ }
137
+
138
+ /* Custom scrollbar for trading terminal */
139
+ ::-webkit-scrollbar {
140
+ width: 6px;
141
+ height: 6px;
142
+ }
143
+ ::-webkit-scrollbar-track {
144
+ background: transparent;
145
+ }
146
+ ::-webkit-scrollbar-thumb {
147
+ background: oklch(0.3 0 0);
148
+ border-radius: 3px;
149
+ }
150
+ ::-webkit-scrollbar-thumb:hover {
151
+ background: oklch(0.4 0 0);
152
+ }
153
+
154
+ /* News item highlight animation */
155
+ @keyframes news-flash {
156
+ 0% { background-color: oklch(0.723 0.191 149.579 / 15%); }
157
+ 100% { background-color: transparent; }
158
+ }
159
+ .news-new {
160
+ animation: news-flash 2s ease-out;
161
+ }
162
+
163
+ /* Pulse glow for live indicator */
164
+ @keyframes pulse-glow {
165
+ 0%, 100% { box-shadow: 0 0 0 0 oklch(0.723 0.191 149.579 / 40%); }
166
+ 50% { box-shadow: 0 0 0 4px oklch(0.723 0.191 149.579 / 0%); }
167
+ }
168
+ .pulse-glow {
169
+ animation: pulse-glow 2s ease-in-out infinite;
170
+ }
@@ -0,0 +1,36 @@
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import { TooltipProvider } from "@/components/ui/tooltip";
4
+ import "./globals.css";
5
+
6
+ const geistSans = Geist({
7
+ variable: "--font-sans",
8
+ subsets: ["latin"],
9
+ });
10
+
11
+ const geistMono = Geist_Mono({
12
+ variable: "--font-geist-mono",
13
+ subsets: ["latin"],
14
+ });
15
+
16
+ export const metadata: Metadata = {
17
+ title: "Open Trademaxxxing",
18
+ description: "Autonomous prediction market agent",
19
+ };
20
+
21
+ export default function RootLayout({
22
+ children,
23
+ }: Readonly<{
24
+ children: React.ReactNode;
25
+ }>) {
26
+ return (
27
+ <html
28
+ lang="en"
29
+ className={`${geistSans.variable} ${geistMono.variable} dark h-full antialiased`}
30
+ >
31
+ <body className="h-full overflow-hidden">
32
+ <TooltipProvider>{children}</TooltipProvider>
33
+ </body>
34
+ </html>
35
+ );
36
+ }