chore: update project files
This commit is contained in:
parent
1e81e2034e
commit
1cc2a3edde
|
|
@ -1,6 +1,6 @@
|
|||
# LLM Providers (set the one you use)
|
||||
OPENAI_API_KEY=
|
||||
GOOGLE_API_KEY=
|
||||
GOOGLE_API_KEY=AIzaSyAunXkbkwv05l7BftH2GpmcDbHJZXSGzCg
|
||||
ANTHROPIC_API_KEY=
|
||||
XAI_API_KEY=
|
||||
OPENROUTER_API_KEY=
|
||||
|
|
|
|||
|
|
@ -217,3 +217,7 @@ __marimo__/
|
|||
|
||||
# Cache
|
||||
**/data_cache/
|
||||
|
||||
# Frontend
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
|
|
|
|||
30
README.md
30
README.md
|
|
@ -158,6 +158,36 @@ An interface will appear showing results as they load, letting you track the age
|
|||
<img src="assets/cli/cli_transaction.png" width="100%" style="display: inline-block; margin: 0 2%;">
|
||||
</p>
|
||||
|
||||
### Web API + React Frontend
|
||||
|
||||
The existing CLI remains unchanged. You can now also run TradingAgents through a lightweight HTTP API and a React frontend.
|
||||
|
||||
Start the API server:
|
||||
```bash
|
||||
uvicorn tradingagents.api.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
```
|
||||
|
||||
In another terminal, start the frontend:
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open `http://localhost:5173` in your browser.
|
||||
|
||||
API endpoints:
|
||||
- `GET /api/health`
|
||||
- `GET /api/options`
|
||||
- `POST /api/analysis/jobs`
|
||||
- `GET /api/analysis/jobs/{job_id}`
|
||||
|
||||
The frontend defaults to `http://127.0.0.1:8000` for API calls.
|
||||
Override with:
|
||||
```bash
|
||||
VITE_API_BASE_URL=http://127.0.0.1:8000 npm run dev
|
||||
```
|
||||
|
||||
## TradingAgents Package
|
||||
|
||||
### Implementation Details
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=IBM+Plex+Mono:wght@400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>TradingAgents Web Console</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "tradingagents-frontend",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.19",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^5.4.10"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,345 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
type JobStatus = "queued" | "running" | "completed" | "failed";
|
||||
|
||||
type ProviderOption = {
|
||||
id: string;
|
||||
base_url: string;
|
||||
models: string[];
|
||||
};
|
||||
|
||||
type OptionsResponse = {
|
||||
providers: ProviderOption[];
|
||||
analysts: string[];
|
||||
research_depths: number[];
|
||||
};
|
||||
|
||||
type AnalysisJob = {
|
||||
id: string;
|
||||
status: JobStatus;
|
||||
created_at: string;
|
||||
started_at?: string;
|
||||
completed_at?: string;
|
||||
request: {
|
||||
ticker: string;
|
||||
analysis_date: string;
|
||||
analysts: string[];
|
||||
research_depth: number;
|
||||
llm_provider: string;
|
||||
quick_think_llm?: string;
|
||||
deep_think_llm?: string;
|
||||
backend_url?: string;
|
||||
google_thinking_level?: string;
|
||||
openai_reasoning_effort?: string;
|
||||
};
|
||||
result?: {
|
||||
ticker: string;
|
||||
analysis_date: string;
|
||||
decision: string;
|
||||
final_trade_decision: string;
|
||||
investment_plan: string;
|
||||
reports: Record<string, string | null>;
|
||||
};
|
||||
error?: string;
|
||||
};
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL || "http://127.0.0.1:8000";
|
||||
|
||||
function todayIso(): string {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [options, setOptions] = useState<OptionsResponse | null>(null);
|
||||
const [loadingOptions, setLoadingOptions] = useState(true);
|
||||
const [ticker, setTicker] = useState("SPY");
|
||||
const [analysisDate, setAnalysisDate] = useState(todayIso());
|
||||
const [researchDepth, setResearchDepth] = useState(1);
|
||||
const [provider, setProvider] = useState("google");
|
||||
const [selectedAnalysts, setSelectedAnalysts] = useState<string[]>([
|
||||
"market",
|
||||
"social",
|
||||
"news",
|
||||
"fundamentals",
|
||||
]);
|
||||
const [quickThink, setQuickThink] = useState("");
|
||||
const [deepThink, setDeepThink] = useState("");
|
||||
const [googleThinkingLevel, setGoogleThinkingLevel] = useState("high");
|
||||
const [job, setJob] = useState<AnalysisJob | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const providers = options?.providers ?? [];
|
||||
const providerMeta = useMemo(
|
||||
() => providers.find((item) => item.id === provider),
|
||||
[provider, providers]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/options`);
|
||||
const data: OptionsResponse = await response.json();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setOptions(data);
|
||||
const google = data.providers.find((item) => item.id === "google");
|
||||
if (google && google.models.length > 0) {
|
||||
setQuickThink(google.models[0]);
|
||||
setDeepThink(google.models[0]);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setLoadingOptions(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!providerMeta?.models?.length) {
|
||||
return;
|
||||
}
|
||||
if (!providerMeta.models.includes(quickThink)) {
|
||||
setQuickThink(providerMeta.models[0]);
|
||||
}
|
||||
if (!providerMeta.models.includes(deepThink)) {
|
||||
setDeepThink(providerMeta.models[0]);
|
||||
}
|
||||
}, [providerMeta, quickThink, deepThink]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!job || (job.status !== "queued" && job.status !== "running")) {
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(async () => {
|
||||
const response = await fetch(`${API_BASE}/api/analysis/jobs/${job.id}`);
|
||||
const updated = (await response.json()) as AnalysisJob;
|
||||
setJob(updated);
|
||||
}, 2500);
|
||||
return () => clearTimeout(timer);
|
||||
}, [job]);
|
||||
|
||||
const toggleAnalyst = (value: string) => {
|
||||
setSelectedAnalysts((current) => {
|
||||
if (current.includes(value)) {
|
||||
const next = current.filter((item) => item !== value);
|
||||
return next.length ? next : current;
|
||||
}
|
||||
return [...current, value];
|
||||
});
|
||||
};
|
||||
|
||||
const submitJob = async () => {
|
||||
setIsSubmitting(true);
|
||||
setJob(null);
|
||||
try {
|
||||
const payload = {
|
||||
ticker,
|
||||
analysis_date: analysisDate,
|
||||
analysts: selectedAnalysts,
|
||||
research_depth: researchDepth,
|
||||
llm_provider: provider,
|
||||
backend_url: providerMeta?.base_url,
|
||||
quick_think_llm: quickThink,
|
||||
deep_think_llm: deepThink,
|
||||
google_thinking_level: provider === "google" ? googleThinkingLevel : null,
|
||||
};
|
||||
const response = await fetch(`${API_BASE}/api/analysis/jobs`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed (${response.status})`);
|
||||
}
|
||||
const created = (await response.json()) as AnalysisJob;
|
||||
setJob(created);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
setJob({
|
||||
id: "n/a",
|
||||
status: "failed",
|
||||
created_at: new Date().toISOString(),
|
||||
request: {
|
||||
ticker,
|
||||
analysis_date: analysisDate,
|
||||
analysts: selectedAnalysts,
|
||||
research_depth: researchDepth,
|
||||
llm_provider: provider,
|
||||
},
|
||||
error: message,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="shell">
|
||||
<header className="hero">
|
||||
<div>
|
||||
<h1>TradingAgents Web Console</h1>
|
||||
<p>
|
||||
Keep the current backend logic intact and orchestrate runs through HTTP APIs.
|
||||
Submit a job, monitor state transitions, and inspect the final decision package.
|
||||
</p>
|
||||
</div>
|
||||
<span className="badge">React + FastAPI</span>
|
||||
</header>
|
||||
|
||||
<section className="grid">
|
||||
<aside className="panel stack">
|
||||
<h2>Run Setup</h2>
|
||||
|
||||
<label>
|
||||
Ticker
|
||||
<input value={ticker} onChange={(e) => setTicker(e.target.value.toUpperCase())} />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Analysis Date
|
||||
<input
|
||||
type="date"
|
||||
value={analysisDate}
|
||||
onChange={(e) => setAnalysisDate(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Provider
|
||||
<select value={provider} onChange={(e) => setProvider(e.target.value)}>
|
||||
{providers.map((item) => (
|
||||
<option value={item.id} key={item.id}>
|
||||
{item.id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Quick-Thinking Model
|
||||
<select value={quickThink} onChange={(e) => setQuickThink(e.target.value)}>
|
||||
{providerMeta?.models?.map((model) => (
|
||||
<option value={model} key={model}>
|
||||
{model}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Deep-Thinking Model
|
||||
<select value={deepThink} onChange={(e) => setDeepThink(e.target.value)}>
|
||||
{providerMeta?.models?.map((model) => (
|
||||
<option value={model} key={model}>
|
||||
{model}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{provider === "google" && (
|
||||
<label>
|
||||
Gemini Thinking Mode
|
||||
<select
|
||||
value={googleThinkingLevel}
|
||||
onChange={(e) => setGoogleThinkingLevel(e.target.value)}
|
||||
>
|
||||
<option value="high">high</option>
|
||||
<option value="minimal">minimal</option>
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
|
||||
<label>
|
||||
Research Depth
|
||||
<div className="depth">
|
||||
{[1, 3, 5].map((depth) => (
|
||||
<button
|
||||
key={depth}
|
||||
type="button"
|
||||
className={researchDepth === depth ? "active" : ""}
|
||||
onClick={() => setResearchDepth(depth)}
|
||||
>
|
||||
{depth === 1 ? "Shallow" : depth === 3 ? "Medium" : "Deep"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div className="stack">
|
||||
<span className="mono">Analyst Team</span>
|
||||
<div className="checks">
|
||||
{(options?.analysts || []).map((analyst) => (
|
||||
<label key={analyst}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedAnalysts.includes(analyst)}
|
||||
onChange={() => toggleAnalyst(analyst)}
|
||||
/>
|
||||
{analyst}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="run" type="button" onClick={submitJob} disabled={isSubmitting || loadingOptions}>
|
||||
{isSubmitting ? "Submitting..." : "Start Analysis Job"}
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<article className="panel">
|
||||
<h3>Execution</h3>
|
||||
<p className="mono">API: {API_BASE}</p>
|
||||
|
||||
{job ? (
|
||||
<>
|
||||
<div className="status">
|
||||
<span className={`dot ${job.status}`} />
|
||||
{job.status}
|
||||
</div>
|
||||
<p className="mono">Job ID: {job.id}</p>
|
||||
<p>
|
||||
{job.request.ticker} on {job.request.analysis_date} with {job.request.llm_provider}
|
||||
</p>
|
||||
|
||||
{job.status === "failed" && (
|
||||
<div className="card error pre">{job.error || "Job failed"}</div>
|
||||
)}
|
||||
|
||||
{job.result && (
|
||||
<div className="result">
|
||||
<div className="card">
|
||||
<h4>Decision</h4>
|
||||
<div className="pre">{job.result.decision}</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<h4>Final Trade Decision</h4>
|
||||
<div className="pre">{String(job.result.final_trade_decision || "")}</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<h4>Investment Plan</h4>
|
||||
<div className="pre">{String(job.result.investment_plan || "")}</div>
|
||||
</div>
|
||||
{Object.entries(job.result.reports || {}).map(([key, value]) => (
|
||||
<div className="card" key={key}>
|
||||
<h4>{key}</h4>
|
||||
<div className="pre">{String(value || "")}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p>Submit a run to see live job status and result artifacts here.</p>
|
||||
)}
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./styles.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
:root {
|
||||
--bg-0: #061824;
|
||||
--bg-1: #0d2b40;
|
||||
--card: rgba(9, 22, 35, 0.74);
|
||||
--card-line: rgba(127, 214, 192, 0.22);
|
||||
--text: #e8f7f2;
|
||||
--muted: #9ec8bc;
|
||||
--accent: #7fd6c0;
|
||||
--accent-2: #f5b971;
|
||||
--danger: #f27575;
|
||||
--ok: #7ee081;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
color: var(--text);
|
||||
font-family: "Space Grotesk", sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at 20% 15%, rgba(127, 214, 192, 0.15), transparent 40%),
|
||||
radial-gradient(circle at 78% 12%, rgba(245, 185, 113, 0.22), transparent 42%),
|
||||
linear-gradient(160deg, var(--bg-0) 0%, var(--bg-1) 60%, #112c22 100%);
|
||||
}
|
||||
|
||||
.shell {
|
||||
max-width: 1160px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 18px 56px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: end;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
animation: rise 0.6s ease;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(1.8rem, 4.6vw, 3.1rem);
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
margin: 8px 0 0;
|
||||
color: var(--muted);
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-family: "IBM Plex Mono", monospace;
|
||||
color: #091623;
|
||||
background: linear-gradient(90deg, var(--accent), var(--accent-2));
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.82rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(300px, 390px) 1fr;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border: 1px solid var(--card-line);
|
||||
background: var(--card);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 18px;
|
||||
padding: 18px;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03), 0 22px 44px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.panel h2,
|
||||
.panel h3 {
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.stack {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
color: var(--muted);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
button,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(127, 214, 192, 0.28);
|
||||
padding: 10px;
|
||||
color: var(--text);
|
||||
background: rgba(7, 18, 30, 0.7);
|
||||
}
|
||||
|
||||
.depth {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.depth button,
|
||||
.run {
|
||||
border: 1px solid rgba(127, 214, 192, 0.45);
|
||||
color: var(--text);
|
||||
background: rgba(12, 30, 45, 0.85);
|
||||
padding: 9px 10px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.depth button.active {
|
||||
background: linear-gradient(100deg, rgba(127, 214, 192, 0.24), rgba(245, 185, 113, 0.21));
|
||||
border-color: rgba(245, 185, 113, 0.74);
|
||||
}
|
||||
|
||||
.depth button:hover,
|
||||
.run:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.run {
|
||||
margin-top: 8px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(100deg, rgba(127, 214, 192, 0.2), rgba(245, 185, 113, 0.14));
|
||||
}
|
||||
|
||||
.run:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.checks {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.checks label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(127, 214, 192, 0.17);
|
||||
background: rgba(4, 16, 26, 0.5);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: "IBM Plex Mono", monospace;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(127, 214, 192, 0.35);
|
||||
background: rgba(7, 20, 30, 0.7);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.status .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.dot.queued,
|
||||
.dot.running {
|
||||
background: var(--accent-2);
|
||||
}
|
||||
|
||||
.dot.completed {
|
||||
background: var(--ok);
|
||||
}
|
||||
|
||||
.dot.failed {
|
||||
background: var(--danger);
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 14px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid rgba(127, 214, 192, 0.18);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
background: rgba(6, 18, 28, 0.66);
|
||||
}
|
||||
|
||||
.card h4 {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.pre {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.45;
|
||||
color: #dbf6ee;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ffe4e4;
|
||||
border-color: rgba(242, 117, 117, 0.46);
|
||||
background: rgba(80, 22, 22, 0.55);
|
||||
}
|
||||
|
||||
@keyframes rise {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.checks {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5173,
|
||||
host: true,
|
||||
},
|
||||
});
|
||||
|
|
@ -12,6 +12,7 @@ dependencies = [
|
|||
"langchain-core>=0.3.81",
|
||||
"backtrader>=1.9.78.123",
|
||||
"chainlit>=2.5.5",
|
||||
"fastapi>=0.118.0",
|
||||
"langchain-anthropic>=0.3.15",
|
||||
"langchain-experimental>=0.3.4",
|
||||
"langchain-google-genai>=2.1.5",
|
||||
|
|
@ -30,6 +31,7 @@ dependencies = [
|
|||
"stockstats>=0.6.5",
|
||||
"tqdm>=4.67.1",
|
||||
"typing-extensions>=4.14.0",
|
||||
"uvicorn>=0.37.0",
|
||||
"yfinance>=0.2.63",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -20,3 +20,5 @@ typer
|
|||
questionary
|
||||
langchain_anthropic
|
||||
langchain-google-genai
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
I apologize, but "gold" is not a valid ticker symbol. Could you please provide the correct ticker symbol for the company you would like me to analyze?
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
"""HTTP API package for TradingAgents."""
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
import json
|
||||
import threading
|
||||
import traceback
|
||||
import uuid
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Any, Dict, List, Literal, Optional
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from tradingagents.default_config import DEFAULT_CONFIG
|
||||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||
from tradingagents.llm_clients.validators import VALID_MODELS
|
||||
|
||||
load_dotenv()
|
||||
|
||||
APP_TITLE = "TradingAgents API"
|
||||
ANALYST_ORDER = ["market", "social", "news", "fundamentals"]
|
||||
PROVIDER_BASE_URLS = {
|
||||
"openai": "https://api.openai.com/v1",
|
||||
"google": "https://generativelanguage.googleapis.com/v1",
|
||||
"anthropic": "https://api.anthropic.com/",
|
||||
"xai": "https://api.x.ai/v1",
|
||||
"openrouter": "https://openrouter.ai/api/v1",
|
||||
"ollama": "http://localhost:11434/v1",
|
||||
}
|
||||
|
||||
|
||||
class AnalysisRequest(BaseModel):
|
||||
ticker: str = Field(..., min_length=1, max_length=12)
|
||||
analysis_date: str = Field(default_factory=lambda: dt.date.today().isoformat())
|
||||
analysts: List[str] = Field(default_factory=lambda: ANALYST_ORDER.copy())
|
||||
research_depth: Literal[1, 3, 5] = 1
|
||||
llm_provider: str = "google"
|
||||
quick_think_llm: Optional[str] = None
|
||||
deep_think_llm: Optional[str] = None
|
||||
backend_url: Optional[str] = None
|
||||
google_thinking_level: Optional[str] = "high"
|
||||
openai_reasoning_effort: Optional[str] = None
|
||||
|
||||
@field_validator("ticker")
|
||||
@classmethod
|
||||
def validate_ticker(cls, value: str) -> str:
|
||||
ticker = value.strip().upper()
|
||||
if not ticker:
|
||||
raise ValueError("ticker cannot be empty")
|
||||
return ticker
|
||||
|
||||
@field_validator("analysis_date")
|
||||
@classmethod
|
||||
def validate_date(cls, value: str) -> str:
|
||||
try:
|
||||
dt.date.fromisoformat(value)
|
||||
except ValueError as exc:
|
||||
raise ValueError("analysis_date must be YYYY-MM-DD") from exc
|
||||
return value
|
||||
|
||||
@field_validator("analysts")
|
||||
@classmethod
|
||||
def validate_analysts(cls, value: List[str]) -> List[str]:
|
||||
normalized = [item.strip().lower() for item in value if item.strip()]
|
||||
if not normalized:
|
||||
raise ValueError("analysts must include at least one analyst")
|
||||
invalid = [item for item in normalized if item not in ANALYST_ORDER]
|
||||
if invalid:
|
||||
raise ValueError(f"invalid analysts: {', '.join(invalid)}")
|
||||
return normalized
|
||||
|
||||
@field_validator("llm_provider")
|
||||
@classmethod
|
||||
def validate_provider(cls, value: str) -> str:
|
||||
provider = value.strip().lower()
|
||||
if provider not in PROVIDER_BASE_URLS:
|
||||
raise ValueError(f"unsupported provider: {provider}")
|
||||
return provider
|
||||
|
||||
|
||||
class AnalysisJob(BaseModel):
|
||||
id: str
|
||||
status: Literal["queued", "running", "completed", "failed"]
|
||||
created_at: str
|
||||
started_at: Optional[str] = None
|
||||
completed_at: Optional[str] = None
|
||||
request: AnalysisRequest
|
||||
result: Optional[Dict[str, Any]] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
app = FastAPI(title=APP_TITLE, version="0.1.0")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
_jobs_lock = threading.Lock()
|
||||
_jobs: Dict[str, AnalysisJob] = {}
|
||||
_executor = ThreadPoolExecutor(max_workers=2)
|
||||
|
||||
|
||||
def _json_safe(value: Any) -> Any:
|
||||
return json.loads(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def _default_model(provider: str) -> str:
|
||||
models = VALID_MODELS.get(provider)
|
||||
if models:
|
||||
return models[0]
|
||||
if provider == "ollama":
|
||||
return "qwen3:latest"
|
||||
return "gpt-5-mini"
|
||||
|
||||
|
||||
def _build_config(request: AnalysisRequest) -> Dict[str, Any]:
|
||||
config = DEFAULT_CONFIG.copy()
|
||||
config["max_debate_rounds"] = request.research_depth
|
||||
config["max_risk_discuss_rounds"] = request.research_depth
|
||||
config["llm_provider"] = request.llm_provider
|
||||
config["backend_url"] = request.backend_url or PROVIDER_BASE_URLS[request.llm_provider]
|
||||
config["quick_think_llm"] = request.quick_think_llm or _default_model(request.llm_provider)
|
||||
config["deep_think_llm"] = request.deep_think_llm or _default_model(request.llm_provider)
|
||||
config["google_thinking_level"] = request.google_thinking_level
|
||||
config["openai_reasoning_effort"] = request.openai_reasoning_effort
|
||||
return config
|
||||
|
||||
|
||||
def _run_job(job_id: str) -> None:
|
||||
with _jobs_lock:
|
||||
job = _jobs[job_id]
|
||||
job.status = "running"
|
||||
job.started_at = dt.datetime.utcnow().isoformat() + "Z"
|
||||
|
||||
try:
|
||||
config = _build_config(job.request)
|
||||
selected_analysts = [a for a in ANALYST_ORDER if a in job.request.analysts]
|
||||
graph = TradingAgentsGraph(
|
||||
selected_analysts=selected_analysts,
|
||||
debug=False,
|
||||
config=config,
|
||||
)
|
||||
final_state, decision = graph.propagate(
|
||||
job.request.ticker,
|
||||
job.request.analysis_date,
|
||||
)
|
||||
|
||||
result = {
|
||||
"ticker": job.request.ticker,
|
||||
"analysis_date": job.request.analysis_date,
|
||||
"decision": decision,
|
||||
"final_trade_decision": _json_safe(final_state.get("final_trade_decision")),
|
||||
"investment_plan": _json_safe(final_state.get("investment_plan")),
|
||||
"reports": {
|
||||
"market": _json_safe(final_state.get("market_report")),
|
||||
"sentiment": _json_safe(final_state.get("sentiment_report")),
|
||||
"news": _json_safe(final_state.get("news_report")),
|
||||
"fundamentals": _json_safe(final_state.get("fundamentals_report")),
|
||||
"trader": _json_safe(final_state.get("trader_investment_plan")),
|
||||
},
|
||||
}
|
||||
|
||||
with _jobs_lock:
|
||||
job.status = "completed"
|
||||
job.completed_at = dt.datetime.utcnow().isoformat() + "Z"
|
||||
job.result = result
|
||||
job.error = None
|
||||
except Exception:
|
||||
with _jobs_lock:
|
||||
job.status = "failed"
|
||||
job.completed_at = dt.datetime.utcnow().isoformat() + "Z"
|
||||
job.error = traceback.format_exc(limit=6)
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
def health() -> Dict[str, str]:
|
||||
return {"status": "ok", "service": APP_TITLE}
|
||||
|
||||
|
||||
@app.get("/api/options")
|
||||
def options() -> Dict[str, Any]:
|
||||
return {
|
||||
"providers": [
|
||||
{
|
||||
"id": provider,
|
||||
"base_url": base_url,
|
||||
"models": VALID_MODELS.get(provider, []),
|
||||
}
|
||||
for provider, base_url in PROVIDER_BASE_URLS.items()
|
||||
],
|
||||
"analysts": ANALYST_ORDER,
|
||||
"research_depths": [1, 3, 5],
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/analysis/jobs", response_model=AnalysisJob)
|
||||
def create_analysis_job(request: AnalysisRequest) -> AnalysisJob:
|
||||
job_id = str(uuid.uuid4())
|
||||
job = AnalysisJob(
|
||||
id=job_id,
|
||||
status="queued",
|
||||
created_at=dt.datetime.utcnow().isoformat() + "Z",
|
||||
request=request,
|
||||
)
|
||||
with _jobs_lock:
|
||||
_jobs[job_id] = job
|
||||
|
||||
_executor.submit(_run_job, job_id)
|
||||
return job
|
||||
|
||||
|
||||
@app.get("/api/analysis/jobs/{job_id}", response_model=AnalysisJob)
|
||||
def get_analysis_job(job_id: str) -> AnalysisJob:
|
||||
with _jobs_lock:
|
||||
job = _jobs.get(job_id)
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
return job
|
||||
Loading…
Reference in New Issue