opentradex 0.1.3 → 0.1.4
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/README.md +3 -1
- package/gossip/__pycache__/polymarket.cpython-314.pyc +0 -0
- package/main.py +38 -7
- package/package.json +1 -1
- package/src/catalog.mjs +76 -4
- package/src/cli.mjs +44 -18
- package/src/index.mjs +433 -19
- package/web/next-env.d.ts +1 -1
- package/web/src/app/api/workspace/route.ts +12 -0
- package/web/src/app/globals.css +28 -0
- package/web/src/app/guide/page.tsx +262 -0
- package/web/src/app/layout.tsx +2 -1
- package/web/src/app/page.tsx +15 -0
- package/web/src/components/DashboardApp.tsx +192 -88
- package/web/src/components/HarnessBootPanel.tsx +160 -0
- package/web/src/components/LiveStream.tsx +632 -255
- package/web/src/components/TopBar.tsx +135 -82
- package/web/src/lib/demo-data.ts +25 -0
- package/web/src/lib/trading-guide-content.ts +337 -0
- package/web/src/lib/types.ts +30 -0
- package/web/src/lib/workspace.ts +117 -0
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { TopBar } from "@/components/TopBar";
|
|
5
|
-
import { PortfolioStrip } from "@/components/PortfolioStrip";
|
|
6
|
-
import { PositionsPanel } from "@/components/PositionsPanel";
|
|
7
|
-
import { MarketScanner } from "@/components/MarketScanner";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
8
4
|
import { LiveStream } from "@/components/LiveStream";
|
|
5
|
+
import { MarketScanner } from "@/components/MarketScanner";
|
|
9
6
|
import { NewsFeed } from "@/components/NewsFeed";
|
|
7
|
+
import { PortfolioStrip } from "@/components/PortfolioStrip";
|
|
8
|
+
import { PositionsPanel } from "@/components/PositionsPanel";
|
|
9
|
+
import { TopBar } from "@/components/TopBar";
|
|
10
10
|
import type {
|
|
11
|
-
Portfolio,
|
|
12
|
-
Trade,
|
|
13
|
-
NewsArticle,
|
|
14
11
|
Market,
|
|
15
|
-
|
|
12
|
+
NewsArticle,
|
|
13
|
+
Portfolio,
|
|
16
14
|
PositionPrice,
|
|
15
|
+
PromptEntry,
|
|
17
16
|
SocialPost,
|
|
17
|
+
StreamLine,
|
|
18
|
+
Trade,
|
|
19
|
+
WorkspaceSummary,
|
|
18
20
|
} from "@/lib/types";
|
|
19
21
|
|
|
20
22
|
const SHOW_PNL = false;
|
|
@@ -25,6 +27,7 @@ export function DashboardApp() {
|
|
|
25
27
|
const [news, setNews] = useState<NewsArticle[]>([]);
|
|
26
28
|
const [liveNews, setLiveNews] = useState<NewsArticle[]>([]);
|
|
27
29
|
const [markets, setMarkets] = useState<Market[]>([]);
|
|
30
|
+
const [workspace, setWorkspace] = useState<WorkspaceSummary | null>(null);
|
|
28
31
|
const [rationale, setRationale] = useState("");
|
|
29
32
|
const [customPrompt, setCustomPrompt] = useState("");
|
|
30
33
|
const [agentStatus, setAgentStatus] = useState("");
|
|
@@ -40,8 +43,19 @@ export function DashboardApp() {
|
|
|
40
43
|
const [loadingTruth, setLoadingTruth] = useState(true);
|
|
41
44
|
const [loadingReddit, setLoadingReddit] = useState(true);
|
|
42
45
|
const [loadingTiktok, setLoadingTiktok] = useState(true);
|
|
46
|
+
const [promptHistory, setPromptHistory] = useState<PromptEntry[]>([]);
|
|
43
47
|
const streamOffset = useRef(0);
|
|
44
48
|
|
|
49
|
+
const fetchWorkspace = useCallback(async () => {
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch("/api/workspace");
|
|
52
|
+
const data = await res.json();
|
|
53
|
+
setWorkspace(data);
|
|
54
|
+
} catch {
|
|
55
|
+
// ignore
|
|
56
|
+
}
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
45
59
|
const fetchAll = useCallback(async () => {
|
|
46
60
|
try {
|
|
47
61
|
const [p, t, n, m] = await Promise.all([
|
|
@@ -101,11 +115,11 @@ export function DashboardApp() {
|
|
|
101
115
|
setLoadingTweets(false);
|
|
102
116
|
tweetRetries.current = 0;
|
|
103
117
|
} else {
|
|
104
|
-
tweetRetries.current
|
|
118
|
+
tweetRetries.current += 1;
|
|
105
119
|
if (tweetRetries.current > 12) setLoadingTweets(false);
|
|
106
120
|
}
|
|
107
121
|
} catch {
|
|
108
|
-
tweetRetries.current
|
|
122
|
+
tweetRetries.current += 1;
|
|
109
123
|
if (tweetRetries.current > 12) setLoadingTweets(false);
|
|
110
124
|
}
|
|
111
125
|
}, []);
|
|
@@ -132,11 +146,11 @@ export function DashboardApp() {
|
|
|
132
146
|
setLoadingReddit(false);
|
|
133
147
|
redditRetries.current = 0;
|
|
134
148
|
} else {
|
|
135
|
-
redditRetries.current
|
|
149
|
+
redditRetries.current += 1;
|
|
136
150
|
if (redditRetries.current > 12) setLoadingReddit(false);
|
|
137
151
|
}
|
|
138
152
|
} catch {
|
|
139
|
-
redditRetries.current
|
|
153
|
+
redditRetries.current += 1;
|
|
140
154
|
if (redditRetries.current > 12) setLoadingReddit(false);
|
|
141
155
|
}
|
|
142
156
|
}, []);
|
|
@@ -151,16 +165,17 @@ export function DashboardApp() {
|
|
|
151
165
|
setLoadingTiktok(false);
|
|
152
166
|
tiktokRetries.current = 0;
|
|
153
167
|
} else {
|
|
154
|
-
tiktokRetries.current
|
|
168
|
+
tiktokRetries.current += 1;
|
|
155
169
|
if (tiktokRetries.current > 12) setLoadingTiktok(false);
|
|
156
170
|
}
|
|
157
171
|
} catch {
|
|
158
|
-
tiktokRetries.current
|
|
172
|
+
tiktokRetries.current += 1;
|
|
159
173
|
if (tiktokRetries.current > 12) setLoadingTiktok(false);
|
|
160
174
|
}
|
|
161
175
|
}, []);
|
|
162
176
|
|
|
163
177
|
useEffect(() => {
|
|
178
|
+
fetchWorkspace();
|
|
164
179
|
fetchAll();
|
|
165
180
|
fetchLiveNews();
|
|
166
181
|
fetchPrices();
|
|
@@ -168,9 +183,10 @@ export function DashboardApp() {
|
|
|
168
183
|
const redditDelay = setTimeout(fetchRedditPosts, 5_000);
|
|
169
184
|
const tweetDelay = setTimeout(fetchTweets, 15_000);
|
|
170
185
|
const tiktokDelay = setTimeout(fetchTiktokPosts, 25_000);
|
|
171
|
-
const
|
|
186
|
+
const dataId = setInterval(fetchAll, 5_000);
|
|
172
187
|
const newsId = setInterval(fetchLiveNews, 120_000);
|
|
173
188
|
const pricesId = setInterval(fetchPrices, 30_000);
|
|
189
|
+
const workspaceId = setInterval(fetchWorkspace, 60_000);
|
|
174
190
|
const tweetsId = setInterval(() => {
|
|
175
191
|
if (tweetRetries.current > 0 && tweetRetries.current <= 12) {
|
|
176
192
|
fetchTweets();
|
|
@@ -190,10 +206,12 @@ export function DashboardApp() {
|
|
|
190
206
|
}
|
|
191
207
|
}, 5_000);
|
|
192
208
|
const tiktokSlowId = setInterval(fetchTiktokPosts, 300_000);
|
|
209
|
+
|
|
193
210
|
return () => {
|
|
194
|
-
clearInterval(
|
|
211
|
+
clearInterval(dataId);
|
|
195
212
|
clearInterval(newsId);
|
|
196
213
|
clearInterval(pricesId);
|
|
214
|
+
clearInterval(workspaceId);
|
|
197
215
|
clearTimeout(tweetDelay);
|
|
198
216
|
clearTimeout(redditDelay);
|
|
199
217
|
clearTimeout(tiktokDelay);
|
|
@@ -205,16 +223,29 @@ export function DashboardApp() {
|
|
|
205
223
|
clearInterval(tiktokRetryId);
|
|
206
224
|
clearInterval(tiktokSlowId);
|
|
207
225
|
};
|
|
208
|
-
}, [
|
|
226
|
+
}, [
|
|
227
|
+
fetchAll,
|
|
228
|
+
fetchLiveNews,
|
|
229
|
+
fetchPrices,
|
|
230
|
+
fetchRedditPosts,
|
|
231
|
+
fetchTiktokPosts,
|
|
232
|
+
fetchTruthPosts,
|
|
233
|
+
fetchTweets,
|
|
234
|
+
fetchWorkspace,
|
|
235
|
+
]);
|
|
209
236
|
|
|
210
237
|
useEffect(() => {
|
|
211
238
|
const pollStream = async () => {
|
|
212
239
|
try {
|
|
213
|
-
const res = await fetch(
|
|
214
|
-
`/api/agent/stream?offset=${streamOffset.current}`
|
|
215
|
-
);
|
|
240
|
+
const res = await fetch(`/api/agent/stream?offset=${streamOffset.current}`);
|
|
216
241
|
const data = await res.json();
|
|
217
242
|
setLiveStatus(data.status?.status || "idle");
|
|
243
|
+
|
|
244
|
+
if (data.offset < streamOffset.current) {
|
|
245
|
+
setStreamLines([]);
|
|
246
|
+
streamOffset.current = 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
218
249
|
if (data.lines.length > 0) {
|
|
219
250
|
setStreamLines((prev) => {
|
|
220
251
|
const next = [...prev, ...data.lines];
|
|
@@ -222,28 +253,42 @@ export function DashboardApp() {
|
|
|
222
253
|
});
|
|
223
254
|
streamOffset.current = data.offset;
|
|
224
255
|
}
|
|
225
|
-
if (data.offset < streamOffset.current) {
|
|
226
|
-
setStreamLines([]);
|
|
227
|
-
streamOffset.current = 0;
|
|
228
|
-
}
|
|
229
256
|
} catch {
|
|
230
257
|
// ignore
|
|
231
258
|
}
|
|
232
259
|
};
|
|
233
|
-
|
|
260
|
+
|
|
261
|
+
const id = setInterval(pollStream, 1_000);
|
|
234
262
|
return () => clearInterval(id);
|
|
235
263
|
}, []);
|
|
236
264
|
|
|
237
265
|
const mergedNews = mergeNews(news, liveNews);
|
|
238
266
|
|
|
239
|
-
const runCycle = async (prompt?: string) => {
|
|
240
|
-
|
|
267
|
+
const runCycle = async (prompt?: string, channel = "command") => {
|
|
268
|
+
const cleanPrompt = prompt?.trim();
|
|
269
|
+
if (cleanPrompt) {
|
|
270
|
+
setPromptHistory((prev) => [
|
|
271
|
+
...prev.slice(-7),
|
|
272
|
+
{
|
|
273
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
274
|
+
text: cleanPrompt,
|
|
275
|
+
channel,
|
|
276
|
+
createdAt: new Date().toISOString(),
|
|
277
|
+
},
|
|
278
|
+
]);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
setAgentStatus(cleanPrompt ? `Routing ${channel} request...` : "Starting cycle...");
|
|
241
282
|
setStreamLines([]);
|
|
242
283
|
streamOffset.current = 0;
|
|
284
|
+
|
|
243
285
|
const res = await fetch("/api/agent", {
|
|
244
286
|
method: "POST",
|
|
245
287
|
headers: { "Content-Type": "application/json" },
|
|
246
|
-
body: JSON.stringify({
|
|
288
|
+
body: JSON.stringify({
|
|
289
|
+
action: "run_cycle",
|
|
290
|
+
prompt: cleanPrompt ? buildChannelPrompt(cleanPrompt, channel, workspace) : undefined,
|
|
291
|
+
}),
|
|
247
292
|
});
|
|
248
293
|
const data = await res.json();
|
|
249
294
|
setAgentStatus(data.message || data.status);
|
|
@@ -262,11 +307,23 @@ export function DashboardApp() {
|
|
|
262
307
|
|
|
263
308
|
const submitRationale = async () => {
|
|
264
309
|
if (!rationale.trim()) return;
|
|
310
|
+
|
|
311
|
+
const thesis = rationale.trim();
|
|
312
|
+
setPromptHistory((prev) => [
|
|
313
|
+
...prev.slice(-7),
|
|
314
|
+
{
|
|
315
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
316
|
+
text: thesis,
|
|
317
|
+
channel: "command",
|
|
318
|
+
createdAt: new Date().toISOString(),
|
|
319
|
+
},
|
|
320
|
+
]);
|
|
265
321
|
setAgentStatus("Researching thesis...");
|
|
322
|
+
|
|
266
323
|
const res = await fetch("/api/agent", {
|
|
267
324
|
method: "POST",
|
|
268
325
|
headers: { "Content-Type": "application/json" },
|
|
269
|
-
body: JSON.stringify({ action: "submit_rationale", rationale }),
|
|
326
|
+
body: JSON.stringify({ action: "submit_rationale", rationale: thesis }),
|
|
270
327
|
});
|
|
271
328
|
const data = await res.json();
|
|
272
329
|
setAgentStatus(data.message || data.status);
|
|
@@ -274,64 +331,73 @@ export function DashboardApp() {
|
|
|
274
331
|
};
|
|
275
332
|
|
|
276
333
|
return (
|
|
277
|
-
<div className="h-screen w-screen max-w-full
|
|
278
|
-
<
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
<
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
334
|
+
<div className="dashboard-shell h-screen w-screen max-w-full overflow-hidden bg-background">
|
|
335
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
336
|
+
<TopBar
|
|
337
|
+
workspace={workspace}
|
|
338
|
+
liveStatus={liveStatus}
|
|
339
|
+
agentStatus={agentStatus}
|
|
340
|
+
rationale={rationale}
|
|
341
|
+
loopInterval={loopInterval}
|
|
342
|
+
onRationaleChange={setRationale}
|
|
343
|
+
onSubmitRationale={submitRationale}
|
|
344
|
+
onRunCycle={() => runCycle()}
|
|
345
|
+
onStartLoop={startLoop}
|
|
346
|
+
onRefresh={() => {
|
|
347
|
+
fetchWorkspace();
|
|
348
|
+
fetchAll();
|
|
349
|
+
fetchLiveNews();
|
|
350
|
+
}}
|
|
351
|
+
onLoopIntervalChange={setLoopInterval}
|
|
352
|
+
/>
|
|
353
|
+
|
|
354
|
+
<PortfolioStrip
|
|
355
|
+
portfolio={portfolio}
|
|
356
|
+
unrealizedPnl={prices.reduce((sum, price) => sum + price.unrealized_pnl, 0)}
|
|
357
|
+
/>
|
|
358
|
+
|
|
359
|
+
<div className="grid min-h-0 flex-1 grid-cols-1 overflow-hidden md:grid-cols-[minmax(0,280px)_minmax(0,1fr)] lg:grid-cols-[minmax(0,300px)_minmax(0,1fr)_minmax(0,360px)]">
|
|
360
|
+
<div className="hidden min-h-0 flex-col overflow-hidden border-r border-border/70 bg-white/40 backdrop-blur md:flex">
|
|
361
|
+
<div className="flex-1 min-h-0 overflow-hidden">
|
|
362
|
+
<PositionsPanel
|
|
363
|
+
positions={portfolio?.open_positions ?? []}
|
|
364
|
+
trades={trades}
|
|
365
|
+
prices={prices}
|
|
366
|
+
/>
|
|
367
|
+
</div>
|
|
368
|
+
<div className="h-[40%] min-h-0 overflow-hidden border-t border-border/70">
|
|
369
|
+
<MarketScanner markets={markets} />
|
|
370
|
+
</div>
|
|
307
371
|
</div>
|
|
308
|
-
</div>
|
|
309
372
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
373
|
+
<div className="min-h-0 overflow-hidden">
|
|
374
|
+
<LiveStream
|
|
375
|
+
lines={streamLines}
|
|
376
|
+
liveStatus={liveStatus}
|
|
377
|
+
customPrompt={customPrompt}
|
|
378
|
+
prompts={promptHistory}
|
|
379
|
+
workspace={workspace}
|
|
380
|
+
onCustomPromptChange={setCustomPrompt}
|
|
381
|
+
onSendCommand={(prompt, channel) => {
|
|
382
|
+
runCycle(prompt, channel);
|
|
383
|
+
setCustomPrompt("");
|
|
384
|
+
}}
|
|
385
|
+
/>
|
|
386
|
+
</div>
|
|
322
387
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
388
|
+
<div className="hidden min-h-0 flex-col overflow-hidden border-l border-border/70 bg-white/40 backdrop-blur lg:flex">
|
|
389
|
+
<NewsFeed
|
|
390
|
+
news={mergedNews}
|
|
391
|
+
tweets={tweets}
|
|
392
|
+
truthPosts={truthPosts}
|
|
393
|
+
redditPosts={redditPosts}
|
|
394
|
+
tiktokPosts={tiktokPosts}
|
|
395
|
+
loadingTweets={loadingTweets}
|
|
396
|
+
loadingTruth={loadingTruth}
|
|
397
|
+
loadingReddit={loadingReddit}
|
|
398
|
+
loadingTiktok={loadingTiktok}
|
|
399
|
+
/>
|
|
400
|
+
</div>
|
|
335
401
|
</div>
|
|
336
402
|
</div>
|
|
337
403
|
</div>
|
|
@@ -348,6 +414,7 @@ function mergeNews(dbNews: NewsArticle[], liveNews: NewsArticle[]): NewsArticle[
|
|
|
348
414
|
merged.push(item);
|
|
349
415
|
}
|
|
350
416
|
}
|
|
417
|
+
|
|
351
418
|
for (const item of liveNews) {
|
|
352
419
|
if (!seen.has(item.title)) {
|
|
353
420
|
seen.add(item.title);
|
|
@@ -355,8 +422,45 @@ function mergeNews(dbNews: NewsArticle[], liveNews: NewsArticle[]): NewsArticle[
|
|
|
355
422
|
}
|
|
356
423
|
}
|
|
357
424
|
|
|
358
|
-
merged.sort(
|
|
359
|
-
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
360
|
-
);
|
|
425
|
+
merged.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
361
426
|
return merged.slice(0, 60);
|
|
362
427
|
}
|
|
428
|
+
|
|
429
|
+
function buildChannelPrompt(
|
|
430
|
+
prompt: string,
|
|
431
|
+
channel: string,
|
|
432
|
+
workspace: WorkspaceSummary | null
|
|
433
|
+
) {
|
|
434
|
+
const rails = workspace?.enabledMarkets.join(", ") || "kalshi";
|
|
435
|
+
const feeds = workspace?.integrations.join(", ") || "apify, rss";
|
|
436
|
+
const watchlist = workspace?.tradingview.watchlist.join(", ") || "SPY, QQQ, BTCUSD, NQ1!";
|
|
437
|
+
|
|
438
|
+
if (channel === "markets") {
|
|
439
|
+
return `Channel: markets.\nFocus on the enabled market rails (${rails}). Compare prices, scan only liquid contracts, and explain the best surviving setup.\n\nUser request: ${prompt}`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (channel === "feeds") {
|
|
443
|
+
return `Channel: feeds.\nFocus on the enabled news and social feeds (${feeds}). Summarize what changed recently, what is already priced in, and what deserves a market follow-up.\n\nUser request: ${prompt}`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (channel === "risk") {
|
|
447
|
+
return `Channel: risk.\nReview open exposure first. Prioritize thesis validation, exit conditions, bankroll preservation, and hard passes over new trades.\n\nUser request: ${prompt}`;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (channel === "execution") {
|
|
451
|
+
return `Channel: execution.\nFocus on the most executable idea only. Confirm supported rails, size, entry level, and why the trade should or should not actually fire.\n\nUser request: ${prompt}`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (channel === "tradingview") {
|
|
455
|
+
const connectorLine =
|
|
456
|
+
workspace?.tradingview.connectorMode === "mcp" && workspace.tradingview.mcpEnabled
|
|
457
|
+
? workspace.tradingview.configured
|
|
458
|
+
? "A TradingView MCP connector is configured for this workspace. Use it if the local Claude session has the server available."
|
|
459
|
+
: "TradingView MCP is enabled in the workspace profile but still incomplete. Fall back to the watchlist unless the connector becomes available."
|
|
460
|
+
: "TradingView is running in watchlist-only mode for this workspace.";
|
|
461
|
+
|
|
462
|
+
return `Channel: tradingview.\nFocus on the TradingView watchlist (${watchlist}). ${connectorLine}\nUse this lane for symbol triage, cross-asset context, and chart-aware market research.\n\nUser request: ${prompt}`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return prompt;
|
|
466
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { WorkspaceSummary } from "@/lib/types";
|
|
4
|
+
|
|
5
|
+
const cockpitLanes = [
|
|
6
|
+
{
|
|
7
|
+
id: "command",
|
|
8
|
+
name: "Command",
|
|
9
|
+
detail: "Direct the harness, launch scans, and ask for a clean next move.",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: "markets",
|
|
13
|
+
name: "Markets",
|
|
14
|
+
detail: "Cross-market comparison across Kalshi, Polymarket, and active rails.",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: "feeds",
|
|
18
|
+
name: "Feeds",
|
|
19
|
+
detail: "News and social context for catalysts, narrative shifts, and timing.",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "risk",
|
|
23
|
+
name: "Risk",
|
|
24
|
+
detail: "Position review, sizing discipline, and reasons to pass.",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "execution",
|
|
28
|
+
name: "Execution",
|
|
29
|
+
detail: "Supported trade routing, order readiness, and cycle recap.",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "tradingview",
|
|
33
|
+
name: "TradingView",
|
|
34
|
+
detail: "Watchlist or MCP-backed chart context for symbols, macro, and timing.",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function buildMissions(workspace: WorkspaceSummary | null) {
|
|
39
|
+
const watchlist = workspace?.tradingview.watchlist.join(", ") || "SPY, QQQ, BTCUSD, NQ1!";
|
|
40
|
+
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
label: "Connector audit",
|
|
44
|
+
prompt:
|
|
45
|
+
"Audit this OpenTradex workspace. Tell me which rails, feeds, channels, and credentials are configured, what is still missing, and the smartest next local step.",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: "Cross-market scan",
|
|
49
|
+
prompt:
|
|
50
|
+
"Scan the enabled market rails, compare overlapping themes, and surface the best 3 setups before recommending one paper trade or pass.",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: "TradingView pass",
|
|
54
|
+
prompt: `Use the TradingView lane and focus on this watchlist: ${watchlist}. Tell me which symbols or macro instruments deserve attention right now and why.`,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
label: "Risk review",
|
|
58
|
+
prompt:
|
|
59
|
+
"Run a strict risk-manager review of the portfolio and open theses. Cut weak ideas, flag unsupported execution paths, and list only what survives.",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
label: "News recap",
|
|
63
|
+
prompt:
|
|
64
|
+
"Summarize the latest feeds, separate what is new from what is already priced in, and point me to one market worth deeper research.",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
label: "Explain setup",
|
|
68
|
+
prompt:
|
|
69
|
+
"Explain this OpenTradex setup like an operator boot briefing: runtime, rails, dashboard surface, channels, and where live execution is actually supported.",
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function HarnessBootPanel({
|
|
75
|
+
workspace,
|
|
76
|
+
onLaunchMission,
|
|
77
|
+
}: {
|
|
78
|
+
workspace: WorkspaceSummary | null;
|
|
79
|
+
onLaunchMission: (prompt: string) => void;
|
|
80
|
+
}) {
|
|
81
|
+
const missions = buildMissions(workspace);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="relative overflow-hidden rounded-[1.6rem] border border-[#553249] bg-[linear-gradient(180deg,#321126_0%,#240b1c_100%)] p-5 text-[#f7e6ee] shadow-[0_30px_120px_rgba(24,6,14,0.45)]">
|
|
85
|
+
<div className="absolute inset-0 pointer-events-none bg-[linear-gradient(rgba(255,255,255,0.04)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[size:22px_22px] opacity-40" />
|
|
86
|
+
|
|
87
|
+
<div className="relative flex flex-wrap items-start justify-between gap-4">
|
|
88
|
+
<div className="max-w-2xl">
|
|
89
|
+
<p className="font-mono text-[0.68rem] uppercase tracking-[0.28em] text-[#f3a96f]">
|
|
90
|
+
OpenTradex onboarding
|
|
91
|
+
</p>
|
|
92
|
+
<h3 className="mt-3 text-3xl font-semibold tracking-tight text-white">
|
|
93
|
+
Start in chat, route by channel, keep the rails honest.
|
|
94
|
+
</h3>
|
|
95
|
+
<p className="mt-3 max-w-xl text-sm leading-7 text-[#e9c9d9]/80">
|
|
96
|
+
This cockpit is meant to feel like an operator interface, not a flat log.
|
|
97
|
+
Pick a lane, send a mission, and use the right connector for the job.
|
|
98
|
+
</p>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div className="rounded-full border border-[#724b62] bg-[#4a2038]/80 px-3 py-1.5 font-mono text-[0.68rem] uppercase tracking-[0.24em] text-[#ffd9a4]">
|
|
102
|
+
{workspace?.dashboardSurface === "chat" ? "Chat cockpit active" : "Stream surface active"}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div className="relative mt-5 grid gap-3 xl:grid-cols-5">
|
|
107
|
+
{cockpitLanes
|
|
108
|
+
.filter((lane) => (lane.id === "tradingview" ? workspace?.tradingview.enabled : true))
|
|
109
|
+
.map((lane) => (
|
|
110
|
+
<div
|
|
111
|
+
key={lane.id}
|
|
112
|
+
className="rounded-[1.2rem] border border-[#6f475f] bg-[#40172f]/70 p-3"
|
|
113
|
+
>
|
|
114
|
+
<p className="font-mono text-[0.66rem] uppercase tracking-[0.24em] text-[#f3a96f]">
|
|
115
|
+
{lane.name}
|
|
116
|
+
</p>
|
|
117
|
+
<p className="mt-2 text-[0.82rem] leading-6 text-[#f4dfea]/78">{lane.detail}</p>
|
|
118
|
+
</div>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div className="relative mt-5 flex flex-wrap gap-2">
|
|
123
|
+
{workspace?.enabledMarkets.map((market) => (
|
|
124
|
+
<span
|
|
125
|
+
key={market}
|
|
126
|
+
className="rounded-full border border-[#724b62] bg-[#4a2038]/70 px-3 py-1 text-[11px] uppercase tracking-[0.18em] text-[#f7d3a7]"
|
|
127
|
+
>
|
|
128
|
+
{market}
|
|
129
|
+
</span>
|
|
130
|
+
))}
|
|
131
|
+
{workspace?.tradingview.enabled ? (
|
|
132
|
+
<span className="rounded-full border border-[#724b62] bg-[#4a2038]/70 px-3 py-1 text-[11px] uppercase tracking-[0.18em] text-[#f7d3a7]">
|
|
133
|
+
{workspace.tradingview.connectorMode === "mcp"
|
|
134
|
+
? workspace.tradingview.configured
|
|
135
|
+
? "tradingview mcp ready"
|
|
136
|
+
: "tradingview mcp incomplete"
|
|
137
|
+
: "tradingview watchlist"}
|
|
138
|
+
</span>
|
|
139
|
+
) : null}
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div className="relative mt-5 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
143
|
+
{missions.map((mission) => (
|
|
144
|
+
<button
|
|
145
|
+
key={mission.label}
|
|
146
|
+
onClick={() => onLaunchMission(mission.prompt)}
|
|
147
|
+
className="rounded-[1.2rem] border border-[#724b62] bg-[#4a2038]/58 px-4 py-3 text-left transition-all hover:-translate-y-0.5 hover:border-[#9e6985] hover:bg-[#572542]/72"
|
|
148
|
+
>
|
|
149
|
+
<p className="font-mono text-[0.66rem] uppercase tracking-[0.24em] text-[#f7d3a7]">
|
|
150
|
+
{mission.label}
|
|
151
|
+
</p>
|
|
152
|
+
<p className="mt-2 text-[0.82rem] leading-6 text-[#f4dfea]/78">
|
|
153
|
+
{mission.prompt}
|
|
154
|
+
</p>
|
|
155
|
+
</button>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|