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.
@@ -1,19 +1,21 @@
1
1
  "use client";
2
2
 
3
- import { Activity, Play, Repeat, RotateCw } from "lucide-react";
3
+ import { Activity, Play, Repeat, RotateCw, Sparkles, Workflow } from "lucide-react";
4
4
  import { Button } from "@/components/ui/button";
5
+ import type { WorkspaceSummary } from "@/lib/types";
5
6
 
6
7
  interface TopBarProps {
8
+ workspace: WorkspaceSummary | null;
7
9
  liveStatus: string;
8
10
  agentStatus: string;
9
11
  rationale: string;
10
12
  loopInterval: number;
11
- onRationaleChange: (v: string) => void;
13
+ onRationaleChange: (value: string) => void;
12
14
  onSubmitRationale: () => void;
13
15
  onRunCycle: () => void;
14
16
  onStartLoop: () => void;
15
17
  onRefresh: () => void;
16
- onLoopIntervalChange: (v: number) => void;
18
+ onLoopIntervalChange: (value: number) => void;
17
19
  }
18
20
 
19
21
  const INTERVALS = [
@@ -25,6 +27,7 @@ const INTERVALS = [
25
27
  ];
26
28
 
27
29
  export function TopBar({
30
+ workspace,
28
31
  liveStatus,
29
32
  agentStatus,
30
33
  rationale,
@@ -36,92 +39,142 @@ export function TopBar({
36
39
  onRefresh,
37
40
  onLoopIntervalChange,
38
41
  }: TopBarProps) {
42
+ const tradingviewLabel = workspace?.tradingview.enabled
43
+ ? workspace.tradingview.connectorMode === "mcp"
44
+ ? workspace.tradingview.configured
45
+ ? "TradingView MCP"
46
+ : "TradingView MCP (needs setup)"
47
+ : "TradingView watchlist"
48
+ : null;
49
+
39
50
  return (
40
- <header className="h-14 flex items-center gap-4 px-5 border-b border-border bg-card shrink-0 overflow-hidden max-w-full">
41
- <div className="flex items-center gap-3 mr-2">
42
- <Activity className="h-5 w-5 text-primary" />
43
- <span className="font-semibold text-sm tracking-tight">
44
- OpenTradex
45
- </span>
46
- </div>
51
+ <header className="shrink-0 border-b border-border/70 bg-[linear-gradient(180deg,rgba(255,255,255,0.96),rgba(255,250,246,0.82))] px-4 py-3 backdrop-blur xl:px-5">
52
+ <div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
53
+ <div className="min-w-0">
54
+ <div className="flex flex-wrap items-center gap-2">
55
+ <div className="flex items-center gap-2 rounded-full border border-emerald-400/20 bg-emerald-500/8 px-3 py-1 text-[11px] uppercase tracking-[0.22em] text-emerald-700">
56
+ <Activity className="h-3.5 w-3.5" />
57
+ OpenTradex
58
+ </div>
59
+ <div className="flex items-center gap-1.5 rounded-full border border-slate-900/8 bg-white/80 px-3 py-1 text-[11px] text-slate-600">
60
+ <span
61
+ className={`h-2 w-2 rounded-full ${
62
+ liveStatus === "running"
63
+ ? "bg-emerald-500 pulse-glow"
64
+ : liveStatus === "error"
65
+ ? "bg-rose-500"
66
+ : "bg-amber-500"
67
+ }`}
68
+ />
69
+ {liveStatus === "running"
70
+ ? "Agent running"
71
+ : liveStatus === "error"
72
+ ? "Needs attention"
73
+ : "Ready"}
74
+ </div>
75
+ {workspace && (
76
+ <>
77
+ <InfoPill label={workspace.runtime} />
78
+ <InfoPill label={`${workspace.mode} mode`} />
79
+ <InfoPill label={workspace.primaryMarket} />
80
+ {tradingviewLabel ? <InfoPill label={tradingviewLabel} /> : null}
81
+ </>
82
+ )}
83
+ </div>
84
+ <div className="mt-2 flex flex-wrap items-center gap-2 text-sm text-slate-600">
85
+ <span className="font-medium text-slate-900">Command cockpit</span>
86
+ <span className="text-slate-400">/</span>
87
+ <span>
88
+ {workspace?.dashboardSurface === "chat"
89
+ ? "channel-based operator chat"
90
+ : "stream-first control surface"}
91
+ </span>
92
+ {agentStatus ? (
93
+ <>
94
+ <span className="text-slate-400">/</span>
95
+ <span className="truncate text-slate-500">{agentStatus}</span>
96
+ </>
97
+ ) : null}
98
+ </div>
99
+ </div>
47
100
 
48
- <div className="flex items-center gap-2">
49
- <span
50
- className={`w-2 h-2 rounded-full shrink-0 ${
51
- liveStatus === "running"
52
- ? "bg-primary pulse-glow"
53
- : liveStatus === "error"
54
- ? "bg-destructive"
55
- : "bg-muted-foreground/40"
56
- }`}
57
- />
58
- <span className="text-xs text-muted-foreground whitespace-nowrap">
59
- {liveStatus === "running"
60
- ? "Running"
61
- : liveStatus === "error"
62
- ? "Error"
63
- : "Idle"}
64
- </span>
65
- </div>
101
+ <div className="grid gap-3 xl:min-w-[44rem] xl:grid-cols-[minmax(0,1fr)_auto_auto] xl:items-center">
102
+ <label className="flex min-w-0 items-center gap-2 rounded-2xl border border-slate-900/8 bg-white/86 px-3 py-2 shadow-sm">
103
+ <Sparkles className="h-4 w-4 shrink-0 text-amber-500" />
104
+ <input
105
+ type="text"
106
+ value={rationale}
107
+ onChange={(event) => onRationaleChange(event.target.value)}
108
+ onKeyDown={(event) => event.key === "Enter" && onSubmitRationale()}
109
+ placeholder="Drop a thesis or catalyst and let the harness research it."
110
+ className="min-w-0 flex-1 bg-transparent text-sm text-slate-800 outline-none placeholder:text-slate-400"
111
+ />
112
+ </label>
66
113
 
67
- {agentStatus && (
68
- <span className="text-xs text-muted-foreground/60 truncate max-w-48">
69
- {agentStatus}
70
- </span>
71
- )}
114
+ <div className="flex items-center gap-1 rounded-full border border-slate-900/8 bg-white/82 p-1">
115
+ {INTERVALS.map((interval) => (
116
+ <button
117
+ key={interval.value}
118
+ onClick={() => onLoopIntervalChange(interval.value)}
119
+ className={`rounded-full px-2.5 py-1 text-[10px] font-medium uppercase tracking-[0.18em] transition-colors ${
120
+ loopInterval === interval.value
121
+ ? "bg-slate-900 text-white"
122
+ : "text-slate-500 hover:bg-slate-100 hover:text-slate-800"
123
+ }`}
124
+ >
125
+ {interval.label}
126
+ </button>
127
+ ))}
128
+ </div>
72
129
 
73
- <div className="flex-1 max-w-lg ml-auto">
74
- <div className="relative">
75
- <input
76
- type="text"
77
- value={rationale}
78
- onChange={(e) => onRationaleChange(e.target.value)}
79
- onKeyDown={(e) => e.key === "Enter" && onSubmitRationale()}
80
- placeholder="Enter a thesis to research & trade..."
81
- className="w-full bg-secondary/50 border border-border rounded-lg px-3 py-1.5 text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 placeholder:text-muted-foreground/40 transition-all"
82
- />
130
+ <div className="flex items-center gap-1.5">
131
+ <Button size="sm" onClick={onRunCycle} className="h-9 gap-1.5 rounded-full px-4 text-xs">
132
+ <Play className="h-3.5 w-3.5" />
133
+ Run cycle
134
+ </Button>
135
+ <Button
136
+ size="sm"
137
+ variant="secondary"
138
+ onClick={onStartLoop}
139
+ className="h-9 gap-1.5 rounded-full px-4 text-xs"
140
+ >
141
+ <Repeat className="h-3.5 w-3.5" />
142
+ Auto loop
143
+ </Button>
144
+ <Button
145
+ size="icon"
146
+ variant="ghost"
147
+ onClick={onRefresh}
148
+ className="h-9 w-9 rounded-full"
149
+ >
150
+ <RotateCw className="h-4 w-4" />
151
+ </Button>
152
+ </div>
83
153
  </div>
84
154
  </div>
85
155
 
86
- <div className="flex items-center gap-1 shrink-0">
87
- {INTERVALS.map((i) => (
88
- <button
89
- key={i.value}
90
- onClick={() => onLoopIntervalChange(i.value)}
91
- className={`px-2 py-1 rounded text-[10px] font-medium transition-colors ${
92
- loopInterval === i.value
93
- ? "bg-primary text-primary-foreground"
94
- : "text-muted-foreground hover:text-foreground hover:bg-secondary"
95
- }`}
96
- >
97
- {i.label}
98
- </button>
99
- ))}
100
- </div>
101
-
102
- <div className="flex items-center gap-1.5 shrink-0">
103
- <Button size="sm" onClick={onRunCycle} className="h-7 text-xs gap-1.5">
104
- <Play className="h-3 w-3" />
105
- Run
106
- </Button>
107
- <Button
108
- size="sm"
109
- variant="secondary"
110
- onClick={onStartLoop}
111
- className="h-7 text-xs gap-1.5"
112
- >
113
- <Repeat className="h-3 w-3" />
114
- Loop
115
- </Button>
116
- <Button
117
- size="icon"
118
- variant="ghost"
119
- onClick={onRefresh}
120
- className="h-7 w-7"
121
- >
122
- <RotateCw className="h-3.5 w-3.5" />
123
- </Button>
124
- </div>
156
+ {workspace ? (
157
+ <div className="mt-3 flex flex-wrap items-center gap-2 text-[11px] text-slate-500">
158
+ <span className="inline-flex items-center gap-1.5 rounded-full border border-slate-900/8 bg-white/75 px-2.5 py-1">
159
+ <Workflow className="h-3 w-3" />
160
+ Channels: {workspace.channels.join(", ")}
161
+ </span>
162
+ <span className="inline-flex items-center gap-1.5 rounded-full border border-slate-900/8 bg-white/75 px-2.5 py-1">
163
+ Rails: {workspace.enabledMarkets.join(", ")}
164
+ </span>
165
+ <span className="inline-flex items-center gap-1.5 rounded-full border border-slate-900/8 bg-white/75 px-2.5 py-1">
166
+ Feeds: {workspace.integrations.join(", ")}
167
+ </span>
168
+ </div>
169
+ ) : null}
125
170
  </header>
126
171
  );
127
172
  }
173
+
174
+ function InfoPill({ label }: { label: string }) {
175
+ return (
176
+ <span className="rounded-full border border-slate-900/8 bg-white/75 px-3 py-1 text-[11px] uppercase tracking-[0.18em] text-slate-500">
177
+ {label}
178
+ </span>
179
+ );
180
+ }
@@ -3,6 +3,7 @@ import type {
3
3
  Market,
4
4
  NewsArticle,
5
5
  Portfolio,
6
+ WorkspaceSummary,
6
7
  StreamLine,
7
8
  Trade,
8
9
  } from "@/lib/types";
@@ -297,3 +298,27 @@ export function getDemoAgentStatus() {
297
298
  next_check_at: hoursFromNow(1),
298
299
  };
299
300
  }
301
+
302
+ export function getDemoWorkspaceSummary(): WorkspaceSummary {
303
+ return {
304
+ isDemo: true,
305
+ runtime: "claude-code",
306
+ packageManager: "npm",
307
+ mode: "paper",
308
+ primaryMarket: "kalshi",
309
+ enabledMarkets: ["kalshi", "polymarket", "tradingview"],
310
+ integrations: ["apify", "rss", "reddit", "twitter"],
311
+ dashboardSurface: "chat",
312
+ channels: ["command", "markets", "feeds", "risk", "execution", "tradingview"],
313
+ tradingview: {
314
+ enabled: true,
315
+ watchlist: ["SPY", "QQQ", "BTCUSD", "NQ1!"],
316
+ connectorMode: "mcp",
317
+ mcpEnabled: true,
318
+ transport: "stdio",
319
+ command: "npx tradingview-mcp",
320
+ args: "--demo",
321
+ configured: true,
322
+ },
323
+ };
324
+ }
@@ -0,0 +1,337 @@
1
+ export type GuideCodeSample = {
2
+ label: string;
3
+ language: string;
4
+ code: string;
5
+ };
6
+
7
+ export type GuideStep = {
8
+ level: string;
9
+ title: string;
10
+ summary: string;
11
+ outcome: string;
12
+ highlights: string[];
13
+ codeSamples: GuideCodeSample[];
14
+ };
15
+
16
+ export const guideBenchmarks = [
17
+ {
18
+ label: "Prompted LLM",
19
+ value: "55-62%",
20
+ note: "Draft benchmark band for raw prompting without task-specific tuning.",
21
+ },
22
+ {
23
+ label: "Fine-tuned",
24
+ value: "65-72%",
25
+ note: "Illustrative range for a LoRA-tuned financial classification task.",
26
+ },
27
+ {
28
+ label: "Fine-tuned + RAG",
29
+ value: "68-75%",
30
+ note: "Adds current retrieval context on top of a domain-adapted model.",
31
+ },
32
+ {
33
+ label: "Reality check",
34
+ value: "Validate it",
35
+ note: "Slippage, data leakage, and regime change still matter more than slide-deck numbers.",
36
+ },
37
+ ];
38
+
39
+ export const guidePrinciples = [
40
+ "Each level should deliver a usable result.",
41
+ "Split by date so the model never sees the future.",
42
+ "Keep monitoring and risk controls separate from prompting.",
43
+ "Paper trade before you even think about live execution.",
44
+ ];
45
+
46
+ export const guideResearchNotes = [
47
+ "The accuracy bands and research framing here should be treated as draft guide copy until you validate them against your own data, date splits, fees, and execution assumptions.",
48
+ "Nothing on this page is financial advice, a return guarantee, or a claim that a model can trade safely without human review.",
49
+ ];
50
+
51
+ export const guideSteps: GuideStep[] = [
52
+ {
53
+ level: "Level 1",
54
+ title: "Your first trading signal in about 50 lines",
55
+ summary:
56
+ "Start with a structured analyst prompt, force JSON output, and map one news item into a next-day directional signal.",
57
+ outcome:
58
+ "You finish with a working analysis function that can classify a headline as bullish, bearish, or neutral.",
59
+ highlights: [
60
+ "Use an OpenAI-compatible client so the same flow can point at OpenAI, Ollama, Together, or a local server.",
61
+ "Low temperature plus JSON output keeps the model consistent enough to evaluate.",
62
+ "Prompt for what is new versus what is already priced in.",
63
+ ],
64
+ codeSamples: [
65
+ {
66
+ label: "Prompt + analyzer",
67
+ language: "python",
68
+ code: `import json
69
+ import os
70
+ from openai import OpenAI
71
+
72
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
73
+
74
+ SYSTEM_PROMPT = """You are a senior equity research analyst.
75
+ Analyze financial news and predict the most likely stock move for the NEXT TRADING DAY.
76
+ Respond only with JSON: {signal, confidence, reasoning, key_factors}"""
77
+
78
+ def analyze_news(news_text: str, ticker: str = "") -> dict:
79
+ response = client.chat.completions.create(
80
+ model="gpt-4o-mini",
81
+ temperature=0.1,
82
+ response_format={"type": "json_object"},
83
+ messages=[
84
+ {"role": "system", "content": SYSTEM_PROMPT},
85
+ {"role": "user", "content": f"Ticker: {ticker}\\nNews: {news_text}"},
86
+ ],
87
+ )
88
+ return json.loads(response.choices[0].message.content)`,
89
+ },
90
+ ],
91
+ },
92
+ {
93
+ level: "Level 2",
94
+ title: "Backtest it so the prompt has to face prices",
95
+ summary:
96
+ "Join dated news to next-day returns, convert returns into labeled outcomes, and compute whether the signals would have helped.",
97
+ outcome:
98
+ "You stop guessing whether the prompt is useful and start measuring a real signal-to-result pipeline.",
99
+ highlights: [
100
+ "Classification accuracy is not enough; you still need a trade filter and return series.",
101
+ "A confidence threshold is often the first useful lever.",
102
+ "Multi-ticker scans show whether the workflow generalizes beyond one name.",
103
+ ],
104
+ codeSamples: [
105
+ {
106
+ label: "Dataset + backtest core",
107
+ language: "python",
108
+ code: `import numpy as np
109
+ import pandas as pd
110
+ import yfinance as yf
111
+
112
+ def build_backtest_dataset(ticker: str, news_list: list[dict]) -> pd.DataFrame:
113
+ stock = yf.download(ticker, start="2024-01-01", end="2025-12-31")
114
+ stock["next_day_return"] = stock["Close"].pct_change().shift(-1)
115
+ rows = []
116
+
117
+ for item in news_list:
118
+ date = pd.Timestamp(item["date"])
119
+ future = stock.loc[stock.index >= date, "next_day_return"]
120
+ if future.empty:
121
+ continue
122
+ ret = float(future.iloc[0])
123
+ actual = "BULLISH" if ret > 0.005 else ("BEARISH" if ret < -0.005 else "NEUTRAL")
124
+ pred = analyze_news(item["text"], ticker)
125
+ rows.append({"actual_return": ret, "actual_signal": actual, **pred})
126
+
127
+ return pd.DataFrame(rows)
128
+
129
+ def run_backtest(df: pd.DataFrame) -> dict:
130
+ accuracy = (df["signal"] == df["actual_signal"]).mean()
131
+ returns = np.where((df["signal"] == "BULLISH") & (df["confidence"] >= 6), df["actual_return"], 0.0)
132
+ sharpe = (returns.mean() / (returns.std() + 1e-9)) * np.sqrt(252)
133
+ return {"accuracy": float(accuracy), "sharpe": float(sharpe)}`,
134
+ },
135
+ ],
136
+ },
137
+ {
138
+ level: "Level 3",
139
+ title: "Fine-tune a small model on your exact task",
140
+ summary:
141
+ "Build a date-split dataset, then use 4-bit loading plus LoRA adapters to teach a local model your label format and financial framing.",
142
+ outcome:
143
+ "You get a portable adapter tuned for your signal format, your examples, and your evaluation loop.",
144
+ highlights: [
145
+ "Date-based train, validation, and test splits matter more than almost any hyperparameter choice.",
146
+ "LoRA lets you adapt a 7B model without retraining every parameter.",
147
+ "Fine-tuning teaches the task behavior; it does not replace evaluation.",
148
+ ],
149
+ codeSamples: [
150
+ {
151
+ label: "Chronological dataset builder",
152
+ language: "python",
153
+ code: `import json
154
+ from pathlib import Path
155
+
156
+ class TradingDatasetBuilder:
157
+ def __init__(self, output_dir="./dataset"):
158
+ self.output_dir = Path(output_dir)
159
+ self.output_dir.mkdir(exist_ok=True)
160
+ self.examples = []
161
+
162
+ def add_news_with_prices(self, news_items: list[dict], ticker: str):
163
+ stock = yf.download(ticker, start="2023-01-01", end="2025-12-31", progress=False)
164
+ stock["ret"] = stock["Close"].pct_change().shift(-1)
165
+
166
+ for item in news_items:
167
+ date = pd.Timestamp(item["date"])
168
+ future = stock[stock.index >= date]
169
+ if future.empty:
170
+ continue
171
+ ret = float(future["ret"].iloc[0])
172
+ signal = "BULLISH" if ret > 0.01 else ("BEARISH" if ret < -0.01 else "NEUTRAL")
173
+ self.examples.append({"input": item["text"], "output": json.dumps({"signal": signal}), "_date": item["date"]})
174
+
175
+ def save(self):
176
+ self.examples.sort(key=lambda row: row["_date"])
177
+ clean = lambda rows: [{k: v for k, v in row.items() if not k.startswith("_")} for row in rows]
178
+ n = len(self.examples)
179
+ splits = {
180
+ "train": clean(self.examples[: int(n * 0.85)]),
181
+ "val": clean(self.examples[int(n * 0.85) : int(n * 0.95)]),
182
+ "test": clean(self.examples[int(n * 0.95) :]),
183
+ }
184
+ for name, rows in splits.items():
185
+ with open(self.output_dir / f"{name}.json", "w", encoding="utf-8") as handle:
186
+ json.dump(rows, handle, indent=2)`,
187
+ },
188
+ ],
189
+ },
190
+ {
191
+ level: "Level 4",
192
+ title: "Add RAG so the model knows what happened today",
193
+ summary:
194
+ "Use embeddings plus a vector store to retrieve recent market context and inject it at inference time.",
195
+ outcome:
196
+ "Your model keeps its learned analysis style while receiving fresh context from current articles and watchlist-specific news.",
197
+ highlights: [
198
+ "Fine-tuning teaches the model how to think; retrieval gives it the latest facts.",
199
+ "Recent context matters because reactions depend on what changed, not just what was said.",
200
+ "A lightweight FAISS stack is enough to test whether retrieval improves calibration.",
201
+ ],
202
+ codeSamples: [
203
+ {
204
+ label: "Financial RAG skeleton",
205
+ language: "python",
206
+ code: `import json
207
+ from langchain_community.embeddings import HuggingFaceEmbeddings
208
+ from langchain_community.vectorstores import FAISS
209
+
210
+ class FinancialRAG:
211
+ def __init__(self):
212
+ self.embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-en-v1.5")
213
+ self.vectorstore = None
214
+
215
+ def add_news(self, articles: list[dict]):
216
+ texts = [article["text"] for article in articles]
217
+ if self.vectorstore is None:
218
+ self.vectorstore = FAISS.from_texts(texts, self.embeddings)
219
+ else:
220
+ self.vectorstore.add_texts(texts)
221
+
222
+ def context_for(self, news: str, ticker: str = "") -> str:
223
+ docs = self.vectorstore.similarity_search(f"{ticker} {news}", k=3) if self.vectorstore else []
224
+ return "\\n---\\n".join(doc.page_content for doc in docs)`,
225
+ },
226
+ ],
227
+ },
228
+ {
229
+ level: "Level 5",
230
+ title: "Use multiple agents so every trade gets argued before it is sized",
231
+ summary:
232
+ "Break the job into specialized analysts, then synthesize their arguments with a portfolio-manager style final pass.",
233
+ outcome:
234
+ "Instead of one opaque answer, you get structured disagreement, consensus, and role-specific reasoning.",
235
+ highlights: [
236
+ "A bull analyst, bear analyst, and quant analyst is already enough to improve reasoning structure.",
237
+ "Consensus is useful as a sizing input; split decisions usually deserve less risk.",
238
+ "This is where debate starts to matter more than one-shot confidence scores.",
239
+ ],
240
+ codeSamples: [
241
+ {
242
+ label: "Minimal multi-agent trader",
243
+ language: "python",
244
+ code: `import json
245
+
246
+ class MultiAgentTrader:
247
+ def __init__(self, client, model="trading-lora"):
248
+ self.client = client
249
+ self.model = model
250
+
251
+ def _ask(self, role_prompt, news, ticker):
252
+ response = self.client.chat.completions.create(
253
+ model=self.model,
254
+ messages=[
255
+ {"role": "system", "content": role_prompt},
256
+ {"role": "user", "content": f"Ticker: {ticker}\\nNews: {news}\\nJSON only."},
257
+ ],
258
+ )
259
+ return json.loads(response.choices[0].message.content)
260
+
261
+ def analyze(self, news, ticker):
262
+ bull = self._ask("BULLISH analyst. Find every positive signal.", news, ticker)
263
+ bear = self._ask("BEARISH analyst. Find every risk.", news, ticker)
264
+ quant = self._ask("QUANT analyst. Only numbers and measurable deltas.", news, ticker)
265
+ return {"bull": bull, "bear": bear, "quant": quant}`,
266
+ },
267
+ ],
268
+ },
269
+ {
270
+ level: "Level 6",
271
+ title: "Production means monitoring, risk controls, and paper execution first",
272
+ summary:
273
+ "Serve the model behind an OpenAI-compatible API, monitor drift, gate trades through hard risk rules, and start on paper.",
274
+ outcome:
275
+ "You end with a system that can say stop, reduce size, or skip entirely when signal quality degrades.",
276
+ highlights: [
277
+ "A model monitor should be able to halt the system when rolling quality drops too far.",
278
+ "Risk management is a separate service boundary, not a prompt afterthought.",
279
+ "Paper trading is the proving ground; live execution is an explicit upgrade.",
280
+ ],
281
+ codeSamples: [
282
+ {
283
+ label: "Monitor + risk layer",
284
+ language: "python",
285
+ code: `from collections import deque
286
+ from dataclasses import dataclass
287
+ import numpy as np
288
+
289
+ class ModelMonitor:
290
+ def __init__(self, window=100):
291
+ self.predictions = deque(maxlen=window)
292
+ self.actuals = deque(maxlen=window)
293
+ self.returns = deque(maxlen=window)
294
+ self.baseline_accuracy = 0.68
295
+
296
+ def status(self):
297
+ if len(self.predictions) < 30:
298
+ return {"status": "WARMING_UP"}
299
+ acc = sum(p == a for p, a in zip(self.predictions, self.actuals)) / len(self.predictions)
300
+ sharpe = (np.mean(self.returns) / (np.std(self.returns) + 1e-9)) * np.sqrt(252)
301
+ if acc < self.baseline_accuracy * 0.8 or sharpe < 0:
302
+ return {"status": "HALT"}
303
+ if acc < self.baseline_accuracy * 0.9:
304
+ return {"status": "CAUTION"}
305
+ return {"status": "HEALTHY"}
306
+
307
+ @dataclass
308
+ class TradeOrder:
309
+ ticker: str
310
+ side: str
311
+ size: float
312
+ stop_loss: float
313
+ take_profit: float
314
+ reason: str`,
315
+ },
316
+ ],
317
+ },
318
+ ];
319
+
320
+ export const guideInstallCommands = [
321
+ {
322
+ label: "Python starter stack",
323
+ command: "pip install openai yfinance pandas numpy scikit-learn",
324
+ },
325
+ {
326
+ label: "OpenTradex onboarding",
327
+ command: "npm install -g opentradex@latest && opentradex onboard",
328
+ },
329
+ {
330
+ label: "Alt package flow",
331
+ command: "npx opentradex@latest onboard",
332
+ },
333
+ {
334
+ label: "Hosted installer",
335
+ command: "curl -fsSL https://opentradex.vercel.app/install.sh | bash",
336
+ },
337
+ ];
@@ -113,6 +113,36 @@ export interface StreamLine {
113
113
  result?: string;
114
114
  }
115
115
 
116
+ export interface WorkspaceSummary {
117
+ isDemo: boolean;
118
+ runtime: string;
119
+ packageManager: string;
120
+ mode: string;
121
+ primaryMarket: string;
122
+ enabledMarkets: string[];
123
+ integrations: string[];
124
+ dashboardSurface: string;
125
+ channels: string[];
126
+ tradingview: {
127
+ enabled: boolean;
128
+ watchlist: string[];
129
+ connectorMode: "watchlist" | "mcp";
130
+ mcpEnabled: boolean;
131
+ transport: "stdio" | "http";
132
+ command?: string;
133
+ args?: string;
134
+ url?: string;
135
+ configured: boolean;
136
+ };
137
+ }
138
+
139
+ export interface PromptEntry {
140
+ id: string;
141
+ text: string;
142
+ channel: string;
143
+ createdAt: string;
144
+ }
145
+
116
146
  export function kalshiUrl(ticker: string, title?: string): string {
117
147
  const parts = ticker.split("-");
118
148
  const event = parts[0].toLowerCase();