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.
- package/.env.example +8 -0
- package/CLAUDE.md +98 -0
- package/README.md +246 -0
- package/SOUL.md +79 -0
- package/SPEC.md +317 -0
- package/SUBMISSION.md +30 -0
- package/architecture.excalidraw +170 -0
- package/architecture.png +0 -0
- package/bin/opentradex.mjs +4 -0
- package/data/.gitkeep +0 -0
- package/data/strategy_notes.md +158 -0
- package/gossip/__init__.py +0 -0
- package/gossip/dashboard.py +150 -0
- package/gossip/db.py +358 -0
- package/gossip/kalshi.py +492 -0
- package/gossip/news.py +235 -0
- package/gossip/trader.py +646 -0
- package/main.py +287 -0
- package/package.json +47 -0
- package/requirements.txt +7 -0
- package/src/cli.mjs +124 -0
- package/src/index.mjs +420 -0
- package/web/AGENTS.md +5 -0
- package/web/CLAUDE.md +1 -0
- package/web/README.md +36 -0
- package/web/components.json +25 -0
- package/web/eslint.config.mjs +18 -0
- package/web/next.config.ts +7 -0
- package/web/package-lock.json +11626 -0
- package/web/package.json +37 -0
- package/web/postcss.config.mjs +7 -0
- package/web/public/file.svg +1 -0
- package/web/public/globe.svg +1 -0
- package/web/public/next.svg +1 -0
- package/web/public/vercel.svg +1 -0
- package/web/public/window.svg +1 -0
- package/web/src/app/api/agent/route.ts +77 -0
- package/web/src/app/api/agent/stream/route.ts +87 -0
- package/web/src/app/api/markets/route.ts +15 -0
- package/web/src/app/api/news/live/route.ts +77 -0
- package/web/src/app/api/news/reddit/route.ts +118 -0
- package/web/src/app/api/news/route.ts +10 -0
- package/web/src/app/api/news/tiktok/route.ts +115 -0
- package/web/src/app/api/news/truthsocial/route.ts +116 -0
- package/web/src/app/api/news/twitter/route.ts +186 -0
- package/web/src/app/api/portfolio/route.ts +50 -0
- package/web/src/app/api/prices/route.ts +18 -0
- package/web/src/app/api/trades/route.ts +10 -0
- package/web/src/app/favicon.ico +0 -0
- package/web/src/app/globals.css +170 -0
- package/web/src/app/layout.tsx +36 -0
- package/web/src/app/page.tsx +366 -0
- package/web/src/components/AgentLog.tsx +71 -0
- package/web/src/components/LiveStream.tsx +394 -0
- package/web/src/components/MarketScanner.tsx +111 -0
- package/web/src/components/NewsFeed.tsx +561 -0
- package/web/src/components/PortfolioStrip.tsx +139 -0
- package/web/src/components/PositionsPanel.tsx +219 -0
- package/web/src/components/TopBar.tsx +127 -0
- package/web/src/components/ui/badge.tsx +52 -0
- package/web/src/components/ui/button.tsx +60 -0
- package/web/src/components/ui/card.tsx +103 -0
- package/web/src/components/ui/scroll-area.tsx +55 -0
- package/web/src/components/ui/separator.tsx +25 -0
- package/web/src/components/ui/tabs.tsx +82 -0
- package/web/src/components/ui/tooltip.tsx +66 -0
- package/web/src/lib/db.ts +81 -0
- package/web/src/lib/types.ts +130 -0
- package/web/src/lib/utils.ts +6 -0
- 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
|
+
}
|