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,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: (
|
|
13
|
+
onRationaleChange: (value: string) => void;
|
|
12
14
|
onSubmitRationale: () => void;
|
|
13
15
|
onRunCycle: () => void;
|
|
14
16
|
onStartLoop: () => void;
|
|
15
17
|
onRefresh: () => void;
|
|
16
|
-
onLoopIntervalChange: (
|
|
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="
|
|
41
|
-
<div className="flex
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
+
}
|
package/web/src/lib/demo-data.ts
CHANGED
|
@@ -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
|
+
];
|
package/web/src/lib/types.ts
CHANGED
|
@@ -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();
|