Hydr473 commited on
Commit
8aafbdb
·
2 Parent(s): b899f273c3f88f

Merge branch 'main' of github-personal:CRIMSONHydra/stocker

Browse files
.gitignore CHANGED
@@ -32,8 +32,10 @@ coverage.xml
32
  .nox/
33
 
34
  # --- Stocker runtime artifacts --------------------------------------------
35
- .cache/ # council vote cache (specialist + moderator)
36
- training/runs/ # GRPO runs, eval rollouts, plots, LoRA adapters
 
 
37
  !training/runs/.gitkeep
38
  results.json
39
  *.log
@@ -42,6 +44,10 @@ results.json
42
  # 3-task dataset (data/*.parquet, data/charts/*.png) IS committed.
43
  data/corpus/
44
  data/charts/cache/
 
 
 
 
45
 
46
  # --- Notebook noise --------------------------------------------------------
47
  .ipynb_checkpoints/
 
32
  .nox/
33
 
34
  # --- Stocker runtime artifacts --------------------------------------------
35
+ # council vote cache (specialist + moderator)
36
+ .cache/
37
+ # GRPO runs, eval rollouts, plots, LoRA adapters
38
+ training/runs/
39
  !training/runs/.gitkeep
40
  results.json
41
  *.log
 
44
  # 3-task dataset (data/*.parquet, data/charts/*.png) IS committed.
45
  data/corpus/
46
  data/charts/cache/
47
+ # --- Frontend (React/Vite) -------------------------------------------------
48
+ frontend/node_modules/
49
+ frontend/dist/
50
+ frontend/.env.local
51
 
52
  # --- Notebook noise --------------------------------------------------------
53
  .ipynb_checkpoints/
CLAUDE.md CHANGED
@@ -39,6 +39,7 @@ via TRL on top of `google/gemma-4-E4B-it`.
39
  ├── server/app.py # OpenEnv entry point (`server.app:main`)
40
  ├── inference.py # council-driven OpenEnv inference loop (root)
41
  ├── client.py
 
42
  ├── scripts/ # build_dataset, render_charts, serve_vllm, validate_tasks
43
  ├── training/ # eval_rollout, train_grpo, runs/
44
  ├── tests/
@@ -47,6 +48,23 @@ via TRL on top of `google/gemma-4-E4B-it`.
47
  └── pyproject.toml
48
  ```
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  ## Conventions
51
 
52
  - All Pydantic schemas live in [app/models.py](app/models.py). Don't move
@@ -83,8 +101,6 @@ via TRL on top of `google/gemma-4-E4B-it`.
83
 
84
  ## Don't
85
 
86
- - Don't introduce a frontend build step — the HTML lives inline in
87
- [app/api/frontend.py](app/api/frontend.py).
88
  - Don't bake live API calls into specialists. Council inputs come from the
89
  bundled parquet dataset (`app/data/loader.py`). Live sources are the data
90
  builder's job.
@@ -94,3 +110,7 @@ via TRL on top of `google/gemma-4-E4B-it`.
94
  trainer's reward replay both depend on it.
95
  - Don't make specialists rely on the moderator's LoRA. Specialists must
96
  remain frozen so their cached votes are reusable across GRPO steps.
 
 
 
 
 
39
  ├── server/app.py # OpenEnv entry point (`server.app:main`)
40
  ├── inference.py # council-driven OpenEnv inference loop (root)
41
  ├── client.py
42
+ ├── frontend/ # Vite + React 19 + Tailwind v4 SPA (built artefact mounted by FastAPI)
43
  ├── scripts/ # build_dataset, render_charts, serve_vllm, validate_tasks
44
  ├── training/ # eval_rollout, train_grpo, runs/
45
  ├── tests/
 
48
  └── pyproject.toml
49
  ```
50
 
51
+ ## Frontend
52
+
53
+ - Source lives in [frontend/](frontend/) — Vite + React 19 + TypeScript +
54
+ Tailwind v4. Single `App.tsx` with six tabs (Terminal, Council, Training,
55
+ Gallery, Portfolio, Intelligence).
56
+ - Dev: `cd frontend && npm install && npm run dev` (HMR on :3000).
57
+ - Production: `cd frontend && npm run build` emits `frontend/dist/`. FastAPI
58
+ serves `frontend/dist/index.html` at `/` and `/web`, and mounts
59
+ `frontend/dist/assets/` at `/assets` (see [app/main.py](app/main.py) and
60
+ [app/api/frontend.py](app/api/frontend.py)).
61
+ - Builds are **manual**. Don't add auto-build to CI, hooks, or `npm` scripts
62
+ triggered from Python.
63
+ - When `frontend/dist/` is absent, FastAPI falls back to the inline
64
+ `FRONTEND_HTML` string in [app/api/frontend.py](app/api/frontend.py). That
65
+ fallback is the lowest-common-denominator UI for headless dev/tests and
66
+ must keep working without Node.
67
+
68
  ## Conventions
69
 
70
  - All Pydantic schemas live in [app/models.py](app/models.py). Don't move
 
101
 
102
  ## Don't
103
 
 
 
104
  - Don't bake live API calls into specialists. Council inputs come from the
105
  bundled parquet dataset (`app/data/loader.py`). Live sources are the data
106
  builder's job.
 
110
  trainer's reward replay both depend on it.
111
  - Don't make specialists rely on the moderator's LoRA. Specialists must
112
  remain frozen so their cached votes are reusable across GRPO steps.
113
+ - Don't auto-build the React frontend from Python or CI. Builds are run by
114
+ hand with `npm run build`. Don't delete the inline `FRONTEND_HTML`
115
+ fallback in [app/api/frontend.py](app/api/frontend.py) — tests and
116
+ Node-less dev rely on it.
app/api/council.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Council preview endpoint: runs the 7 specialists + moderator on the
2
+ current env state and returns the resulting CouncilDecision.
3
+
4
+ Defaults to MockLLMClient so the endpoint works on a laptop with no API
5
+ keys / GPU. The mock is deterministic per (ticker, date) and the Council
6
+ runner caches results to .cache/council, so repeat hits are instant.
7
+ """
8
+
9
+ from fastapi import APIRouter, HTTPException
10
+
11
+ from app.council.llm import MockLLMClient
12
+ from app.council.runner import Council
13
+ from app.models import CouncilDecision
14
+
15
+ router = APIRouter(tags=["council"])
16
+
17
+ _council = Council(client=MockLLMClient(), use_cache=True)
18
+
19
+
20
+ @router.get("/council", response_model=CouncilDecision)
21
+ async def get_council() -> CouncilDecision:
22
+ import app.api.env as env_module
23
+
24
+ env = env_module.current_env
25
+ if not env.is_ready():
26
+ raise HTTPException(
27
+ status_code=409,
28
+ detail="env not initialized or episode complete — call /reset first",
29
+ )
30
+ return await _council.run_async(env.current_observation())
app/api/frontend.py CHANGED
@@ -1,10 +1,19 @@
1
- """Serves the embedded HTML frontend."""
 
 
 
 
 
 
 
2
 
3
  from fastapi import APIRouter
4
- from fastapi.responses import HTMLResponse
5
 
6
  router = APIRouter(tags=["frontend"])
7
 
 
 
8
  FRONTEND_HTML = """<!DOCTYPE html>
9
  <html lang="en">
10
  <head>
@@ -119,7 +128,9 @@ async function submit() {
119
  </html>"""
120
 
121
 
122
- @router.get("/", response_class=HTMLResponse)
123
- @router.get("/web", response_class=HTMLResponse)
124
- async def index() -> HTMLResponse:
 
 
125
  return HTMLResponse(content=FRONTEND_HTML)
 
1
+ """Serves the embedded HTML frontend.
2
+
3
+ When a built React SPA exists at ``frontend/dist/index.html``, that is served at
4
+ ``/`` and ``/web``. Otherwise the inline HTML below is served as a fallback so
5
+ the API is usable without Node tooling (and so tests do not require a build).
6
+ """
7
+
8
+ from pathlib import Path
9
 
10
  from fastapi import APIRouter
11
+ from fastapi.responses import FileResponse, HTMLResponse
12
 
13
  router = APIRouter(tags=["frontend"])
14
 
15
+ DIST_INDEX = Path(__file__).resolve().parents[2] / "frontend" / "dist" / "index.html"
16
+
17
  FRONTEND_HTML = """<!DOCTYPE html>
18
  <html lang="en">
19
  <head>
 
128
  </html>"""
129
 
130
 
131
+ @router.get("/")
132
+ @router.get("/web")
133
+ async def index():
134
+ if DIST_INDEX.is_file():
135
+ return FileResponse(DIST_INDEX)
136
  return HTMLResponse(content=FRONTEND_HTML)
app/api/ohlcv.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OHLCV bars for chart rendering.
2
+
3
+ Returns the full per-task daily series (warmup + episode rows) so the chart
4
+ panel has historical context. Sourced from data/prices.parquet via
5
+ :mod:`app.data.loader`. The agent's MarketObservation is unaffected — this
6
+ endpoint is purely a UI concern.
7
+ """
8
+
9
+ from fastapi import APIRouter, HTTPException, Query
10
+
11
+ from app.data.loader import prices
12
+
13
+ router = APIRouter(tags=["ohlcv"])
14
+
15
+
16
+ @router.get("/ohlcv")
17
+ async def get_ohlcv(task_id: str = Query(...)) -> dict:
18
+ df = prices()
19
+ sub = df[df["task_id"] == task_id].sort_values("date")
20
+ if sub.empty:
21
+ raise HTTPException(status_code=404, detail=f"no price rows for task {task_id}")
22
+
23
+ ticker = str(sub["ticker"].iloc[0])
24
+ cols = ["date", "open", "high", "low", "close", "volume", "in_episode"]
25
+ bars = [
26
+ {
27
+ "time": str(r["date"]),
28
+ "open": float(r["open"]),
29
+ "high": float(r["high"]),
30
+ "low": float(r["low"]),
31
+ "close": float(r["close"]),
32
+ "volume": float(r["volume"]),
33
+ "in_episode": bool(r["in_episode"]),
34
+ }
35
+ for r in sub[cols].to_dict("records")
36
+ ]
37
+ return {"task_id": task_id, "ticker": ticker, "bars": bars}
app/api/router.py CHANGED
@@ -2,7 +2,7 @@
2
 
3
  from fastapi import APIRouter
4
 
5
- from app.api import corpus, env, health, meta, state
6
 
7
  api_router = APIRouter()
8
  api_router.include_router(health.router)
@@ -10,3 +10,6 @@ api_router.include_router(meta.router)
10
  api_router.include_router(env.router)
11
  api_router.include_router(state.router)
12
  api_router.include_router(corpus.router)
 
 
 
 
2
 
3
  from fastapi import APIRouter
4
 
5
+ from app.api import corpus, council, env, health, meta, ohlcv, state, training
6
 
7
  api_router = APIRouter()
8
  api_router.include_router(health.router)
 
10
  api_router.include_router(env.router)
11
  api_router.include_router(state.router)
12
  api_router.include_router(corpus.router)
13
+ api_router.include_router(ohlcv.router)
14
+ api_router.include_router(council.router)
15
+ api_router.include_router(training.router)
app/api/training.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Training metrics endpoint: surfaces what is actually on disk in
2
+ ``training/runs/<run>/`` — currently a per-task ``summary.csv`` with
3
+ total_reward / final_portfolio / buy_and_hold / alpha_pct columns plus PNG
4
+ curve plots saved by ``training/eval_rollout.py``.
5
+ """
6
+
7
+ import csv
8
+ from pathlib import Path
9
+
10
+ from fastapi import APIRouter
11
+
12
+ router = APIRouter(tags=["training"])
13
+
14
+ RUNS_DIR = Path(__file__).resolve().parents[2] / "training" / "runs"
15
+
16
+
17
+ def _latest_run() -> Path | None:
18
+ if not RUNS_DIR.is_dir():
19
+ return None
20
+ candidates = [p for p in RUNS_DIR.iterdir() if p.is_dir()]
21
+ if not candidates:
22
+ return None
23
+ return max(candidates, key=lambda p: p.stat().st_mtime)
24
+
25
+
26
+ @router.get("/training/metrics")
27
+ async def get_metrics() -> dict:
28
+ run = _latest_run()
29
+ if run is None:
30
+ return {"status": "no_runs", "summary": [], "mean_alpha_pct": 0.0}
31
+
32
+ summary_path = run / "summary.csv"
33
+ summary: list[dict] = []
34
+ if summary_path.is_file():
35
+ with summary_path.open() as f:
36
+ for row in csv.DictReader(f):
37
+ summary.append({
38
+ "task_id": row["task_id"],
39
+ "total_reward": float(row["total_reward"]),
40
+ "final_portfolio": float(row["final_portfolio"]),
41
+ "buy_and_hold": float(row["buy_and_hold"]),
42
+ "alpha_pct": float(row["alpha_pct"]),
43
+ })
44
+
45
+ mean_alpha = (
46
+ sum(r["alpha_pct"] for r in summary) / len(summary) if summary else 0.0
47
+ )
48
+
49
+ def _png(name: str) -> str | None:
50
+ p = run / name
51
+ return f"/training/runs/{run.name}/{name}" if p.is_file() else None
52
+
53
+ return {
54
+ "status": "completed" if summary else "no_runs",
55
+ "run_name": run.name,
56
+ "summary": summary,
57
+ "mean_alpha_pct": round(mean_alpha, 2),
58
+ "reward_curve_png": _png("reward_curve.png"),
59
+ "portfolio_curve_png": _png("portfolio_curve.png"),
60
+ }
app/config.py CHANGED
@@ -8,6 +8,11 @@ class Settings(BaseSettings):
8
  allow_origins: list[str] = ["*"]
9
  port: int = 7860
10
 
 
 
 
 
 
11
  model_config = {"env_prefix": "STOCKER_", "case_sensitive": False}
12
 
13
 
 
8
  allow_origins: list[str] = ["*"]
9
  port: int = 7860
10
 
11
+ transaction_cost_rate: float = 0.001
12
+ annual_inflation_rate: float = 0.05
13
+ reward_weight_performance: float = 0.7
14
+ reward_weight_inflation: float = 0.3
15
+
16
  model_config = {"env_prefix": "STOCKER_", "case_sensitive": False}
17
 
18
 
app/core/environment.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
 
4
  import uuid
5
 
 
6
  from app.core.graders import compute_step_reward, compute_trajectory_bonus
7
  from app.core.tasks import get_task_definition
8
  from app.data import loader
@@ -85,16 +86,19 @@ class StockerEnv:
85
  )
86
 
87
  price = self._prices[self._current_index]
88
- prev_portfolio = self._cash + self._position * price
89
  invalid = self._apply_action(action, price)
90
  new_portfolio = self._cash + self._position * price
91
 
92
  result = compute_step_reward(
93
  action=action,
94
- prev_portfolio=prev_portfolio,
95
  new_portfolio=new_portfolio,
96
  starting_cash=float(self._task["starting_cash"]),
97
  invalid=invalid,
 
 
 
 
 
98
  )
99
  reward = result.score
100
 
@@ -134,6 +138,19 @@ class StockerEnv:
134
  },
135
  )
136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  # ------------------------------------------------------------------ state
138
  def state(self) -> EnvironmentState:
139
  price = (
@@ -166,22 +183,30 @@ class StockerEnv:
166
 
167
  # ---------------------------------------------------------------- helpers
168
  def _apply_action(self, action: TradeAction, price: float) -> bool:
169
- """Apply trade. Returns True if invalid (insufficient cash/position)."""
 
 
 
 
170
  if action.side == "hold" or action.quantity <= 0:
171
  return False
172
 
 
 
173
  if action.side == "buy":
174
- cost = action.quantity * price
175
- if cost > self._cash:
 
176
  return True
177
- self._cash -= cost
178
  self._position += action.quantity
179
  return False
180
 
181
  if action.side == "sell":
182
  if action.quantity > self._position:
183
  return True
184
- self._cash += action.quantity * price
 
185
  self._position -= action.quantity
186
  return False
187
 
 
3
 
4
  import uuid
5
 
6
+ from app.config import settings
7
  from app.core.graders import compute_step_reward, compute_trajectory_bonus
8
  from app.core.tasks import get_task_definition
9
  from app.data import loader
 
86
  )
87
 
88
  price = self._prices[self._current_index]
 
89
  invalid = self._apply_action(action, price)
90
  new_portfolio = self._cash + self._position * price
91
 
92
  result = compute_step_reward(
93
  action=action,
 
94
  new_portfolio=new_portfolio,
95
  starting_cash=float(self._task["starting_cash"]),
96
  invalid=invalid,
97
+ step_index=self._current_index,
98
+ total_steps=len(self._prices),
99
+ ideal_pnl_pct_series=self._task.get("ideal_pnl_pct_series", []),
100
+ ideal_pnl_pct_total=float(self._task.get("ideal_pnl_pct_total", 0.0)),
101
+ settings=settings,
102
  )
103
  reward = result.score
104
 
 
138
  },
139
  )
140
 
141
+ # ----------------------------------------------------------------- public
142
+ def is_ready(self) -> bool:
143
+ """True if the env has been reset and the episode is still live."""
144
+ return bool(self._prices) and not self._done
145
+
146
+ def current_observation(self) -> MarketObservation:
147
+ """Public accessor for the agent-visible observation at the current step."""
148
+ if not self._prices:
149
+ raise RuntimeError("env has not been reset yet")
150
+ if self._done:
151
+ return self._terminal_observation()
152
+ return self._build_observation(self._current_index)
153
+
154
  # ------------------------------------------------------------------ state
155
  def state(self) -> EnvironmentState:
156
  price = (
 
183
 
184
  # ---------------------------------------------------------------- helpers
185
  def _apply_action(self, action: TradeAction, price: float) -> bool:
186
+ """Apply trade. Returns True if invalid (insufficient cash/position).
187
+
188
+ Buys and sells incur a transaction cost equal to
189
+ settings.transaction_cost_rate * trade_notional, deducted from cash.
190
+ """
191
  if action.side == "hold" or action.quantity <= 0:
192
  return False
193
 
194
+ rate = settings.transaction_cost_rate
195
+
196
  if action.side == "buy":
197
+ notional = action.quantity * price
198
+ total_cost = notional * (1.0 + rate)
199
+ if total_cost > self._cash:
200
  return True
201
+ self._cash -= total_cost
202
  self._position += action.quantity
203
  return False
204
 
205
  if action.side == "sell":
206
  if action.quantity > self._position:
207
  return True
208
+ notional = action.quantity * price
209
+ self._cash += notional * (1.0 - rate)
210
  self._position -= action.quantity
211
  return False
212
 
app/core/graders.py CHANGED
@@ -1,39 +1,109 @@
1
  """Reward and shaping for the Stocker backtest.
2
 
3
- Per-step reward = pct change in portfolio value (with a small invalid-action
4
- penalty). End-of-episode reward adds:
5
- * alpha bonus: +0.10 if final portfolio beats buy-and-hold by >= 1%
6
- * drawdown penalty: linear in the worst drawdown observed during the episode
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  All rewards are clipped to [-1.0, 1.0] in the caller (StockerEnv.step).
9
  """
10
  from __future__ import annotations
11
 
 
12
  from app.models import RewardResult, TradeAction
13
 
 
 
 
14
 
15
  def compute_step_reward(
 
16
  action: TradeAction,
17
- prev_portfolio: float,
18
  new_portfolio: float,
19
  starting_cash: float,
20
  invalid: bool,
 
 
 
 
 
21
  ) -> RewardResult:
22
  breakdown: dict[str, float] = {}
23
 
24
- pnl_pct = (new_portfolio - prev_portfolio) / max(prev_portfolio, 1e-9)
25
- breakdown["pnl_pct"] = round(pnl_pct, 5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
- penalty = 0.0
28
  if invalid:
29
- penalty = 0.01
30
- breakdown["invalid_action_penalty"] = -penalty
31
 
32
- score = pnl_pct - penalty
33
- feedback = f"{action.side}({action.quantity}) -> portfolio {new_portfolio:.2f}"
 
 
34
  if invalid:
35
  feedback += " [invalid: insufficient cash/position]"
36
- return RewardResult(score=round(score, 5), breakdown=breakdown, feedback=feedback)
 
37
 
38
 
39
  def compute_trajectory_bonus(
@@ -45,13 +115,11 @@ def compute_trajectory_bonus(
45
  """End-of-episode shaping: alpha bonus minus drawdown penalty."""
46
  bonus = 0.0
47
 
48
- # Alpha vs. buy-and-hold (capped at +0.10)
49
  if buy_and_hold_value > 0:
50
  alpha = (final_portfolio - buy_and_hold_value) / buy_and_hold_value
51
  if alpha >= 0.01:
52
  bonus += min(0.10, alpha)
53
 
54
- # Drawdown penalty (only if the curve is provided)
55
  if portfolio_curve:
56
  peak = portfolio_curve[0]
57
  max_dd = 0.0
 
1
  """Reward and shaping for the Stocker backtest.
2
 
3
+ Per-step reward is a weighted sum of two components:
4
+
5
+ 1. Performance factor (W_PERF) asymmetric piecewise-linear function of the
6
+ gap between the precomputed ideal PnL trajectory and the model's actual
7
+ real (inflation-adjusted) PnL at this step. Small underperformance is
8
+ rewarded; large underperformance is punished; outperformance gets a
9
+ bonus.
10
+ 2. Inflation factor (W_INFLATION) — penalty for the share of nominal gain
11
+ eaten by inflation drag at this point in the episode.
12
+
13
+ A small invalid-action penalty is subtracted on top. Transaction cost is not
14
+ a separate term — it is deducted from cash at trade time inside the env, so
15
+ nominal_pnl_pct already reflects friction.
16
+
17
+ End-of-episode shaping (alpha vs. buy-and-hold + drawdown penalty) lives in
18
+ compute_trajectory_bonus and is unchanged.
19
 
20
  All rewards are clipped to [-1.0, 1.0] in the caller (StockerEnv.step).
21
  """
22
  from __future__ import annotations
23
 
24
+ from app.config import Settings
25
  from app.models import RewardResult, TradeAction
26
 
27
+ TRADING_DAYS_PER_YEAR = 252
28
+ INVALID_ACTION_PENALTY = 0.01
29
+
30
 
31
  def compute_step_reward(
32
+ *,
33
  action: TradeAction,
 
34
  new_portfolio: float,
35
  starting_cash: float,
36
  invalid: bool,
37
+ step_index: int,
38
+ total_steps: int,
39
+ ideal_pnl_pct_series: list[float],
40
+ ideal_pnl_pct_total: float,
41
+ settings: Settings,
42
  ) -> RewardResult:
43
  breakdown: dict[str, float] = {}
44
 
45
+ # Cumulative nominal PnL fraction (relative to starting cash)
46
+ nominal_pnl_pct = (new_portfolio - starting_cash) / max(starting_cash, 1e-9)
47
+
48
+ # Inflation-adjusted real PnL, scaled by elapsed years in the episode
49
+ years_elapsed = max(step_index, 0) / TRADING_DAYS_PER_YEAR
50
+ inflation_growth = (1.0 + settings.annual_inflation_rate) ** years_elapsed - 1.0
51
+ real_pnl_pct = (1.0 + nominal_pnl_pct) / (1.0 + inflation_growth) - 1.0
52
+
53
+ breakdown["nominal_pnl_pct"] = round(nominal_pnl_pct, 6)
54
+ breakdown["real_pnl_pct"] = round(real_pnl_pct, 6)
55
+ breakdown["inflation_drag"] = round(inflation_growth, 6)
56
+
57
+ # Inflation factor: how much of the nominal gain was eaten by inflation.
58
+ # Always <= 0 (real <= nominal under positive inflation), clipped at -1.
59
+ inflation_factor = max(-1.0, real_pnl_pct - nominal_pnl_pct)
60
+ breakdown["inflation_factor"] = round(inflation_factor, 6)
61
+
62
+ # Performance factor: asymmetric piecewise linear in the gap to ideal.
63
+ ideal_at_step = (
64
+ ideal_pnl_pct_series[step_index]
65
+ if 0 <= step_index < len(ideal_pnl_pct_series)
66
+ else ideal_pnl_pct_total
67
+ )
68
+ gap = ideal_at_step - real_pnl_pct
69
+ scale = max(0.05, 0.5 * abs(ideal_pnl_pct_total))
70
+
71
+ if gap < 0.0:
72
+ # Model exceeded ideal — bonus up to +2.0 (env clip caps at +1.0).
73
+ performance_factor = 1.0 + min(1.0, abs(gap) / scale)
74
+ elif gap <= scale:
75
+ # Close to ideal — high reward decaying linearly toward 0.
76
+ performance_factor = 1.0 - gap / scale
77
+ else:
78
+ # Far behind ideal — punishment down to -1.0.
79
+ performance_factor = -min(1.0, (gap - scale) / scale)
80
+
81
+ breakdown["ideal_pnl_pct_at_step"] = round(ideal_at_step, 6)
82
+ breakdown["gap"] = round(gap, 6)
83
+ breakdown["performance_factor"] = round(performance_factor, 6)
84
+
85
+ # Weighted combination
86
+ w_perf = settings.reward_weight_performance
87
+ w_inf = settings.reward_weight_inflation
88
+ weighted_perf = w_perf * performance_factor
89
+ weighted_inf = w_inf * inflation_factor
90
+ breakdown["weighted_performance"] = round(weighted_perf, 6)
91
+ breakdown["weighted_inflation"] = round(weighted_inf, 6)
92
+
93
+ score = weighted_perf + weighted_inf
94
 
 
95
  if invalid:
96
+ score -= INVALID_ACTION_PENALTY
97
+ breakdown["invalid_action_penalty"] = -INVALID_ACTION_PENALTY
98
 
99
+ feedback = (
100
+ f"{action.side}({action.quantity}) -> portfolio {new_portfolio:.2f} "
101
+ f"(real {real_pnl_pct:+.3%}, ideal {ideal_at_step:+.3%}, gap {gap:+.3%})"
102
+ )
103
  if invalid:
104
  feedback += " [invalid: insufficient cash/position]"
105
+
106
+ return RewardResult(score=round(score, 6), breakdown=breakdown, feedback=feedback)
107
 
108
 
109
  def compute_trajectory_bonus(
 
115
  """End-of-episode shaping: alpha bonus minus drawdown penalty."""
116
  bonus = 0.0
117
 
 
118
  if buy_and_hold_value > 0:
119
  alpha = (final_portfolio - buy_and_hold_value) / buy_and_hold_value
120
  if alpha >= 0.01:
121
  bonus += min(0.10, alpha)
122
 
 
123
  if portfolio_curve:
124
  peak = portfolio_curve[0]
125
  max_dd = 0.0
app/core/tasks.py CHANGED
@@ -10,13 +10,17 @@ The 3 stable IDs are kept so the OpenEnv contract doesn't change.
10
  """
11
  from __future__ import annotations
12
 
 
13
  import logging
 
14
 
15
  from app.core import corpus_tasks
16
  from app.data import loader
17
 
18
  logger = logging.getLogger(__name__)
19
 
 
 
20
 
21
  TASK_META = {
22
  "task_easy": {
@@ -54,6 +58,40 @@ def list_task_ids() -> list[str]:
54
  return list(TASK_META.keys()) + corpus_tasks.list_corpus_task_ids()
55
 
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  def get_task_definition(task_id: str) -> dict:
58
  if corpus_tasks.is_corpus_task(task_id):
59
  return corpus_tasks.get_corpus_task_definition(task_id)
@@ -67,6 +105,8 @@ def get_task_definition(task_id: str) -> dict:
67
  raise RuntimeError(
68
  f"No episode data for {task_id}. Run scripts/build_dataset.py."
69
  )
 
 
70
  return {
71
  "task_id": task_id,
72
  "description": meta["description"],
@@ -74,5 +114,7 @@ def get_task_definition(task_id: str) -> dict:
74
  "starting_cash": meta["starting_cash"],
75
  "fundamentals": meta["fundamentals"],
76
  "dates": rows["date"].tolist(),
77
- "prices": rows["close"].astype(float).tolist(),
 
 
78
  }
 
10
  """
11
  from __future__ import annotations
12
 
13
+ import json
14
  import logging
15
+ from pathlib import Path
16
 
17
  from app.core import corpus_tasks
18
  from app.data import loader
19
 
20
  logger = logging.getLogger(__name__)
21
 
22
+ IDEAL_PROFITS_DIR = Path(__file__).resolve().parent.parent.parent / "data" / "ideal_profits"
23
+
24
 
25
  TASK_META = {
26
  "task_easy": {
 
58
  return list(TASK_META.keys()) + corpus_tasks.list_corpus_task_ids()
59
 
60
 
61
+ def _load_ideal_profits(task_id: str, n_steps: int) -> tuple[list[float], float]:
62
+ """Load per-step ideal PnL series for a task. Returns (series, total).
63
+
64
+ Falls back to a flat zero series if the sidecar is missing — the grader
65
+ still runs but the performance component collapses to zero gap signal.
66
+ """
67
+ path = IDEAL_PROFITS_DIR / f"{task_id}.json"
68
+ if not path.exists():
69
+ logger.warning(
70
+ "Ideal-profit sidecar missing for %s at %s. "
71
+ "Run `python scripts/build_ideal_profit.py`. "
72
+ "Performance component will be flat for this episode.",
73
+ task_id,
74
+ path,
75
+ )
76
+ return [0.0] * n_steps, 0.0
77
+
78
+ payload = json.loads(path.read_text())
79
+ series = [float(v) for v in payload.get("ideal_pnl_pct_series", [])]
80
+ total = float(payload.get("ideal_pnl_pct_total", series[-1] if series else 0.0))
81
+
82
+ if len(series) != n_steps:
83
+ logger.warning(
84
+ "Ideal-profit length mismatch for %s: sidecar=%d, episode=%d. "
85
+ "Re-run scripts/build_ideal_profit.py.",
86
+ task_id, len(series), n_steps,
87
+ )
88
+ if len(series) < n_steps:
89
+ series = series + [series[-1] if series else 0.0] * (n_steps - len(series))
90
+ else:
91
+ series = series[:n_steps]
92
+ return series, total
93
+
94
+
95
  def get_task_definition(task_id: str) -> dict:
96
  if corpus_tasks.is_corpus_task(task_id):
97
  return corpus_tasks.get_corpus_task_definition(task_id)
 
105
  raise RuntimeError(
106
  f"No episode data for {task_id}. Run scripts/build_dataset.py."
107
  )
108
+ prices = rows["close"].astype(float).tolist()
109
+ ideal_series, ideal_total = _load_ideal_profits(task_id, len(prices))
110
  return {
111
  "task_id": task_id,
112
  "description": meta["description"],
 
114
  "starting_cash": meta["starting_cash"],
115
  "fundamentals": meta["fundamentals"],
116
  "dates": rows["date"].tolist(),
117
+ "prices": prices,
118
+ "ideal_pnl_pct_series": ideal_series,
119
+ "ideal_pnl_pct_total": ideal_total,
120
  }
app/main.py CHANGED
@@ -6,15 +6,20 @@ import logging
6
  import sys
7
  import time
8
  import traceback
 
9
 
10
  from fastapi import FastAPI, Request
11
  from fastapi.middleware.cors import CORSMiddleware
12
  from fastapi.responses import JSONResponse
 
13
 
14
  from app.api.router import api_router
15
  from app.api.frontend import router as frontend_router
16
  from app.config import settings
17
 
 
 
 
18
  logging.basicConfig(
19
  level=logging.INFO,
20
  format="%(levelname)s:\t%(name)s - %(message)s",
@@ -59,6 +64,18 @@ def create_app() -> FastAPI:
59
 
60
  app.include_router(api_router)
61
  app.include_router(frontend_router)
 
 
 
 
 
 
 
 
 
 
 
 
62
  return app
63
 
64
 
 
6
  import sys
7
  import time
8
  import traceback
9
+ from pathlib import Path
10
 
11
  from fastapi import FastAPI, Request
12
  from fastapi.middleware.cors import CORSMiddleware
13
  from fastapi.responses import JSONResponse
14
+ from fastapi.staticfiles import StaticFiles
15
 
16
  from app.api.router import api_router
17
  from app.api.frontend import router as frontend_router
18
  from app.config import settings
19
 
20
+ FRONTEND_DIST = Path(__file__).resolve().parents[1] / "frontend" / "dist"
21
+ TRAINING_RUNS = Path(__file__).resolve().parents[1] / "training" / "runs"
22
+
23
  logging.basicConfig(
24
  level=logging.INFO,
25
  format="%(levelname)s:\t%(name)s - %(message)s",
 
64
 
65
  app.include_router(api_router)
66
  app.include_router(frontend_router)
67
+
68
+ assets_dir = FRONTEND_DIST / "assets"
69
+ if assets_dir.is_dir():
70
+ app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="assets")
71
+
72
+ if TRAINING_RUNS.is_dir():
73
+ app.mount(
74
+ "/training/runs",
75
+ StaticFiles(directory=str(TRAINING_RUNS)),
76
+ name="training-runs",
77
+ )
78
+
79
  return app
80
 
81
 
data/ideal_profits/task_easy.json ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "task_easy",
3
+ "ticker": "AAPL",
4
+ "starting_cash": 10000.0,
5
+ "transaction_cost_rate": 0.001,
6
+ "ideal_pnl_pct_series": [
7
+ 0.0,
8
+ 0.0,
9
+ 0.0,
10
+ 0.0,
11
+ -0.000984,
12
+ 0.003252,
13
+ 0.003252,
14
+ 0.003252,
15
+ 0.002257,
16
+ 0.010604,
17
+ 0.010604,
18
+ 0.010604,
19
+ 0.009595,
20
+ 0.012437,
21
+ 0.020267,
22
+ 0.028329,
23
+ 0.04984,
24
+ 0.0488,
25
+ 0.061956,
26
+ 0.071279,
27
+ 0.094465,
28
+ 0.115292,
29
+ 0.11659,
30
+ 0.125972,
31
+ 0.126268,
32
+ 0.126268,
33
+ 0.12515,
34
+ 0.129056,
35
+ 0.13536,
36
+ 0.13536,
37
+ 0.134227,
38
+ 0.14303,
39
+ 0.141892,
40
+ 0.161132,
41
+ 0.167119,
42
+ 0.167119,
43
+ 0.165953,
44
+ 0.171715,
45
+ 0.179179,
46
+ 0.179179,
47
+ 0.178003,
48
+ 0.179797,
49
+ 0.182203
50
+ ],
51
+ "ideal_pnl_pct_total": 0.182203,
52
+ "ideal_portfolio_curve": [
53
+ 10000.0,
54
+ 10000.0,
55
+ 10000.0,
56
+ 10000.0,
57
+ 9990.1632,
58
+ 10032.5241,
59
+ 10032.5241,
60
+ 10032.5241,
61
+ 10022.5678,
62
+ 10106.0388,
63
+ 10106.0388,
64
+ 10106.0388,
65
+ 10095.9468,
66
+ 10124.3672,
67
+ 10202.6666,
68
+ 10283.2866,
69
+ 10498.4016,
70
+ 10487.9952,
71
+ 10619.5649,
72
+ 10712.785,
73
+ 10944.6546,
74
+ 11152.9245,
75
+ 11165.9046,
76
+ 11259.7153,
77
+ 11262.6824,
78
+ 11262.6824,
79
+ 11251.4961,
80
+ 11290.5558,
81
+ 11353.5966,
82
+ 11353.5966,
83
+ 11342.273,
84
+ 11430.2998,
85
+ 11418.9242,
86
+ 11611.3246,
87
+ 11671.1854,
88
+ 11671.1854,
89
+ 11659.5321,
90
+ 11717.1522,
91
+ 11791.7854,
92
+ 11791.7854,
93
+ 11780.0257,
94
+ 11797.9664,
95
+ 11822.0332
96
+ ]
97
+ }
data/ideal_profits/task_hard.json ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "task_hard",
3
+ "ticker": "META",
4
+ "starting_cash": 10000.0,
5
+ "transaction_cost_rate": 0.001,
6
+ "ideal_pnl_pct_series": [
7
+ 0.0,
8
+ 0.0,
9
+ -0.000999,
10
+ 0.010656,
11
+ 0.021177,
12
+ 0.064779,
13
+ 0.064779,
14
+ 0.064779,
15
+ 0.064779,
16
+ 0.064779,
17
+ 0.063725,
18
+ 0.075116,
19
+ 0.075116,
20
+ 0.07405,
21
+ 0.078229,
22
+ 0.078229,
23
+ 0.078229,
24
+ 0.077153,
25
+ 0.133701,
26
+ 0.133701,
27
+ 0.132574,
28
+ 0.156894,
29
+ 0.16959,
30
+ 0.168423,
31
+ 0.168011,
32
+ 0.16685,
33
+ 0.168644,
34
+ 0.168644,
35
+ 0.167483,
36
+ 0.191687,
37
+ 0.190508,
38
+ 0.256965,
39
+ 0.255717,
40
+ 0.258506,
41
+ 0.258506,
42
+ 0.258506,
43
+ 0.257261,
44
+ 0.330725,
45
+ 0.330725,
46
+ 0.329403,
47
+ 0.345073,
48
+ 0.345073
49
+ ],
50
+ "ideal_pnl_pct_total": 0.345073,
51
+ "ideal_portfolio_curve": [
52
+ 10000.0,
53
+ 10000.0,
54
+ 9990.012,
55
+ 10106.5624,
56
+ 10211.7722,
57
+ 10647.7856,
58
+ 10647.7856,
59
+ 10647.7856,
60
+ 10647.7856,
61
+ 10647.7856,
62
+ 10637.2527,
63
+ 10751.156,
64
+ 10751.156,
65
+ 10740.497,
66
+ 10782.2865,
67
+ 10782.2865,
68
+ 10782.2865,
69
+ 10771.5345,
70
+ 11337.0062,
71
+ 11337.0062,
72
+ 11325.7448,
73
+ 11568.9354,
74
+ 11695.902,
75
+ 11684.2277,
76
+ 11680.1068,
77
+ 11668.4966,
78
+ 11686.4366,
79
+ 11686.4366,
80
+ 11674.8341,
81
+ 11916.8671,
82
+ 11905.0784,
83
+ 12569.6519,
84
+ 12557.1687,
85
+ 12585.0644,
86
+ 12585.0644,
87
+ 12585.0644,
88
+ 12572.6112,
89
+ 13307.2496,
90
+ 13307.2496,
91
+ 13294.0277,
92
+ 13450.735,
93
+ 13450.735
94
+ ]
95
+ }
data/ideal_profits/task_medium.json ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "task_medium",
3
+ "ticker": "INTC",
4
+ "starting_cash": 10000.0,
5
+ "transaction_cost_rate": 0.001,
6
+ "ideal_pnl_pct_series": [
7
+ 0.0,
8
+ 0.0,
9
+ -0.000998,
10
+ -0.000572,
11
+ 0.031624,
12
+ 0.031624,
13
+ 0.030594,
14
+ 0.033249,
15
+ 0.033249,
16
+ 0.033249,
17
+ 0.032217,
18
+ 0.047449,
19
+ 0.079033,
20
+ 0.080601,
21
+ 0.095609,
22
+ 0.100089,
23
+ 0.109283,
24
+ 0.108179,
25
+ 0.111877,
26
+ 0.110769,
27
+ 0.114897,
28
+ 0.121003,
29
+ 0.119886,
30
+ 0.12322,
31
+ 0.1221,
32
+ 0.122027,
33
+ 0.120909,
34
+ 0.142213,
35
+ 0.15999,
36
+ 0.158834,
37
+ 0.185253,
38
+ 0.185253,
39
+ 0.18407,
40
+ 0.210331,
41
+ 0.210331,
42
+ 0.209123,
43
+ 0.208196,
44
+ 0.208196,
45
+ 0.208196,
46
+ 0.206991,
47
+ 0.236178
48
+ ],
49
+ "ideal_pnl_pct_total": 0.236178,
50
+ "ideal_portfolio_curve": [
51
+ 10000.0,
52
+ 10000.0,
53
+ 9990.0167,
54
+ 9994.2768,
55
+ 10316.2372,
56
+ 10316.2372,
57
+ 10305.9362,
58
+ 10332.488,
59
+ 10332.488,
60
+ 10332.488,
61
+ 10322.1705,
62
+ 10474.4906,
63
+ 10790.3306,
64
+ 10806.0105,
65
+ 10956.0901,
66
+ 11000.8902,
67
+ 11092.8308,
68
+ 11081.7874,
69
+ 11118.7655,
70
+ 11107.6922,
71
+ 11148.9731,
72
+ 11210.0259,
73
+ 11198.8647,
74
+ 11232.1995,
75
+ 11221.0016,
76
+ 11220.2725,
77
+ 11209.095,
78
+ 11422.1254,
79
+ 11599.9048,
80
+ 11588.3379,
81
+ 11852.5347,
82
+ 11852.5347,
83
+ 11840.6999,
84
+ 12103.3111,
85
+ 12103.3111,
86
+ 12091.2337,
87
+ 12081.9641,
88
+ 12081.9641,
89
+ 12081.9641,
90
+ 12069.913,
91
+ 12361.7769
92
+ ]
93
+ }
frontend/.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ node_modules/
2
+ build/
3
+ dist/
4
+ coverage/
5
+ .DS_Store
6
+ *.log
7
+ .env*
8
+ !.env.example
frontend/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Stocker AI | Autonomous Trading Terminal</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
13
+
frontend/package-lock.json ADDED
@@ -0,0 +1,2605 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "stocker-ai-frontend",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "stocker-ai-frontend",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "lightweight-charts": "^4.2.3",
12
+ "lucide-react": "^0.546.0",
13
+ "motion": "^12.23.24",
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@tailwindcss/vite": "^4.1.14",
19
+ "@types/node": "^22.14.0",
20
+ "@types/react": "^19.2.14",
21
+ "@types/react-dom": "^19.2.3",
22
+ "@vitejs/plugin-react": "^5.0.4",
23
+ "autoprefixer": "^10.4.21",
24
+ "tailwindcss": "^4.1.14",
25
+ "typescript": "~5.8.2",
26
+ "vite": "^6.2.0"
27
+ }
28
+ },
29
+ "node_modules/@babel/code-frame": {
30
+ "version": "7.29.0",
31
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
32
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
33
+ "dev": true,
34
+ "license": "MIT",
35
+ "dependencies": {
36
+ "@babel/helper-validator-identifier": "^7.28.5",
37
+ "js-tokens": "^4.0.0",
38
+ "picocolors": "^1.1.1"
39
+ },
40
+ "engines": {
41
+ "node": ">=6.9.0"
42
+ }
43
+ },
44
+ "node_modules/@babel/compat-data": {
45
+ "version": "7.29.0",
46
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
47
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
48
+ "dev": true,
49
+ "license": "MIT",
50
+ "engines": {
51
+ "node": ">=6.9.0"
52
+ }
53
+ },
54
+ "node_modules/@babel/core": {
55
+ "version": "7.29.0",
56
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
57
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
58
+ "dev": true,
59
+ "license": "MIT",
60
+ "dependencies": {
61
+ "@babel/code-frame": "^7.29.0",
62
+ "@babel/generator": "^7.29.0",
63
+ "@babel/helper-compilation-targets": "^7.28.6",
64
+ "@babel/helper-module-transforms": "^7.28.6",
65
+ "@babel/helpers": "^7.28.6",
66
+ "@babel/parser": "^7.29.0",
67
+ "@babel/template": "^7.28.6",
68
+ "@babel/traverse": "^7.29.0",
69
+ "@babel/types": "^7.29.0",
70
+ "@jridgewell/remapping": "^2.3.5",
71
+ "convert-source-map": "^2.0.0",
72
+ "debug": "^4.1.0",
73
+ "gensync": "^1.0.0-beta.2",
74
+ "json5": "^2.2.3",
75
+ "semver": "^6.3.1"
76
+ },
77
+ "engines": {
78
+ "node": ">=6.9.0"
79
+ },
80
+ "funding": {
81
+ "type": "opencollective",
82
+ "url": "https://opencollective.com/babel"
83
+ }
84
+ },
85
+ "node_modules/@babel/generator": {
86
+ "version": "7.29.1",
87
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
88
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
89
+ "dev": true,
90
+ "license": "MIT",
91
+ "dependencies": {
92
+ "@babel/parser": "^7.29.0",
93
+ "@babel/types": "^7.29.0",
94
+ "@jridgewell/gen-mapping": "^0.3.12",
95
+ "@jridgewell/trace-mapping": "^0.3.28",
96
+ "jsesc": "^3.0.2"
97
+ },
98
+ "engines": {
99
+ "node": ">=6.9.0"
100
+ }
101
+ },
102
+ "node_modules/@babel/helper-compilation-targets": {
103
+ "version": "7.28.6",
104
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
105
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
106
+ "dev": true,
107
+ "license": "MIT",
108
+ "dependencies": {
109
+ "@babel/compat-data": "^7.28.6",
110
+ "@babel/helper-validator-option": "^7.27.1",
111
+ "browserslist": "^4.24.0",
112
+ "lru-cache": "^5.1.1",
113
+ "semver": "^6.3.1"
114
+ },
115
+ "engines": {
116
+ "node": ">=6.9.0"
117
+ }
118
+ },
119
+ "node_modules/@babel/helper-globals": {
120
+ "version": "7.28.0",
121
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
122
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
123
+ "dev": true,
124
+ "license": "MIT",
125
+ "engines": {
126
+ "node": ">=6.9.0"
127
+ }
128
+ },
129
+ "node_modules/@babel/helper-module-imports": {
130
+ "version": "7.28.6",
131
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
132
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
133
+ "dev": true,
134
+ "license": "MIT",
135
+ "dependencies": {
136
+ "@babel/traverse": "^7.28.6",
137
+ "@babel/types": "^7.28.6"
138
+ },
139
+ "engines": {
140
+ "node": ">=6.9.0"
141
+ }
142
+ },
143
+ "node_modules/@babel/helper-module-transforms": {
144
+ "version": "7.28.6",
145
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
146
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
147
+ "dev": true,
148
+ "license": "MIT",
149
+ "dependencies": {
150
+ "@babel/helper-module-imports": "^7.28.6",
151
+ "@babel/helper-validator-identifier": "^7.28.5",
152
+ "@babel/traverse": "^7.28.6"
153
+ },
154
+ "engines": {
155
+ "node": ">=6.9.0"
156
+ },
157
+ "peerDependencies": {
158
+ "@babel/core": "^7.0.0"
159
+ }
160
+ },
161
+ "node_modules/@babel/helper-plugin-utils": {
162
+ "version": "7.28.6",
163
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
164
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
165
+ "dev": true,
166
+ "license": "MIT",
167
+ "engines": {
168
+ "node": ">=6.9.0"
169
+ }
170
+ },
171
+ "node_modules/@babel/helper-string-parser": {
172
+ "version": "7.27.1",
173
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
174
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
175
+ "dev": true,
176
+ "license": "MIT",
177
+ "engines": {
178
+ "node": ">=6.9.0"
179
+ }
180
+ },
181
+ "node_modules/@babel/helper-validator-identifier": {
182
+ "version": "7.28.5",
183
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
184
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
185
+ "dev": true,
186
+ "license": "MIT",
187
+ "engines": {
188
+ "node": ">=6.9.0"
189
+ }
190
+ },
191
+ "node_modules/@babel/helper-validator-option": {
192
+ "version": "7.27.1",
193
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
194
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
195
+ "dev": true,
196
+ "license": "MIT",
197
+ "engines": {
198
+ "node": ">=6.9.0"
199
+ }
200
+ },
201
+ "node_modules/@babel/helpers": {
202
+ "version": "7.29.2",
203
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
204
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
205
+ "dev": true,
206
+ "license": "MIT",
207
+ "dependencies": {
208
+ "@babel/template": "^7.28.6",
209
+ "@babel/types": "^7.29.0"
210
+ },
211
+ "engines": {
212
+ "node": ">=6.9.0"
213
+ }
214
+ },
215
+ "node_modules/@babel/parser": {
216
+ "version": "7.29.2",
217
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
218
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
219
+ "dev": true,
220
+ "license": "MIT",
221
+ "dependencies": {
222
+ "@babel/types": "^7.29.0"
223
+ },
224
+ "bin": {
225
+ "parser": "bin/babel-parser.js"
226
+ },
227
+ "engines": {
228
+ "node": ">=6.0.0"
229
+ }
230
+ },
231
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
232
+ "version": "7.27.1",
233
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
234
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
235
+ "dev": true,
236
+ "license": "MIT",
237
+ "dependencies": {
238
+ "@babel/helper-plugin-utils": "^7.27.1"
239
+ },
240
+ "engines": {
241
+ "node": ">=6.9.0"
242
+ },
243
+ "peerDependencies": {
244
+ "@babel/core": "^7.0.0-0"
245
+ }
246
+ },
247
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
248
+ "version": "7.27.1",
249
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
250
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
251
+ "dev": true,
252
+ "license": "MIT",
253
+ "dependencies": {
254
+ "@babel/helper-plugin-utils": "^7.27.1"
255
+ },
256
+ "engines": {
257
+ "node": ">=6.9.0"
258
+ },
259
+ "peerDependencies": {
260
+ "@babel/core": "^7.0.0-0"
261
+ }
262
+ },
263
+ "node_modules/@babel/template": {
264
+ "version": "7.28.6",
265
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
266
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
267
+ "dev": true,
268
+ "license": "MIT",
269
+ "dependencies": {
270
+ "@babel/code-frame": "^7.28.6",
271
+ "@babel/parser": "^7.28.6",
272
+ "@babel/types": "^7.28.6"
273
+ },
274
+ "engines": {
275
+ "node": ">=6.9.0"
276
+ }
277
+ },
278
+ "node_modules/@babel/traverse": {
279
+ "version": "7.29.0",
280
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
281
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
282
+ "dev": true,
283
+ "license": "MIT",
284
+ "dependencies": {
285
+ "@babel/code-frame": "^7.29.0",
286
+ "@babel/generator": "^7.29.0",
287
+ "@babel/helper-globals": "^7.28.0",
288
+ "@babel/parser": "^7.29.0",
289
+ "@babel/template": "^7.28.6",
290
+ "@babel/types": "^7.29.0",
291
+ "debug": "^4.3.1"
292
+ },
293
+ "engines": {
294
+ "node": ">=6.9.0"
295
+ }
296
+ },
297
+ "node_modules/@babel/types": {
298
+ "version": "7.29.0",
299
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
300
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
301
+ "dev": true,
302
+ "license": "MIT",
303
+ "dependencies": {
304
+ "@babel/helper-string-parser": "^7.27.1",
305
+ "@babel/helper-validator-identifier": "^7.28.5"
306
+ },
307
+ "engines": {
308
+ "node": ">=6.9.0"
309
+ }
310
+ },
311
+ "node_modules/@esbuild/aix-ppc64": {
312
+ "version": "0.25.12",
313
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
314
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
315
+ "cpu": [
316
+ "ppc64"
317
+ ],
318
+ "dev": true,
319
+ "license": "MIT",
320
+ "optional": true,
321
+ "os": [
322
+ "aix"
323
+ ],
324
+ "engines": {
325
+ "node": ">=18"
326
+ }
327
+ },
328
+ "node_modules/@esbuild/android-arm": {
329
+ "version": "0.25.12",
330
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
331
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
332
+ "cpu": [
333
+ "arm"
334
+ ],
335
+ "dev": true,
336
+ "license": "MIT",
337
+ "optional": true,
338
+ "os": [
339
+ "android"
340
+ ],
341
+ "engines": {
342
+ "node": ">=18"
343
+ }
344
+ },
345
+ "node_modules/@esbuild/android-arm64": {
346
+ "version": "0.25.12",
347
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
348
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
349
+ "cpu": [
350
+ "arm64"
351
+ ],
352
+ "dev": true,
353
+ "license": "MIT",
354
+ "optional": true,
355
+ "os": [
356
+ "android"
357
+ ],
358
+ "engines": {
359
+ "node": ">=18"
360
+ }
361
+ },
362
+ "node_modules/@esbuild/android-x64": {
363
+ "version": "0.25.12",
364
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
365
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
366
+ "cpu": [
367
+ "x64"
368
+ ],
369
+ "dev": true,
370
+ "license": "MIT",
371
+ "optional": true,
372
+ "os": [
373
+ "android"
374
+ ],
375
+ "engines": {
376
+ "node": ">=18"
377
+ }
378
+ },
379
+ "node_modules/@esbuild/darwin-arm64": {
380
+ "version": "0.25.12",
381
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
382
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
383
+ "cpu": [
384
+ "arm64"
385
+ ],
386
+ "dev": true,
387
+ "license": "MIT",
388
+ "optional": true,
389
+ "os": [
390
+ "darwin"
391
+ ],
392
+ "engines": {
393
+ "node": ">=18"
394
+ }
395
+ },
396
+ "node_modules/@esbuild/darwin-x64": {
397
+ "version": "0.25.12",
398
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
399
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
400
+ "cpu": [
401
+ "x64"
402
+ ],
403
+ "dev": true,
404
+ "license": "MIT",
405
+ "optional": true,
406
+ "os": [
407
+ "darwin"
408
+ ],
409
+ "engines": {
410
+ "node": ">=18"
411
+ }
412
+ },
413
+ "node_modules/@esbuild/freebsd-arm64": {
414
+ "version": "0.25.12",
415
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
416
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
417
+ "cpu": [
418
+ "arm64"
419
+ ],
420
+ "dev": true,
421
+ "license": "MIT",
422
+ "optional": true,
423
+ "os": [
424
+ "freebsd"
425
+ ],
426
+ "engines": {
427
+ "node": ">=18"
428
+ }
429
+ },
430
+ "node_modules/@esbuild/freebsd-x64": {
431
+ "version": "0.25.12",
432
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
433
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
434
+ "cpu": [
435
+ "x64"
436
+ ],
437
+ "dev": true,
438
+ "license": "MIT",
439
+ "optional": true,
440
+ "os": [
441
+ "freebsd"
442
+ ],
443
+ "engines": {
444
+ "node": ">=18"
445
+ }
446
+ },
447
+ "node_modules/@esbuild/linux-arm": {
448
+ "version": "0.25.12",
449
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
450
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
451
+ "cpu": [
452
+ "arm"
453
+ ],
454
+ "dev": true,
455
+ "license": "MIT",
456
+ "optional": true,
457
+ "os": [
458
+ "linux"
459
+ ],
460
+ "engines": {
461
+ "node": ">=18"
462
+ }
463
+ },
464
+ "node_modules/@esbuild/linux-arm64": {
465
+ "version": "0.25.12",
466
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
467
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
468
+ "cpu": [
469
+ "arm64"
470
+ ],
471
+ "dev": true,
472
+ "license": "MIT",
473
+ "optional": true,
474
+ "os": [
475
+ "linux"
476
+ ],
477
+ "engines": {
478
+ "node": ">=18"
479
+ }
480
+ },
481
+ "node_modules/@esbuild/linux-ia32": {
482
+ "version": "0.25.12",
483
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
484
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
485
+ "cpu": [
486
+ "ia32"
487
+ ],
488
+ "dev": true,
489
+ "license": "MIT",
490
+ "optional": true,
491
+ "os": [
492
+ "linux"
493
+ ],
494
+ "engines": {
495
+ "node": ">=18"
496
+ }
497
+ },
498
+ "node_modules/@esbuild/linux-loong64": {
499
+ "version": "0.25.12",
500
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
501
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
502
+ "cpu": [
503
+ "loong64"
504
+ ],
505
+ "dev": true,
506
+ "license": "MIT",
507
+ "optional": true,
508
+ "os": [
509
+ "linux"
510
+ ],
511
+ "engines": {
512
+ "node": ">=18"
513
+ }
514
+ },
515
+ "node_modules/@esbuild/linux-mips64el": {
516
+ "version": "0.25.12",
517
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
518
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
519
+ "cpu": [
520
+ "mips64el"
521
+ ],
522
+ "dev": true,
523
+ "license": "MIT",
524
+ "optional": true,
525
+ "os": [
526
+ "linux"
527
+ ],
528
+ "engines": {
529
+ "node": ">=18"
530
+ }
531
+ },
532
+ "node_modules/@esbuild/linux-ppc64": {
533
+ "version": "0.25.12",
534
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
535
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
536
+ "cpu": [
537
+ "ppc64"
538
+ ],
539
+ "dev": true,
540
+ "license": "MIT",
541
+ "optional": true,
542
+ "os": [
543
+ "linux"
544
+ ],
545
+ "engines": {
546
+ "node": ">=18"
547
+ }
548
+ },
549
+ "node_modules/@esbuild/linux-riscv64": {
550
+ "version": "0.25.12",
551
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
552
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
553
+ "cpu": [
554
+ "riscv64"
555
+ ],
556
+ "dev": true,
557
+ "license": "MIT",
558
+ "optional": true,
559
+ "os": [
560
+ "linux"
561
+ ],
562
+ "engines": {
563
+ "node": ">=18"
564
+ }
565
+ },
566
+ "node_modules/@esbuild/linux-s390x": {
567
+ "version": "0.25.12",
568
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
569
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
570
+ "cpu": [
571
+ "s390x"
572
+ ],
573
+ "dev": true,
574
+ "license": "MIT",
575
+ "optional": true,
576
+ "os": [
577
+ "linux"
578
+ ],
579
+ "engines": {
580
+ "node": ">=18"
581
+ }
582
+ },
583
+ "node_modules/@esbuild/linux-x64": {
584
+ "version": "0.25.12",
585
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
586
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
587
+ "cpu": [
588
+ "x64"
589
+ ],
590
+ "dev": true,
591
+ "license": "MIT",
592
+ "optional": true,
593
+ "os": [
594
+ "linux"
595
+ ],
596
+ "engines": {
597
+ "node": ">=18"
598
+ }
599
+ },
600
+ "node_modules/@esbuild/netbsd-arm64": {
601
+ "version": "0.25.12",
602
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
603
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
604
+ "cpu": [
605
+ "arm64"
606
+ ],
607
+ "dev": true,
608
+ "license": "MIT",
609
+ "optional": true,
610
+ "os": [
611
+ "netbsd"
612
+ ],
613
+ "engines": {
614
+ "node": ">=18"
615
+ }
616
+ },
617
+ "node_modules/@esbuild/netbsd-x64": {
618
+ "version": "0.25.12",
619
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
620
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
621
+ "cpu": [
622
+ "x64"
623
+ ],
624
+ "dev": true,
625
+ "license": "MIT",
626
+ "optional": true,
627
+ "os": [
628
+ "netbsd"
629
+ ],
630
+ "engines": {
631
+ "node": ">=18"
632
+ }
633
+ },
634
+ "node_modules/@esbuild/openbsd-arm64": {
635
+ "version": "0.25.12",
636
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
637
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
638
+ "cpu": [
639
+ "arm64"
640
+ ],
641
+ "dev": true,
642
+ "license": "MIT",
643
+ "optional": true,
644
+ "os": [
645
+ "openbsd"
646
+ ],
647
+ "engines": {
648
+ "node": ">=18"
649
+ }
650
+ },
651
+ "node_modules/@esbuild/openbsd-x64": {
652
+ "version": "0.25.12",
653
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
654
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
655
+ "cpu": [
656
+ "x64"
657
+ ],
658
+ "dev": true,
659
+ "license": "MIT",
660
+ "optional": true,
661
+ "os": [
662
+ "openbsd"
663
+ ],
664
+ "engines": {
665
+ "node": ">=18"
666
+ }
667
+ },
668
+ "node_modules/@esbuild/openharmony-arm64": {
669
+ "version": "0.25.12",
670
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
671
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
672
+ "cpu": [
673
+ "arm64"
674
+ ],
675
+ "dev": true,
676
+ "license": "MIT",
677
+ "optional": true,
678
+ "os": [
679
+ "openharmony"
680
+ ],
681
+ "engines": {
682
+ "node": ">=18"
683
+ }
684
+ },
685
+ "node_modules/@esbuild/sunos-x64": {
686
+ "version": "0.25.12",
687
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
688
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
689
+ "cpu": [
690
+ "x64"
691
+ ],
692
+ "dev": true,
693
+ "license": "MIT",
694
+ "optional": true,
695
+ "os": [
696
+ "sunos"
697
+ ],
698
+ "engines": {
699
+ "node": ">=18"
700
+ }
701
+ },
702
+ "node_modules/@esbuild/win32-arm64": {
703
+ "version": "0.25.12",
704
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
705
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
706
+ "cpu": [
707
+ "arm64"
708
+ ],
709
+ "dev": true,
710
+ "license": "MIT",
711
+ "optional": true,
712
+ "os": [
713
+ "win32"
714
+ ],
715
+ "engines": {
716
+ "node": ">=18"
717
+ }
718
+ },
719
+ "node_modules/@esbuild/win32-ia32": {
720
+ "version": "0.25.12",
721
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
722
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
723
+ "cpu": [
724
+ "ia32"
725
+ ],
726
+ "dev": true,
727
+ "license": "MIT",
728
+ "optional": true,
729
+ "os": [
730
+ "win32"
731
+ ],
732
+ "engines": {
733
+ "node": ">=18"
734
+ }
735
+ },
736
+ "node_modules/@esbuild/win32-x64": {
737
+ "version": "0.25.12",
738
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
739
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
740
+ "cpu": [
741
+ "x64"
742
+ ],
743
+ "dev": true,
744
+ "license": "MIT",
745
+ "optional": true,
746
+ "os": [
747
+ "win32"
748
+ ],
749
+ "engines": {
750
+ "node": ">=18"
751
+ }
752
+ },
753
+ "node_modules/@jridgewell/gen-mapping": {
754
+ "version": "0.3.13",
755
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
756
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
757
+ "dev": true,
758
+ "license": "MIT",
759
+ "dependencies": {
760
+ "@jridgewell/sourcemap-codec": "^1.5.0",
761
+ "@jridgewell/trace-mapping": "^0.3.24"
762
+ }
763
+ },
764
+ "node_modules/@jridgewell/remapping": {
765
+ "version": "2.3.5",
766
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
767
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
768
+ "dev": true,
769
+ "license": "MIT",
770
+ "dependencies": {
771
+ "@jridgewell/gen-mapping": "^0.3.5",
772
+ "@jridgewell/trace-mapping": "^0.3.24"
773
+ }
774
+ },
775
+ "node_modules/@jridgewell/resolve-uri": {
776
+ "version": "3.1.2",
777
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
778
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
779
+ "dev": true,
780
+ "license": "MIT",
781
+ "engines": {
782
+ "node": ">=6.0.0"
783
+ }
784
+ },
785
+ "node_modules/@jridgewell/sourcemap-codec": {
786
+ "version": "1.5.5",
787
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
788
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
789
+ "dev": true,
790
+ "license": "MIT"
791
+ },
792
+ "node_modules/@jridgewell/trace-mapping": {
793
+ "version": "0.3.31",
794
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
795
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
796
+ "dev": true,
797
+ "license": "MIT",
798
+ "dependencies": {
799
+ "@jridgewell/resolve-uri": "^3.1.0",
800
+ "@jridgewell/sourcemap-codec": "^1.4.14"
801
+ }
802
+ },
803
+ "node_modules/@rolldown/pluginutils": {
804
+ "version": "1.0.0-rc.3",
805
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
806
+ "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
807
+ "dev": true,
808
+ "license": "MIT"
809
+ },
810
+ "node_modules/@rollup/rollup-android-arm-eabi": {
811
+ "version": "4.60.2",
812
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
813
+ "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",
814
+ "cpu": [
815
+ "arm"
816
+ ],
817
+ "dev": true,
818
+ "license": "MIT",
819
+ "optional": true,
820
+ "os": [
821
+ "android"
822
+ ]
823
+ },
824
+ "node_modules/@rollup/rollup-android-arm64": {
825
+ "version": "4.60.2",
826
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",
827
+ "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",
828
+ "cpu": [
829
+ "arm64"
830
+ ],
831
+ "dev": true,
832
+ "license": "MIT",
833
+ "optional": true,
834
+ "os": [
835
+ "android"
836
+ ]
837
+ },
838
+ "node_modules/@rollup/rollup-darwin-arm64": {
839
+ "version": "4.60.2",
840
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",
841
+ "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",
842
+ "cpu": [
843
+ "arm64"
844
+ ],
845
+ "dev": true,
846
+ "license": "MIT",
847
+ "optional": true,
848
+ "os": [
849
+ "darwin"
850
+ ]
851
+ },
852
+ "node_modules/@rollup/rollup-darwin-x64": {
853
+ "version": "4.60.2",
854
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",
855
+ "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",
856
+ "cpu": [
857
+ "x64"
858
+ ],
859
+ "dev": true,
860
+ "license": "MIT",
861
+ "optional": true,
862
+ "os": [
863
+ "darwin"
864
+ ]
865
+ },
866
+ "node_modules/@rollup/rollup-freebsd-arm64": {
867
+ "version": "4.60.2",
868
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",
869
+ "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",
870
+ "cpu": [
871
+ "arm64"
872
+ ],
873
+ "dev": true,
874
+ "license": "MIT",
875
+ "optional": true,
876
+ "os": [
877
+ "freebsd"
878
+ ]
879
+ },
880
+ "node_modules/@rollup/rollup-freebsd-x64": {
881
+ "version": "4.60.2",
882
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",
883
+ "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",
884
+ "cpu": [
885
+ "x64"
886
+ ],
887
+ "dev": true,
888
+ "license": "MIT",
889
+ "optional": true,
890
+ "os": [
891
+ "freebsd"
892
+ ]
893
+ },
894
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
895
+ "version": "4.60.2",
896
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",
897
+ "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",
898
+ "cpu": [
899
+ "arm"
900
+ ],
901
+ "dev": true,
902
+ "license": "MIT",
903
+ "optional": true,
904
+ "os": [
905
+ "linux"
906
+ ]
907
+ },
908
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
909
+ "version": "4.60.2",
910
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",
911
+ "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",
912
+ "cpu": [
913
+ "arm"
914
+ ],
915
+ "dev": true,
916
+ "license": "MIT",
917
+ "optional": true,
918
+ "os": [
919
+ "linux"
920
+ ]
921
+ },
922
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
923
+ "version": "4.60.2",
924
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",
925
+ "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",
926
+ "cpu": [
927
+ "arm64"
928
+ ],
929
+ "dev": true,
930
+ "license": "MIT",
931
+ "optional": true,
932
+ "os": [
933
+ "linux"
934
+ ]
935
+ },
936
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
937
+ "version": "4.60.2",
938
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",
939
+ "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",
940
+ "cpu": [
941
+ "arm64"
942
+ ],
943
+ "dev": true,
944
+ "license": "MIT",
945
+ "optional": true,
946
+ "os": [
947
+ "linux"
948
+ ]
949
+ },
950
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
951
+ "version": "4.60.2",
952
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",
953
+ "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",
954
+ "cpu": [
955
+ "loong64"
956
+ ],
957
+ "dev": true,
958
+ "license": "MIT",
959
+ "optional": true,
960
+ "os": [
961
+ "linux"
962
+ ]
963
+ },
964
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
965
+ "version": "4.60.2",
966
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",
967
+ "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",
968
+ "cpu": [
969
+ "loong64"
970
+ ],
971
+ "dev": true,
972
+ "license": "MIT",
973
+ "optional": true,
974
+ "os": [
975
+ "linux"
976
+ ]
977
+ },
978
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
979
+ "version": "4.60.2",
980
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",
981
+ "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",
982
+ "cpu": [
983
+ "ppc64"
984
+ ],
985
+ "dev": true,
986
+ "license": "MIT",
987
+ "optional": true,
988
+ "os": [
989
+ "linux"
990
+ ]
991
+ },
992
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
993
+ "version": "4.60.2",
994
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",
995
+ "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",
996
+ "cpu": [
997
+ "ppc64"
998
+ ],
999
+ "dev": true,
1000
+ "license": "MIT",
1001
+ "optional": true,
1002
+ "os": [
1003
+ "linux"
1004
+ ]
1005
+ },
1006
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
1007
+ "version": "4.60.2",
1008
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",
1009
+ "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",
1010
+ "cpu": [
1011
+ "riscv64"
1012
+ ],
1013
+ "dev": true,
1014
+ "license": "MIT",
1015
+ "optional": true,
1016
+ "os": [
1017
+ "linux"
1018
+ ]
1019
+ },
1020
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
1021
+ "version": "4.60.2",
1022
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",
1023
+ "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",
1024
+ "cpu": [
1025
+ "riscv64"
1026
+ ],
1027
+ "dev": true,
1028
+ "license": "MIT",
1029
+ "optional": true,
1030
+ "os": [
1031
+ "linux"
1032
+ ]
1033
+ },
1034
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
1035
+ "version": "4.60.2",
1036
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
1037
+ "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
1038
+ "cpu": [
1039
+ "s390x"
1040
+ ],
1041
+ "dev": true,
1042
+ "license": "MIT",
1043
+ "optional": true,
1044
+ "os": [
1045
+ "linux"
1046
+ ]
1047
+ },
1048
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
1049
+ "version": "4.60.2",
1050
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
1051
+ "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
1052
+ "cpu": [
1053
+ "x64"
1054
+ ],
1055
+ "dev": true,
1056
+ "license": "MIT",
1057
+ "optional": true,
1058
+ "os": [
1059
+ "linux"
1060
+ ]
1061
+ },
1062
+ "node_modules/@rollup/rollup-linux-x64-musl": {
1063
+ "version": "4.60.2",
1064
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
1065
+ "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
1066
+ "cpu": [
1067
+ "x64"
1068
+ ],
1069
+ "dev": true,
1070
+ "license": "MIT",
1071
+ "optional": true,
1072
+ "os": [
1073
+ "linux"
1074
+ ]
1075
+ },
1076
+ "node_modules/@rollup/rollup-openbsd-x64": {
1077
+ "version": "4.60.2",
1078
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
1079
+ "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
1080
+ "cpu": [
1081
+ "x64"
1082
+ ],
1083
+ "dev": true,
1084
+ "license": "MIT",
1085
+ "optional": true,
1086
+ "os": [
1087
+ "openbsd"
1088
+ ]
1089
+ },
1090
+ "node_modules/@rollup/rollup-openharmony-arm64": {
1091
+ "version": "4.60.2",
1092
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
1093
+ "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
1094
+ "cpu": [
1095
+ "arm64"
1096
+ ],
1097
+ "dev": true,
1098
+ "license": "MIT",
1099
+ "optional": true,
1100
+ "os": [
1101
+ "openharmony"
1102
+ ]
1103
+ },
1104
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1105
+ "version": "4.60.2",
1106
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
1107
+ "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
1108
+ "cpu": [
1109
+ "arm64"
1110
+ ],
1111
+ "dev": true,
1112
+ "license": "MIT",
1113
+ "optional": true,
1114
+ "os": [
1115
+ "win32"
1116
+ ]
1117
+ },
1118
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1119
+ "version": "4.60.2",
1120
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
1121
+ "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
1122
+ "cpu": [
1123
+ "ia32"
1124
+ ],
1125
+ "dev": true,
1126
+ "license": "MIT",
1127
+ "optional": true,
1128
+ "os": [
1129
+ "win32"
1130
+ ]
1131
+ },
1132
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1133
+ "version": "4.60.2",
1134
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",
1135
+ "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",
1136
+ "cpu": [
1137
+ "x64"
1138
+ ],
1139
+ "dev": true,
1140
+ "license": "MIT",
1141
+ "optional": true,
1142
+ "os": [
1143
+ "win32"
1144
+ ]
1145
+ },
1146
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1147
+ "version": "4.60.2",
1148
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",
1149
+ "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",
1150
+ "cpu": [
1151
+ "x64"
1152
+ ],
1153
+ "dev": true,
1154
+ "license": "MIT",
1155
+ "optional": true,
1156
+ "os": [
1157
+ "win32"
1158
+ ]
1159
+ },
1160
+ "node_modules/@tailwindcss/node": {
1161
+ "version": "4.2.4",
1162
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz",
1163
+ "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==",
1164
+ "dev": true,
1165
+ "license": "MIT",
1166
+ "dependencies": {
1167
+ "@jridgewell/remapping": "^2.3.5",
1168
+ "enhanced-resolve": "^5.19.0",
1169
+ "jiti": "^2.6.1",
1170
+ "lightningcss": "1.32.0",
1171
+ "magic-string": "^0.30.21",
1172
+ "source-map-js": "^1.2.1",
1173
+ "tailwindcss": "4.2.4"
1174
+ }
1175
+ },
1176
+ "node_modules/@tailwindcss/oxide": {
1177
+ "version": "4.2.4",
1178
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz",
1179
+ "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==",
1180
+ "dev": true,
1181
+ "license": "MIT",
1182
+ "engines": {
1183
+ "node": ">= 20"
1184
+ },
1185
+ "optionalDependencies": {
1186
+ "@tailwindcss/oxide-android-arm64": "4.2.4",
1187
+ "@tailwindcss/oxide-darwin-arm64": "4.2.4",
1188
+ "@tailwindcss/oxide-darwin-x64": "4.2.4",
1189
+ "@tailwindcss/oxide-freebsd-x64": "4.2.4",
1190
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4",
1191
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4",
1192
+ "@tailwindcss/oxide-linux-arm64-musl": "4.2.4",
1193
+ "@tailwindcss/oxide-linux-x64-gnu": "4.2.4",
1194
+ "@tailwindcss/oxide-linux-x64-musl": "4.2.4",
1195
+ "@tailwindcss/oxide-wasm32-wasi": "4.2.4",
1196
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4",
1197
+ "@tailwindcss/oxide-win32-x64-msvc": "4.2.4"
1198
+ }
1199
+ },
1200
+ "node_modules/@tailwindcss/oxide-android-arm64": {
1201
+ "version": "4.2.4",
1202
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz",
1203
+ "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==",
1204
+ "cpu": [
1205
+ "arm64"
1206
+ ],
1207
+ "dev": true,
1208
+ "license": "MIT",
1209
+ "optional": true,
1210
+ "os": [
1211
+ "android"
1212
+ ],
1213
+ "engines": {
1214
+ "node": ">= 20"
1215
+ }
1216
+ },
1217
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
1218
+ "version": "4.2.4",
1219
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz",
1220
+ "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==",
1221
+ "cpu": [
1222
+ "arm64"
1223
+ ],
1224
+ "dev": true,
1225
+ "license": "MIT",
1226
+ "optional": true,
1227
+ "os": [
1228
+ "darwin"
1229
+ ],
1230
+ "engines": {
1231
+ "node": ">= 20"
1232
+ }
1233
+ },
1234
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
1235
+ "version": "4.2.4",
1236
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz",
1237
+ "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==",
1238
+ "cpu": [
1239
+ "x64"
1240
+ ],
1241
+ "dev": true,
1242
+ "license": "MIT",
1243
+ "optional": true,
1244
+ "os": [
1245
+ "darwin"
1246
+ ],
1247
+ "engines": {
1248
+ "node": ">= 20"
1249
+ }
1250
+ },
1251
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
1252
+ "version": "4.2.4",
1253
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz",
1254
+ "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==",
1255
+ "cpu": [
1256
+ "x64"
1257
+ ],
1258
+ "dev": true,
1259
+ "license": "MIT",
1260
+ "optional": true,
1261
+ "os": [
1262
+ "freebsd"
1263
+ ],
1264
+ "engines": {
1265
+ "node": ">= 20"
1266
+ }
1267
+ },
1268
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
1269
+ "version": "4.2.4",
1270
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz",
1271
+ "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==",
1272
+ "cpu": [
1273
+ "arm"
1274
+ ],
1275
+ "dev": true,
1276
+ "license": "MIT",
1277
+ "optional": true,
1278
+ "os": [
1279
+ "linux"
1280
+ ],
1281
+ "engines": {
1282
+ "node": ">= 20"
1283
+ }
1284
+ },
1285
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
1286
+ "version": "4.2.4",
1287
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz",
1288
+ "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==",
1289
+ "cpu": [
1290
+ "arm64"
1291
+ ],
1292
+ "dev": true,
1293
+ "license": "MIT",
1294
+ "optional": true,
1295
+ "os": [
1296
+ "linux"
1297
+ ],
1298
+ "engines": {
1299
+ "node": ">= 20"
1300
+ }
1301
+ },
1302
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
1303
+ "version": "4.2.4",
1304
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz",
1305
+ "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==",
1306
+ "cpu": [
1307
+ "arm64"
1308
+ ],
1309
+ "dev": true,
1310
+ "license": "MIT",
1311
+ "optional": true,
1312
+ "os": [
1313
+ "linux"
1314
+ ],
1315
+ "engines": {
1316
+ "node": ">= 20"
1317
+ }
1318
+ },
1319
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
1320
+ "version": "4.2.4",
1321
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz",
1322
+ "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==",
1323
+ "cpu": [
1324
+ "x64"
1325
+ ],
1326
+ "dev": true,
1327
+ "license": "MIT",
1328
+ "optional": true,
1329
+ "os": [
1330
+ "linux"
1331
+ ],
1332
+ "engines": {
1333
+ "node": ">= 20"
1334
+ }
1335
+ },
1336
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
1337
+ "version": "4.2.4",
1338
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz",
1339
+ "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==",
1340
+ "cpu": [
1341
+ "x64"
1342
+ ],
1343
+ "dev": true,
1344
+ "license": "MIT",
1345
+ "optional": true,
1346
+ "os": [
1347
+ "linux"
1348
+ ],
1349
+ "engines": {
1350
+ "node": ">= 20"
1351
+ }
1352
+ },
1353
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
1354
+ "version": "4.2.4",
1355
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz",
1356
+ "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==",
1357
+ "bundleDependencies": [
1358
+ "@napi-rs/wasm-runtime",
1359
+ "@emnapi/core",
1360
+ "@emnapi/runtime",
1361
+ "@tybys/wasm-util",
1362
+ "@emnapi/wasi-threads",
1363
+ "tslib"
1364
+ ],
1365
+ "cpu": [
1366
+ "wasm32"
1367
+ ],
1368
+ "dev": true,
1369
+ "license": "MIT",
1370
+ "optional": true,
1371
+ "dependencies": {
1372
+ "@emnapi/core": "^1.8.1",
1373
+ "@emnapi/runtime": "^1.8.1",
1374
+ "@emnapi/wasi-threads": "^1.1.0",
1375
+ "@napi-rs/wasm-runtime": "^1.1.1",
1376
+ "@tybys/wasm-util": "^0.10.1",
1377
+ "tslib": "^2.8.1"
1378
+ },
1379
+ "engines": {
1380
+ "node": ">=14.0.0"
1381
+ }
1382
+ },
1383
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
1384
+ "version": "4.2.4",
1385
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz",
1386
+ "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==",
1387
+ "cpu": [
1388
+ "arm64"
1389
+ ],
1390
+ "dev": true,
1391
+ "license": "MIT",
1392
+ "optional": true,
1393
+ "os": [
1394
+ "win32"
1395
+ ],
1396
+ "engines": {
1397
+ "node": ">= 20"
1398
+ }
1399
+ },
1400
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
1401
+ "version": "4.2.4",
1402
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz",
1403
+ "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==",
1404
+ "cpu": [
1405
+ "x64"
1406
+ ],
1407
+ "dev": true,
1408
+ "license": "MIT",
1409
+ "optional": true,
1410
+ "os": [
1411
+ "win32"
1412
+ ],
1413
+ "engines": {
1414
+ "node": ">= 20"
1415
+ }
1416
+ },
1417
+ "node_modules/@tailwindcss/vite": {
1418
+ "version": "4.2.4",
1419
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.4.tgz",
1420
+ "integrity": "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==",
1421
+ "dev": true,
1422
+ "license": "MIT",
1423
+ "dependencies": {
1424
+ "@tailwindcss/node": "4.2.4",
1425
+ "@tailwindcss/oxide": "4.2.4",
1426
+ "tailwindcss": "4.2.4"
1427
+ },
1428
+ "peerDependencies": {
1429
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
1430
+ }
1431
+ },
1432
+ "node_modules/@types/babel__core": {
1433
+ "version": "7.20.5",
1434
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1435
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1436
+ "dev": true,
1437
+ "license": "MIT",
1438
+ "dependencies": {
1439
+ "@babel/parser": "^7.20.7",
1440
+ "@babel/types": "^7.20.7",
1441
+ "@types/babel__generator": "*",
1442
+ "@types/babel__template": "*",
1443
+ "@types/babel__traverse": "*"
1444
+ }
1445
+ },
1446
+ "node_modules/@types/babel__generator": {
1447
+ "version": "7.27.0",
1448
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1449
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1450
+ "dev": true,
1451
+ "license": "MIT",
1452
+ "dependencies": {
1453
+ "@babel/types": "^7.0.0"
1454
+ }
1455
+ },
1456
+ "node_modules/@types/babel__template": {
1457
+ "version": "7.4.4",
1458
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1459
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1460
+ "dev": true,
1461
+ "license": "MIT",
1462
+ "dependencies": {
1463
+ "@babel/parser": "^7.1.0",
1464
+ "@babel/types": "^7.0.0"
1465
+ }
1466
+ },
1467
+ "node_modules/@types/babel__traverse": {
1468
+ "version": "7.28.0",
1469
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1470
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1471
+ "dev": true,
1472
+ "license": "MIT",
1473
+ "dependencies": {
1474
+ "@babel/types": "^7.28.2"
1475
+ }
1476
+ },
1477
+ "node_modules/@types/estree": {
1478
+ "version": "1.0.8",
1479
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
1480
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1481
+ "dev": true,
1482
+ "license": "MIT"
1483
+ },
1484
+ "node_modules/@types/node": {
1485
+ "version": "22.19.17",
1486
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
1487
+ "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
1488
+ "dev": true,
1489
+ "license": "MIT",
1490
+ "dependencies": {
1491
+ "undici-types": "~6.21.0"
1492
+ }
1493
+ },
1494
+ "node_modules/@types/react": {
1495
+ "version": "19.2.14",
1496
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
1497
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
1498
+ "dev": true,
1499
+ "license": "MIT",
1500
+ "dependencies": {
1501
+ "csstype": "^3.2.2"
1502
+ }
1503
+ },
1504
+ "node_modules/@types/react-dom": {
1505
+ "version": "19.2.3",
1506
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
1507
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
1508
+ "dev": true,
1509
+ "license": "MIT",
1510
+ "peerDependencies": {
1511
+ "@types/react": "^19.2.0"
1512
+ }
1513
+ },
1514
+ "node_modules/@vitejs/plugin-react": {
1515
+ "version": "5.2.0",
1516
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
1517
+ "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==",
1518
+ "dev": true,
1519
+ "license": "MIT",
1520
+ "dependencies": {
1521
+ "@babel/core": "^7.29.0",
1522
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1523
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1524
+ "@rolldown/pluginutils": "1.0.0-rc.3",
1525
+ "@types/babel__core": "^7.20.5",
1526
+ "react-refresh": "^0.18.0"
1527
+ },
1528
+ "engines": {
1529
+ "node": "^20.19.0 || >=22.12.0"
1530
+ },
1531
+ "peerDependencies": {
1532
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
1533
+ }
1534
+ },
1535
+ "node_modules/autoprefixer": {
1536
+ "version": "10.5.0",
1537
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
1538
+ "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==",
1539
+ "dev": true,
1540
+ "funding": [
1541
+ {
1542
+ "type": "opencollective",
1543
+ "url": "https://opencollective.com/postcss/"
1544
+ },
1545
+ {
1546
+ "type": "tidelift",
1547
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
1548
+ },
1549
+ {
1550
+ "type": "github",
1551
+ "url": "https://github.com/sponsors/ai"
1552
+ }
1553
+ ],
1554
+ "license": "MIT",
1555
+ "dependencies": {
1556
+ "browserslist": "^4.28.2",
1557
+ "caniuse-lite": "^1.0.30001787",
1558
+ "fraction.js": "^5.3.4",
1559
+ "picocolors": "^1.1.1",
1560
+ "postcss-value-parser": "^4.2.0"
1561
+ },
1562
+ "bin": {
1563
+ "autoprefixer": "bin/autoprefixer"
1564
+ },
1565
+ "engines": {
1566
+ "node": "^10 || ^12 || >=14"
1567
+ },
1568
+ "peerDependencies": {
1569
+ "postcss": "^8.1.0"
1570
+ }
1571
+ },
1572
+ "node_modules/baseline-browser-mapping": {
1573
+ "version": "2.10.22",
1574
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.22.tgz",
1575
+ "integrity": "sha512-6qruVrb5rse6WylFkU0FhBKKGuecWseqdpQfhkawn6ztyk2QlfwSRjsDxMCLJrkfmfN21qvhl9ABgaMeRkuwww==",
1576
+ "dev": true,
1577
+ "license": "Apache-2.0",
1578
+ "bin": {
1579
+ "baseline-browser-mapping": "dist/cli.cjs"
1580
+ },
1581
+ "engines": {
1582
+ "node": ">=6.0.0"
1583
+ }
1584
+ },
1585
+ "node_modules/browserslist": {
1586
+ "version": "4.28.2",
1587
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
1588
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
1589
+ "dev": true,
1590
+ "funding": [
1591
+ {
1592
+ "type": "opencollective",
1593
+ "url": "https://opencollective.com/browserslist"
1594
+ },
1595
+ {
1596
+ "type": "tidelift",
1597
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1598
+ },
1599
+ {
1600
+ "type": "github",
1601
+ "url": "https://github.com/sponsors/ai"
1602
+ }
1603
+ ],
1604
+ "license": "MIT",
1605
+ "dependencies": {
1606
+ "baseline-browser-mapping": "^2.10.12",
1607
+ "caniuse-lite": "^1.0.30001782",
1608
+ "electron-to-chromium": "^1.5.328",
1609
+ "node-releases": "^2.0.36",
1610
+ "update-browserslist-db": "^1.2.3"
1611
+ },
1612
+ "bin": {
1613
+ "browserslist": "cli.js"
1614
+ },
1615
+ "engines": {
1616
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1617
+ }
1618
+ },
1619
+ "node_modules/caniuse-lite": {
1620
+ "version": "1.0.30001790",
1621
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz",
1622
+ "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==",
1623
+ "dev": true,
1624
+ "funding": [
1625
+ {
1626
+ "type": "opencollective",
1627
+ "url": "https://opencollective.com/browserslist"
1628
+ },
1629
+ {
1630
+ "type": "tidelift",
1631
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1632
+ },
1633
+ {
1634
+ "type": "github",
1635
+ "url": "https://github.com/sponsors/ai"
1636
+ }
1637
+ ],
1638
+ "license": "CC-BY-4.0"
1639
+ },
1640
+ "node_modules/convert-source-map": {
1641
+ "version": "2.0.0",
1642
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1643
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1644
+ "dev": true,
1645
+ "license": "MIT"
1646
+ },
1647
+ "node_modules/csstype": {
1648
+ "version": "3.2.3",
1649
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1650
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1651
+ "dev": true,
1652
+ "license": "MIT"
1653
+ },
1654
+ "node_modules/debug": {
1655
+ "version": "4.4.3",
1656
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1657
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1658
+ "dev": true,
1659
+ "license": "MIT",
1660
+ "dependencies": {
1661
+ "ms": "^2.1.3"
1662
+ },
1663
+ "engines": {
1664
+ "node": ">=6.0"
1665
+ },
1666
+ "peerDependenciesMeta": {
1667
+ "supports-color": {
1668
+ "optional": true
1669
+ }
1670
+ }
1671
+ },
1672
+ "node_modules/detect-libc": {
1673
+ "version": "2.1.2",
1674
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
1675
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
1676
+ "dev": true,
1677
+ "license": "Apache-2.0",
1678
+ "engines": {
1679
+ "node": ">=8"
1680
+ }
1681
+ },
1682
+ "node_modules/electron-to-chromium": {
1683
+ "version": "1.5.344",
1684
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
1685
+ "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==",
1686
+ "dev": true,
1687
+ "license": "ISC"
1688
+ },
1689
+ "node_modules/enhanced-resolve": {
1690
+ "version": "5.21.0",
1691
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz",
1692
+ "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==",
1693
+ "dev": true,
1694
+ "license": "MIT",
1695
+ "dependencies": {
1696
+ "graceful-fs": "^4.2.4",
1697
+ "tapable": "^2.3.3"
1698
+ },
1699
+ "engines": {
1700
+ "node": ">=10.13.0"
1701
+ }
1702
+ },
1703
+ "node_modules/esbuild": {
1704
+ "version": "0.25.12",
1705
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
1706
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
1707
+ "dev": true,
1708
+ "hasInstallScript": true,
1709
+ "license": "MIT",
1710
+ "bin": {
1711
+ "esbuild": "bin/esbuild"
1712
+ },
1713
+ "engines": {
1714
+ "node": ">=18"
1715
+ },
1716
+ "optionalDependencies": {
1717
+ "@esbuild/aix-ppc64": "0.25.12",
1718
+ "@esbuild/android-arm": "0.25.12",
1719
+ "@esbuild/android-arm64": "0.25.12",
1720
+ "@esbuild/android-x64": "0.25.12",
1721
+ "@esbuild/darwin-arm64": "0.25.12",
1722
+ "@esbuild/darwin-x64": "0.25.12",
1723
+ "@esbuild/freebsd-arm64": "0.25.12",
1724
+ "@esbuild/freebsd-x64": "0.25.12",
1725
+ "@esbuild/linux-arm": "0.25.12",
1726
+ "@esbuild/linux-arm64": "0.25.12",
1727
+ "@esbuild/linux-ia32": "0.25.12",
1728
+ "@esbuild/linux-loong64": "0.25.12",
1729
+ "@esbuild/linux-mips64el": "0.25.12",
1730
+ "@esbuild/linux-ppc64": "0.25.12",
1731
+ "@esbuild/linux-riscv64": "0.25.12",
1732
+ "@esbuild/linux-s390x": "0.25.12",
1733
+ "@esbuild/linux-x64": "0.25.12",
1734
+ "@esbuild/netbsd-arm64": "0.25.12",
1735
+ "@esbuild/netbsd-x64": "0.25.12",
1736
+ "@esbuild/openbsd-arm64": "0.25.12",
1737
+ "@esbuild/openbsd-x64": "0.25.12",
1738
+ "@esbuild/openharmony-arm64": "0.25.12",
1739
+ "@esbuild/sunos-x64": "0.25.12",
1740
+ "@esbuild/win32-arm64": "0.25.12",
1741
+ "@esbuild/win32-ia32": "0.25.12",
1742
+ "@esbuild/win32-x64": "0.25.12"
1743
+ }
1744
+ },
1745
+ "node_modules/escalade": {
1746
+ "version": "3.2.0",
1747
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1748
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1749
+ "dev": true,
1750
+ "license": "MIT",
1751
+ "engines": {
1752
+ "node": ">=6"
1753
+ }
1754
+ },
1755
+ "node_modules/fancy-canvas": {
1756
+ "version": "2.1.0",
1757
+ "resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-2.1.0.tgz",
1758
+ "integrity": "sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==",
1759
+ "license": "MIT"
1760
+ },
1761
+ "node_modules/fdir": {
1762
+ "version": "6.5.0",
1763
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1764
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1765
+ "dev": true,
1766
+ "license": "MIT",
1767
+ "engines": {
1768
+ "node": ">=12.0.0"
1769
+ },
1770
+ "peerDependencies": {
1771
+ "picomatch": "^3 || ^4"
1772
+ },
1773
+ "peerDependenciesMeta": {
1774
+ "picomatch": {
1775
+ "optional": true
1776
+ }
1777
+ }
1778
+ },
1779
+ "node_modules/fraction.js": {
1780
+ "version": "5.3.4",
1781
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
1782
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
1783
+ "dev": true,
1784
+ "license": "MIT",
1785
+ "engines": {
1786
+ "node": "*"
1787
+ },
1788
+ "funding": {
1789
+ "type": "github",
1790
+ "url": "https://github.com/sponsors/rawify"
1791
+ }
1792
+ },
1793
+ "node_modules/framer-motion": {
1794
+ "version": "12.38.0",
1795
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
1796
+ "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
1797
+ "license": "MIT",
1798
+ "dependencies": {
1799
+ "motion-dom": "^12.38.0",
1800
+ "motion-utils": "^12.36.0",
1801
+ "tslib": "^2.4.0"
1802
+ },
1803
+ "peerDependencies": {
1804
+ "@emotion/is-prop-valid": "*",
1805
+ "react": "^18.0.0 || ^19.0.0",
1806
+ "react-dom": "^18.0.0 || ^19.0.0"
1807
+ },
1808
+ "peerDependenciesMeta": {
1809
+ "@emotion/is-prop-valid": {
1810
+ "optional": true
1811
+ },
1812
+ "react": {
1813
+ "optional": true
1814
+ },
1815
+ "react-dom": {
1816
+ "optional": true
1817
+ }
1818
+ }
1819
+ },
1820
+ "node_modules/fsevents": {
1821
+ "version": "2.3.3",
1822
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1823
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1824
+ "dev": true,
1825
+ "hasInstallScript": true,
1826
+ "license": "MIT",
1827
+ "optional": true,
1828
+ "os": [
1829
+ "darwin"
1830
+ ],
1831
+ "engines": {
1832
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1833
+ }
1834
+ },
1835
+ "node_modules/gensync": {
1836
+ "version": "1.0.0-beta.2",
1837
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1838
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1839
+ "dev": true,
1840
+ "license": "MIT",
1841
+ "engines": {
1842
+ "node": ">=6.9.0"
1843
+ }
1844
+ },
1845
+ "node_modules/graceful-fs": {
1846
+ "version": "4.2.11",
1847
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
1848
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
1849
+ "dev": true,
1850
+ "license": "ISC"
1851
+ },
1852
+ "node_modules/jiti": {
1853
+ "version": "2.6.1",
1854
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
1855
+ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
1856
+ "dev": true,
1857
+ "license": "MIT",
1858
+ "bin": {
1859
+ "jiti": "lib/jiti-cli.mjs"
1860
+ }
1861
+ },
1862
+ "node_modules/js-tokens": {
1863
+ "version": "4.0.0",
1864
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1865
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1866
+ "dev": true,
1867
+ "license": "MIT"
1868
+ },
1869
+ "node_modules/jsesc": {
1870
+ "version": "3.1.0",
1871
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1872
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1873
+ "dev": true,
1874
+ "license": "MIT",
1875
+ "bin": {
1876
+ "jsesc": "bin/jsesc"
1877
+ },
1878
+ "engines": {
1879
+ "node": ">=6"
1880
+ }
1881
+ },
1882
+ "node_modules/json5": {
1883
+ "version": "2.2.3",
1884
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1885
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1886
+ "dev": true,
1887
+ "license": "MIT",
1888
+ "bin": {
1889
+ "json5": "lib/cli.js"
1890
+ },
1891
+ "engines": {
1892
+ "node": ">=6"
1893
+ }
1894
+ },
1895
+ "node_modules/lightningcss": {
1896
+ "version": "1.32.0",
1897
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
1898
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
1899
+ "dev": true,
1900
+ "license": "MPL-2.0",
1901
+ "dependencies": {
1902
+ "detect-libc": "^2.0.3"
1903
+ },
1904
+ "engines": {
1905
+ "node": ">= 12.0.0"
1906
+ },
1907
+ "funding": {
1908
+ "type": "opencollective",
1909
+ "url": "https://opencollective.com/parcel"
1910
+ },
1911
+ "optionalDependencies": {
1912
+ "lightningcss-android-arm64": "1.32.0",
1913
+ "lightningcss-darwin-arm64": "1.32.0",
1914
+ "lightningcss-darwin-x64": "1.32.0",
1915
+ "lightningcss-freebsd-x64": "1.32.0",
1916
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
1917
+ "lightningcss-linux-arm64-gnu": "1.32.0",
1918
+ "lightningcss-linux-arm64-musl": "1.32.0",
1919
+ "lightningcss-linux-x64-gnu": "1.32.0",
1920
+ "lightningcss-linux-x64-musl": "1.32.0",
1921
+ "lightningcss-win32-arm64-msvc": "1.32.0",
1922
+ "lightningcss-win32-x64-msvc": "1.32.0"
1923
+ }
1924
+ },
1925
+ "node_modules/lightningcss-android-arm64": {
1926
+ "version": "1.32.0",
1927
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
1928
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
1929
+ "cpu": [
1930
+ "arm64"
1931
+ ],
1932
+ "dev": true,
1933
+ "license": "MPL-2.0",
1934
+ "optional": true,
1935
+ "os": [
1936
+ "android"
1937
+ ],
1938
+ "engines": {
1939
+ "node": ">= 12.0.0"
1940
+ },
1941
+ "funding": {
1942
+ "type": "opencollective",
1943
+ "url": "https://opencollective.com/parcel"
1944
+ }
1945
+ },
1946
+ "node_modules/lightningcss-darwin-arm64": {
1947
+ "version": "1.32.0",
1948
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
1949
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
1950
+ "cpu": [
1951
+ "arm64"
1952
+ ],
1953
+ "dev": true,
1954
+ "license": "MPL-2.0",
1955
+ "optional": true,
1956
+ "os": [
1957
+ "darwin"
1958
+ ],
1959
+ "engines": {
1960
+ "node": ">= 12.0.0"
1961
+ },
1962
+ "funding": {
1963
+ "type": "opencollective",
1964
+ "url": "https://opencollective.com/parcel"
1965
+ }
1966
+ },
1967
+ "node_modules/lightningcss-darwin-x64": {
1968
+ "version": "1.32.0",
1969
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
1970
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
1971
+ "cpu": [
1972
+ "x64"
1973
+ ],
1974
+ "dev": true,
1975
+ "license": "MPL-2.0",
1976
+ "optional": true,
1977
+ "os": [
1978
+ "darwin"
1979
+ ],
1980
+ "engines": {
1981
+ "node": ">= 12.0.0"
1982
+ },
1983
+ "funding": {
1984
+ "type": "opencollective",
1985
+ "url": "https://opencollective.com/parcel"
1986
+ }
1987
+ },
1988
+ "node_modules/lightningcss-freebsd-x64": {
1989
+ "version": "1.32.0",
1990
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
1991
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
1992
+ "cpu": [
1993
+ "x64"
1994
+ ],
1995
+ "dev": true,
1996
+ "license": "MPL-2.0",
1997
+ "optional": true,
1998
+ "os": [
1999
+ "freebsd"
2000
+ ],
2001
+ "engines": {
2002
+ "node": ">= 12.0.0"
2003
+ },
2004
+ "funding": {
2005
+ "type": "opencollective",
2006
+ "url": "https://opencollective.com/parcel"
2007
+ }
2008
+ },
2009
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
2010
+ "version": "1.32.0",
2011
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
2012
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
2013
+ "cpu": [
2014
+ "arm"
2015
+ ],
2016
+ "dev": true,
2017
+ "license": "MPL-2.0",
2018
+ "optional": true,
2019
+ "os": [
2020
+ "linux"
2021
+ ],
2022
+ "engines": {
2023
+ "node": ">= 12.0.0"
2024
+ },
2025
+ "funding": {
2026
+ "type": "opencollective",
2027
+ "url": "https://opencollective.com/parcel"
2028
+ }
2029
+ },
2030
+ "node_modules/lightningcss-linux-arm64-gnu": {
2031
+ "version": "1.32.0",
2032
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
2033
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
2034
+ "cpu": [
2035
+ "arm64"
2036
+ ],
2037
+ "dev": true,
2038
+ "license": "MPL-2.0",
2039
+ "optional": true,
2040
+ "os": [
2041
+ "linux"
2042
+ ],
2043
+ "engines": {
2044
+ "node": ">= 12.0.0"
2045
+ },
2046
+ "funding": {
2047
+ "type": "opencollective",
2048
+ "url": "https://opencollective.com/parcel"
2049
+ }
2050
+ },
2051
+ "node_modules/lightningcss-linux-arm64-musl": {
2052
+ "version": "1.32.0",
2053
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
2054
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
2055
+ "cpu": [
2056
+ "arm64"
2057
+ ],
2058
+ "dev": true,
2059
+ "license": "MPL-2.0",
2060
+ "optional": true,
2061
+ "os": [
2062
+ "linux"
2063
+ ],
2064
+ "engines": {
2065
+ "node": ">= 12.0.0"
2066
+ },
2067
+ "funding": {
2068
+ "type": "opencollective",
2069
+ "url": "https://opencollective.com/parcel"
2070
+ }
2071
+ },
2072
+ "node_modules/lightningcss-linux-x64-gnu": {
2073
+ "version": "1.32.0",
2074
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
2075
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
2076
+ "cpu": [
2077
+ "x64"
2078
+ ],
2079
+ "dev": true,
2080
+ "license": "MPL-2.0",
2081
+ "optional": true,
2082
+ "os": [
2083
+ "linux"
2084
+ ],
2085
+ "engines": {
2086
+ "node": ">= 12.0.0"
2087
+ },
2088
+ "funding": {
2089
+ "type": "opencollective",
2090
+ "url": "https://opencollective.com/parcel"
2091
+ }
2092
+ },
2093
+ "node_modules/lightningcss-linux-x64-musl": {
2094
+ "version": "1.32.0",
2095
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
2096
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
2097
+ "cpu": [
2098
+ "x64"
2099
+ ],
2100
+ "dev": true,
2101
+ "license": "MPL-2.0",
2102
+ "optional": true,
2103
+ "os": [
2104
+ "linux"
2105
+ ],
2106
+ "engines": {
2107
+ "node": ">= 12.0.0"
2108
+ },
2109
+ "funding": {
2110
+ "type": "opencollective",
2111
+ "url": "https://opencollective.com/parcel"
2112
+ }
2113
+ },
2114
+ "node_modules/lightningcss-win32-arm64-msvc": {
2115
+ "version": "1.32.0",
2116
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
2117
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
2118
+ "cpu": [
2119
+ "arm64"
2120
+ ],
2121
+ "dev": true,
2122
+ "license": "MPL-2.0",
2123
+ "optional": true,
2124
+ "os": [
2125
+ "win32"
2126
+ ],
2127
+ "engines": {
2128
+ "node": ">= 12.0.0"
2129
+ },
2130
+ "funding": {
2131
+ "type": "opencollective",
2132
+ "url": "https://opencollective.com/parcel"
2133
+ }
2134
+ },
2135
+ "node_modules/lightningcss-win32-x64-msvc": {
2136
+ "version": "1.32.0",
2137
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
2138
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
2139
+ "cpu": [
2140
+ "x64"
2141
+ ],
2142
+ "dev": true,
2143
+ "license": "MPL-2.0",
2144
+ "optional": true,
2145
+ "os": [
2146
+ "win32"
2147
+ ],
2148
+ "engines": {
2149
+ "node": ">= 12.0.0"
2150
+ },
2151
+ "funding": {
2152
+ "type": "opencollective",
2153
+ "url": "https://opencollective.com/parcel"
2154
+ }
2155
+ },
2156
+ "node_modules/lightweight-charts": {
2157
+ "version": "4.2.3",
2158
+ "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-4.2.3.tgz",
2159
+ "integrity": "sha512-5kS/2hY3wNYNzhnS8Gb+GAS07DX8GPF2YVDnd2NMC85gJVQ6RLU6YrXNgNJ6eg0AnWPwCnvaGtYmGky3HiLQEw==",
2160
+ "license": "Apache-2.0",
2161
+ "dependencies": {
2162
+ "fancy-canvas": "2.1.0"
2163
+ }
2164
+ },
2165
+ "node_modules/lru-cache": {
2166
+ "version": "5.1.1",
2167
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
2168
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
2169
+ "dev": true,
2170
+ "license": "ISC",
2171
+ "dependencies": {
2172
+ "yallist": "^3.0.2"
2173
+ }
2174
+ },
2175
+ "node_modules/lucide-react": {
2176
+ "version": "0.546.0",
2177
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.546.0.tgz",
2178
+ "integrity": "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==",
2179
+ "license": "ISC",
2180
+ "peerDependencies": {
2181
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
2182
+ }
2183
+ },
2184
+ "node_modules/magic-string": {
2185
+ "version": "0.30.21",
2186
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
2187
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
2188
+ "dev": true,
2189
+ "license": "MIT",
2190
+ "dependencies": {
2191
+ "@jridgewell/sourcemap-codec": "^1.5.5"
2192
+ }
2193
+ },
2194
+ "node_modules/motion": {
2195
+ "version": "12.38.0",
2196
+ "resolved": "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz",
2197
+ "integrity": "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==",
2198
+ "license": "MIT",
2199
+ "dependencies": {
2200
+ "framer-motion": "^12.38.0",
2201
+ "tslib": "^2.4.0"
2202
+ },
2203
+ "peerDependencies": {
2204
+ "@emotion/is-prop-valid": "*",
2205
+ "react": "^18.0.0 || ^19.0.0",
2206
+ "react-dom": "^18.0.0 || ^19.0.0"
2207
+ },
2208
+ "peerDependenciesMeta": {
2209
+ "@emotion/is-prop-valid": {
2210
+ "optional": true
2211
+ },
2212
+ "react": {
2213
+ "optional": true
2214
+ },
2215
+ "react-dom": {
2216
+ "optional": true
2217
+ }
2218
+ }
2219
+ },
2220
+ "node_modules/motion-dom": {
2221
+ "version": "12.38.0",
2222
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
2223
+ "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
2224
+ "license": "MIT",
2225
+ "dependencies": {
2226
+ "motion-utils": "^12.36.0"
2227
+ }
2228
+ },
2229
+ "node_modules/motion-utils": {
2230
+ "version": "12.36.0",
2231
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
2232
+ "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
2233
+ "license": "MIT"
2234
+ },
2235
+ "node_modules/ms": {
2236
+ "version": "2.1.3",
2237
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
2238
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
2239
+ "dev": true,
2240
+ "license": "MIT"
2241
+ },
2242
+ "node_modules/nanoid": {
2243
+ "version": "3.3.11",
2244
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
2245
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
2246
+ "dev": true,
2247
+ "funding": [
2248
+ {
2249
+ "type": "github",
2250
+ "url": "https://github.com/sponsors/ai"
2251
+ }
2252
+ ],
2253
+ "license": "MIT",
2254
+ "bin": {
2255
+ "nanoid": "bin/nanoid.cjs"
2256
+ },
2257
+ "engines": {
2258
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
2259
+ }
2260
+ },
2261
+ "node_modules/node-releases": {
2262
+ "version": "2.0.38",
2263
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
2264
+ "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
2265
+ "dev": true,
2266
+ "license": "MIT"
2267
+ },
2268
+ "node_modules/picocolors": {
2269
+ "version": "1.1.1",
2270
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
2271
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
2272
+ "dev": true,
2273
+ "license": "ISC"
2274
+ },
2275
+ "node_modules/picomatch": {
2276
+ "version": "4.0.4",
2277
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
2278
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
2279
+ "dev": true,
2280
+ "license": "MIT",
2281
+ "engines": {
2282
+ "node": ">=12"
2283
+ },
2284
+ "funding": {
2285
+ "url": "https://github.com/sponsors/jonschlinkert"
2286
+ }
2287
+ },
2288
+ "node_modules/postcss": {
2289
+ "version": "8.5.10",
2290
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
2291
+ "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
2292
+ "dev": true,
2293
+ "funding": [
2294
+ {
2295
+ "type": "opencollective",
2296
+ "url": "https://opencollective.com/postcss/"
2297
+ },
2298
+ {
2299
+ "type": "tidelift",
2300
+ "url": "https://tidelift.com/funding/github/npm/postcss"
2301
+ },
2302
+ {
2303
+ "type": "github",
2304
+ "url": "https://github.com/sponsors/ai"
2305
+ }
2306
+ ],
2307
+ "license": "MIT",
2308
+ "dependencies": {
2309
+ "nanoid": "^3.3.11",
2310
+ "picocolors": "^1.1.1",
2311
+ "source-map-js": "^1.2.1"
2312
+ },
2313
+ "engines": {
2314
+ "node": "^10 || ^12 || >=14"
2315
+ }
2316
+ },
2317
+ "node_modules/postcss-value-parser": {
2318
+ "version": "4.2.0",
2319
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
2320
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
2321
+ "dev": true,
2322
+ "license": "MIT"
2323
+ },
2324
+ "node_modules/react": {
2325
+ "version": "19.2.5",
2326
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
2327
+ "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
2328
+ "license": "MIT",
2329
+ "engines": {
2330
+ "node": ">=0.10.0"
2331
+ }
2332
+ },
2333
+ "node_modules/react-dom": {
2334
+ "version": "19.2.5",
2335
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
2336
+ "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
2337
+ "license": "MIT",
2338
+ "dependencies": {
2339
+ "scheduler": "^0.27.0"
2340
+ },
2341
+ "peerDependencies": {
2342
+ "react": "^19.2.5"
2343
+ }
2344
+ },
2345
+ "node_modules/react-refresh": {
2346
+ "version": "0.18.0",
2347
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
2348
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
2349
+ "dev": true,
2350
+ "license": "MIT",
2351
+ "engines": {
2352
+ "node": ">=0.10.0"
2353
+ }
2354
+ },
2355
+ "node_modules/rollup": {
2356
+ "version": "4.60.2",
2357
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
2358
+ "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",
2359
+ "dev": true,
2360
+ "license": "MIT",
2361
+ "dependencies": {
2362
+ "@types/estree": "1.0.8"
2363
+ },
2364
+ "bin": {
2365
+ "rollup": "dist/bin/rollup"
2366
+ },
2367
+ "engines": {
2368
+ "node": ">=18.0.0",
2369
+ "npm": ">=8.0.0"
2370
+ },
2371
+ "optionalDependencies": {
2372
+ "@rollup/rollup-android-arm-eabi": "4.60.2",
2373
+ "@rollup/rollup-android-arm64": "4.60.2",
2374
+ "@rollup/rollup-darwin-arm64": "4.60.2",
2375
+ "@rollup/rollup-darwin-x64": "4.60.2",
2376
+ "@rollup/rollup-freebsd-arm64": "4.60.2",
2377
+ "@rollup/rollup-freebsd-x64": "4.60.2",
2378
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
2379
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.2",
2380
+ "@rollup/rollup-linux-arm64-gnu": "4.60.2",
2381
+ "@rollup/rollup-linux-arm64-musl": "4.60.2",
2382
+ "@rollup/rollup-linux-loong64-gnu": "4.60.2",
2383
+ "@rollup/rollup-linux-loong64-musl": "4.60.2",
2384
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.2",
2385
+ "@rollup/rollup-linux-ppc64-musl": "4.60.2",
2386
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.2",
2387
+ "@rollup/rollup-linux-riscv64-musl": "4.60.2",
2388
+ "@rollup/rollup-linux-s390x-gnu": "4.60.2",
2389
+ "@rollup/rollup-linux-x64-gnu": "4.60.2",
2390
+ "@rollup/rollup-linux-x64-musl": "4.60.2",
2391
+ "@rollup/rollup-openbsd-x64": "4.60.2",
2392
+ "@rollup/rollup-openharmony-arm64": "4.60.2",
2393
+ "@rollup/rollup-win32-arm64-msvc": "4.60.2",
2394
+ "@rollup/rollup-win32-ia32-msvc": "4.60.2",
2395
+ "@rollup/rollup-win32-x64-gnu": "4.60.2",
2396
+ "@rollup/rollup-win32-x64-msvc": "4.60.2",
2397
+ "fsevents": "~2.3.2"
2398
+ }
2399
+ },
2400
+ "node_modules/scheduler": {
2401
+ "version": "0.27.0",
2402
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
2403
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
2404
+ "license": "MIT"
2405
+ },
2406
+ "node_modules/semver": {
2407
+ "version": "6.3.1",
2408
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
2409
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
2410
+ "dev": true,
2411
+ "license": "ISC",
2412
+ "bin": {
2413
+ "semver": "bin/semver.js"
2414
+ }
2415
+ },
2416
+ "node_modules/source-map-js": {
2417
+ "version": "1.2.1",
2418
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
2419
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
2420
+ "dev": true,
2421
+ "license": "BSD-3-Clause",
2422
+ "engines": {
2423
+ "node": ">=0.10.0"
2424
+ }
2425
+ },
2426
+ "node_modules/tailwindcss": {
2427
+ "version": "4.2.4",
2428
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz",
2429
+ "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==",
2430
+ "dev": true,
2431
+ "license": "MIT"
2432
+ },
2433
+ "node_modules/tapable": {
2434
+ "version": "2.3.3",
2435
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
2436
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
2437
+ "dev": true,
2438
+ "license": "MIT",
2439
+ "engines": {
2440
+ "node": ">=6"
2441
+ },
2442
+ "funding": {
2443
+ "type": "opencollective",
2444
+ "url": "https://opencollective.com/webpack"
2445
+ }
2446
+ },
2447
+ "node_modules/tinyglobby": {
2448
+ "version": "0.2.16",
2449
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
2450
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
2451
+ "dev": true,
2452
+ "license": "MIT",
2453
+ "dependencies": {
2454
+ "fdir": "^6.5.0",
2455
+ "picomatch": "^4.0.4"
2456
+ },
2457
+ "engines": {
2458
+ "node": ">=12.0.0"
2459
+ },
2460
+ "funding": {
2461
+ "url": "https://github.com/sponsors/SuperchupuDev"
2462
+ }
2463
+ },
2464
+ "node_modules/tslib": {
2465
+ "version": "2.8.1",
2466
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
2467
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
2468
+ "license": "0BSD"
2469
+ },
2470
+ "node_modules/typescript": {
2471
+ "version": "5.8.3",
2472
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
2473
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
2474
+ "dev": true,
2475
+ "license": "Apache-2.0",
2476
+ "bin": {
2477
+ "tsc": "bin/tsc",
2478
+ "tsserver": "bin/tsserver"
2479
+ },
2480
+ "engines": {
2481
+ "node": ">=14.17"
2482
+ }
2483
+ },
2484
+ "node_modules/undici-types": {
2485
+ "version": "6.21.0",
2486
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
2487
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
2488
+ "dev": true,
2489
+ "license": "MIT"
2490
+ },
2491
+ "node_modules/update-browserslist-db": {
2492
+ "version": "1.2.3",
2493
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
2494
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
2495
+ "dev": true,
2496
+ "funding": [
2497
+ {
2498
+ "type": "opencollective",
2499
+ "url": "https://opencollective.com/browserslist"
2500
+ },
2501
+ {
2502
+ "type": "tidelift",
2503
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
2504
+ },
2505
+ {
2506
+ "type": "github",
2507
+ "url": "https://github.com/sponsors/ai"
2508
+ }
2509
+ ],
2510
+ "license": "MIT",
2511
+ "dependencies": {
2512
+ "escalade": "^3.2.0",
2513
+ "picocolors": "^1.1.1"
2514
+ },
2515
+ "bin": {
2516
+ "update-browserslist-db": "cli.js"
2517
+ },
2518
+ "peerDependencies": {
2519
+ "browserslist": ">= 4.21.0"
2520
+ }
2521
+ },
2522
+ "node_modules/vite": {
2523
+ "version": "6.4.2",
2524
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
2525
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
2526
+ "dev": true,
2527
+ "license": "MIT",
2528
+ "dependencies": {
2529
+ "esbuild": "^0.25.0",
2530
+ "fdir": "^6.4.4",
2531
+ "picomatch": "^4.0.2",
2532
+ "postcss": "^8.5.3",
2533
+ "rollup": "^4.34.9",
2534
+ "tinyglobby": "^0.2.13"
2535
+ },
2536
+ "bin": {
2537
+ "vite": "bin/vite.js"
2538
+ },
2539
+ "engines": {
2540
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
2541
+ },
2542
+ "funding": {
2543
+ "url": "https://github.com/vitejs/vite?sponsor=1"
2544
+ },
2545
+ "optionalDependencies": {
2546
+ "fsevents": "~2.3.3"
2547
+ },
2548
+ "peerDependencies": {
2549
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
2550
+ "jiti": ">=1.21.0",
2551
+ "less": "*",
2552
+ "lightningcss": "^1.21.0",
2553
+ "sass": "*",
2554
+ "sass-embedded": "*",
2555
+ "stylus": "*",
2556
+ "sugarss": "*",
2557
+ "terser": "^5.16.0",
2558
+ "tsx": "^4.8.1",
2559
+ "yaml": "^2.4.2"
2560
+ },
2561
+ "peerDependenciesMeta": {
2562
+ "@types/node": {
2563
+ "optional": true
2564
+ },
2565
+ "jiti": {
2566
+ "optional": true
2567
+ },
2568
+ "less": {
2569
+ "optional": true
2570
+ },
2571
+ "lightningcss": {
2572
+ "optional": true
2573
+ },
2574
+ "sass": {
2575
+ "optional": true
2576
+ },
2577
+ "sass-embedded": {
2578
+ "optional": true
2579
+ },
2580
+ "stylus": {
2581
+ "optional": true
2582
+ },
2583
+ "sugarss": {
2584
+ "optional": true
2585
+ },
2586
+ "terser": {
2587
+ "optional": true
2588
+ },
2589
+ "tsx": {
2590
+ "optional": true
2591
+ },
2592
+ "yaml": {
2593
+ "optional": true
2594
+ }
2595
+ }
2596
+ },
2597
+ "node_modules/yallist": {
2598
+ "version": "3.1.1",
2599
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
2600
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
2601
+ "dev": true,
2602
+ "license": "ISC"
2603
+ }
2604
+ }
2605
+ }
frontend/package.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "stocker-ai-frontend",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite --port=3000 --host=0.0.0.0",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "clean": "rm -rf dist",
11
+ "lint": "tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "lightweight-charts": "^4.2.3",
15
+ "lucide-react": "^0.546.0",
16
+ "motion": "^12.23.24",
17
+ "react": "^19.0.0",
18
+ "react-dom": "^19.0.0"
19
+ },
20
+ "devDependencies": {
21
+ "@tailwindcss/vite": "^4.1.14",
22
+ "@types/node": "^22.14.0",
23
+ "@types/react": "^19.2.14",
24
+ "@types/react-dom": "^19.2.3",
25
+ "@vitejs/plugin-react": "^5.0.4",
26
+ "autoprefixer": "^10.4.21",
27
+ "tailwindcss": "^4.1.14",
28
+ "typescript": "~5.8.2",
29
+ "vite": "^6.2.0"
30
+ }
31
+ }
frontend/src/App.tsx ADDED
@@ -0,0 +1,1554 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { useEffect, useMemo, useRef, useState } from 'react';
7
+ import {
8
+ Monitor,
9
+ Users,
10
+ TrendingUp,
11
+ Library,
12
+ Wallet,
13
+ Brain,
14
+ FileText,
15
+ LifeBuoy,
16
+ Bell,
17
+ CreditCard,
18
+ User,
19
+ Search,
20
+ ChevronUp,
21
+ Cpu,
22
+ BarChart3,
23
+ Globe,
24
+ Newspaper,
25
+ Zap,
26
+ Activity,
27
+ Calendar,
28
+ LineChart,
29
+ } from 'lucide-react';
30
+ import { motion, AnimatePresence } from 'motion/react';
31
+ import {
32
+ CrosshairMode,
33
+ ColorType,
34
+ createChart,
35
+ type IChartApi,
36
+ type ISeriesApi,
37
+ } from 'lightweight-charts';
38
+
39
+ import {
40
+ api,
41
+ SPECIALIST_DISPLAY,
42
+ statusFromSignal,
43
+ useStockerEnv,
44
+ type CouncilDecision,
45
+ type EnvironmentState,
46
+ type MarketObservation,
47
+ type OhlcvBar,
48
+ type OhlcvResponse,
49
+ type Side,
50
+ type StockerEnv,
51
+ type TrainingMetrics,
52
+ } from './api';
53
+
54
+ // --------------------------------------------------------------------- atoms
55
+ const SidebarItem = ({
56
+ icon: Icon,
57
+ label,
58
+ active,
59
+ onClick,
60
+ }: {
61
+ icon: any;
62
+ label: string;
63
+ active: boolean;
64
+ onClick: () => void;
65
+ }) => (
66
+ <button
67
+ onClick={onClick}
68
+ className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-lg transition-all duration-200 group relative ${
69
+ active
70
+ ? 'text-[#00FF41] bg-gradient-to-r from-[#00FF41]/10 to-transparent border-l-2 border-[#00FF41]'
71
+ : 'text-neutral-400 hover:bg-white/5 hover:text-[#00FF41] hover:translate-x-1'
72
+ }`}
73
+ >
74
+ <Icon className={`w-5 h-5 ${active ? 'text-[#00FF41]' : 'group-hover:text-[#00FF41]'}`} />
75
+ <span className="font-medium text-[13px] tracking-tight">{label}</span>
76
+ </button>
77
+ );
78
+
79
+ const AgentCard = ({
80
+ title,
81
+ icon: Icon,
82
+ stat,
83
+ color,
84
+ children,
85
+ active,
86
+ }: {
87
+ title: string;
88
+ icon: any;
89
+ stat?: string;
90
+ color: string;
91
+ children: any;
92
+ active?: boolean;
93
+ }) => (
94
+ <div
95
+ className={`glass-panel rounded-xl flex flex-col transition-all duration-300 group hover:border-${color}/40 ${
96
+ active ? `border-${color}/30 shadow-[0_0_30px_rgba(115,31,255,0.1)]` : ''
97
+ }`}
98
+ >
99
+ <div className="p-3 px-4 border-b border-white/5 flex justify-between items-center bg-white/[0.02]">
100
+ <div className="flex items-center gap-2">
101
+ <Icon className={`w-4 h-4 text-${color}`} />
102
+ <h3 className="font-semibold text-sm text-[#e5e2e1]">{title}</h3>
103
+ </div>
104
+ {stat && <span className={`font-mono text-xs font-bold text-${color}`}>{stat}</span>}
105
+ </div>
106
+ <div className="p-4 flex-1 flex flex-col gap-3">{children}</div>
107
+ </div>
108
+ );
109
+
110
+ // ---------------------------------------------------------------------- chart
111
+
112
+ const CandlestickChart = ({ bars }: { bars: OhlcvBar[] }) => {
113
+ const containerRef = useRef<HTMLDivElement | null>(null);
114
+ const chartRef = useRef<IChartApi | null>(null);
115
+ const candleRef = useRef<ISeriesApi<'Candlestick'> | null>(null);
116
+ const volumeRef = useRef<ISeriesApi<'Histogram'> | null>(null);
117
+
118
+ // mount once
119
+ useEffect(() => {
120
+ if (!containerRef.current) return;
121
+ const chart = createChart(containerRef.current, {
122
+ layout: {
123
+ background: { type: ColorType.Solid, color: 'transparent' },
124
+ textColor: '#a3a3a3',
125
+ fontFamily: 'JetBrains Mono, ui-monospace, monospace',
126
+ },
127
+ grid: {
128
+ vertLines: { color: 'rgba(255,255,255,0.04)' },
129
+ horzLines: { color: 'rgba(255,255,255,0.04)' },
130
+ },
131
+ timeScale: {
132
+ borderVisible: false,
133
+ timeVisible: false,
134
+ secondsVisible: false,
135
+ },
136
+ rightPriceScale: {
137
+ borderVisible: false,
138
+ scaleMargins: { top: 0.05, bottom: 0.28 },
139
+ },
140
+ crosshair: {
141
+ mode: CrosshairMode.Normal,
142
+ vertLine: { color: 'rgba(0,255,65,0.3)', width: 1, style: 3, labelBackgroundColor: '#00FF41' },
143
+ horzLine: { color: 'rgba(0,255,65,0.3)', width: 1, style: 3, labelBackgroundColor: '#00FF41' },
144
+ },
145
+ autoSize: false,
146
+ });
147
+ chartRef.current = chart;
148
+
149
+ candleRef.current = chart.addCandlestickSeries({
150
+ upColor: '#00FF41',
151
+ downColor: '#ffb4ab',
152
+ wickUpColor: '#00FF41',
153
+ wickDownColor: '#ffb4ab',
154
+ borderVisible: false,
155
+ });
156
+
157
+ volumeRef.current = chart.addHistogramSeries({
158
+ priceFormat: { type: 'volume' },
159
+ priceScaleId: '',
160
+ color: 'rgba(0,255,65,0.3)',
161
+ });
162
+ volumeRef.current.priceScale().applyOptions({
163
+ scaleMargins: { top: 0.78, bottom: 0 },
164
+ });
165
+
166
+ const ro = new ResizeObserver((entries) => {
167
+ const rect = entries[0]?.contentRect;
168
+ if (rect) chart.applyOptions({ width: Math.floor(rect.width), height: Math.floor(rect.height) });
169
+ });
170
+ ro.observe(containerRef.current);
171
+
172
+ return () => {
173
+ ro.disconnect();
174
+ chart.remove();
175
+ chartRef.current = null;
176
+ candleRef.current = null;
177
+ volumeRef.current = null;
178
+ };
179
+ }, []);
180
+
181
+ // push data when bars change
182
+ useEffect(() => {
183
+ if (!candleRef.current || !volumeRef.current) return;
184
+ if (!bars || bars.length === 0) {
185
+ candleRef.current.setData([]);
186
+ volumeRef.current.setData([]);
187
+ return;
188
+ }
189
+ candleRef.current.setData(
190
+ bars.map((b) => ({
191
+ time: b.time,
192
+ open: b.open,
193
+ high: b.high,
194
+ low: b.low,
195
+ close: b.close,
196
+ })),
197
+ );
198
+ volumeRef.current.setData(
199
+ bars.map((b) => ({
200
+ time: b.time,
201
+ value: b.volume,
202
+ color: b.close >= b.open ? 'rgba(0,255,65,0.35)' : 'rgba(255,180,171,0.35)',
203
+ })),
204
+ );
205
+ chartRef.current?.timeScale().fitContent();
206
+ }, [bars]);
207
+
208
+ return <div ref={containerRef} className="flex-1 min-h-[300px] w-full" />;
209
+ };
210
+
211
+ // ---------------------------------------------------------------- TerminalView
212
+
213
+ const TerminalView = ({ env }: { env: StockerEnv }) => {
214
+ const { observation, envState, council, ohlcv, startingCash, submitTrade, loading } = env;
215
+
216
+ const [side, setSide] = useState<Side>('buy');
217
+ const [qty, setQty] = useState<number>(10);
218
+
219
+ const change = useMemo(() => {
220
+ if (!ohlcv || ohlcv.bars.length < 2) return 0;
221
+ const last = ohlcv.bars[ohlcv.bars.length - 1];
222
+ const prev = ohlcv.bars[ohlcv.bars.length - 2];
223
+ return ((last.close - prev.close) / prev.close) * 100;
224
+ }, [ohlcv]);
225
+
226
+ const lastVolume = ohlcv?.bars[ohlcv.bars.length - 1]?.volume ?? 0;
227
+ const portfolioValue = envState?.portfolio_value ?? observation?.portfolio_value ?? 0;
228
+ const alphaPct = portfolioValue && startingCash > 0
229
+ ? ((portfolioValue - startingCash) / startingCash) * 100
230
+ : 0;
231
+
232
+ const consensus = council && council.votes.length > 0
233
+ ? council.votes.reduce((a, v) => a + v.signal, 0) / council.votes.length
234
+ : 0;
235
+
236
+ const submitDisabled = loading || !observation || envState?.done;
237
+
238
+ return (
239
+ <div className="flex-1 flex flex-col gap-4 min-h-0">
240
+ {/* Top Ticker Bar */}
241
+ <div className="h-14 shrink-0 bg-white/5 backdrop-blur-md border border-white/10 rounded-lg flex items-center justify-between px-4">
242
+ <div className="flex items-center gap-6">
243
+ <div className="flex items-baseline gap-2">
244
+ <span className="font-bold text-lg text-white">{observation?.ticker ?? '—'}</span>
245
+ <span className="font-mono text-neutral-400">
246
+ {observation ? `$${observation.price.toFixed(2)}` : '—'}
247
+ </span>
248
+ <span
249
+ className={`font-mono text-xs ${change >= 0 ? 'text-[#00FF41]' : 'text-[#ffb4ab]'}`}
250
+ >
251
+ ({change >= 0 ? '+' : ''}
252
+ {change.toFixed(2)}%)
253
+ </span>
254
+ </div>
255
+ <div className="w-px h-6 bg-white/10" />
256
+ <div className="flex items-center gap-3">
257
+ <span className="text-[10px] uppercase font-bold text-neutral-500 tracking-widest">Vol:</span>
258
+ <span className="font-mono text-sm text-white">
259
+ {lastVolume ? `${(lastVolume / 1_000_000).toFixed(1)}M` : '—'}
260
+ </span>
261
+ </div>
262
+ <div className="w-px h-6 bg-white/10" />
263
+ <div className="flex items-center gap-3">
264
+ <span className="text-[10px] uppercase font-bold text-neutral-500 tracking-widest">Step:</span>
265
+ <span className="font-mono text-sm text-white">
266
+ {envState ? `${envState.current_step}/${envState.total_steps}` : '—'}
267
+ </span>
268
+ </div>
269
+ </div>
270
+ <div className="flex items-center gap-8">
271
+ <div className="flex flex-col items-end">
272
+ <span className="text-[10px] uppercase font-bold text-neutral-500 tracking-widest mb-0.5">Portfolio</span>
273
+ <span className="font-mono font-bold text-white">
274
+ ${portfolioValue.toLocaleString(undefined, { maximumFractionDigits: 2 })}
275
+ </span>
276
+ </div>
277
+ <div className="w-px h-8 bg-white/10" />
278
+ <div className="flex flex-col items-end">
279
+ <span className="text-[10px] uppercase font-bold text-neutral-500 tracking-widest mb-0.5">Alpha</span>
280
+ <span
281
+ className={`font-mono font-bold ${alphaPct >= 0 ? 'text-[#00FF41]' : 'text-[#ffb4ab]'}`}
282
+ >
283
+ {alphaPct >= 0 ? '+' : ''}
284
+ {alphaPct.toFixed(2)}%
285
+ </span>
286
+ </div>
287
+ </div>
288
+ </div>
289
+
290
+ <div className="flex-1 flex gap-4 min-h-0">
291
+ {/* Chart Area */}
292
+ <div className="flex-1 glass-panel rounded-lg flex flex-col relative overflow-hidden group">
293
+ <div className="absolute inset-0 scanline z-0 pointer-events-none" />
294
+ <div className="h-10 border-b border-white/5 flex items-center justify-between px-4 bg-white/5 z-10">
295
+ <div className="flex gap-4 font-mono text-[10px] uppercase tracking-widest">
296
+ <button className="text-[#00FF41] border-b border-[#00FF41] py-3">1D</button>
297
+ <button className="text-neutral-500 hover:text-white py-3">1W</button>
298
+ <button className="text-neutral-500 hover:text-white py-3">1M</button>
299
+ <button className="text-neutral-500 hover:text-white py-3">3M</button>
300
+ </div>
301
+ <div className="flex gap-2">
302
+ <button className="p-1 hover:bg-white/10 rounded transition-colors text-neutral-400">
303
+ <Zap className="w-4 h-4" />
304
+ </button>
305
+ <button className="p-1 hover:bg-white/10 rounded transition-colors text-neutral-400">
306
+ <Monitor className="w-4 h-4" />
307
+ </button>
308
+ </div>
309
+ </div>
310
+ <CandlestickChart bars={ohlcv?.bars ?? []} />
311
+ </div>
312
+
313
+ {/* Council Feed Sidebar */}
314
+ <div className="w-80 glass-panel rounded-lg flex flex-col overflow-hidden shrink-0">
315
+ <div className="h-10 border-b border-white/5 px-4 flex items-center justify-between bg-white/5">
316
+ <span className="text-[10px] font-bold uppercase tracking-widest text-[#e5e2e1]">Council Live Feed</span>
317
+ <div className="w-2 h-2 rounded-full bg-[#00FF41] animate-pulse" />
318
+ </div>
319
+ <div className="flex-1 overflow-y-auto p-3 space-y-1">
320
+ {council?.votes.length ? (
321
+ council.votes.map((v) => {
322
+ const status = statusFromSignal(v.signal);
323
+ return (
324
+ <div
325
+ key={v.name}
326
+ className="p-3 rounded border border-transparent hover:border-white/10 hover:bg-white/5 transition-all group cursor-default"
327
+ >
328
+ <div className="flex justify-between items-center mb-1.5">
329
+ <span className="text-xs font-semibold group-hover:text-[#00FF41] transition-colors">
330
+ {SPECIALIST_DISPLAY[v.name] ?? v.name}
331
+ </span>
332
+ <div
333
+ className={`w-1.5 h-1.5 rounded-full ${
334
+ status === 'green'
335
+ ? 'bg-[#00FF41]'
336
+ : status === 'red'
337
+ ? 'bg-[#ffb4ab]'
338
+ : 'bg-neutral-500'
339
+ }`}
340
+ />
341
+ </div>
342
+ <p className="text-[11px] text-neutral-400 leading-relaxed font-mono">{v.rationale}</p>
343
+ </div>
344
+ );
345
+ })
346
+ ) : (
347
+ <div className="p-3 text-[11px] text-neutral-500 font-mono">
348
+ {observation ? 'Polling council…' : 'Waiting for environment.'}
349
+ </div>
350
+ )}
351
+ </div>
352
+ </div>
353
+ </div>
354
+
355
+ {/* Bottom Panel */}
356
+ <div className="h-32 shrink-0 glass-panel rounded-lg flex overflow-hidden border-[#731fff]/20">
357
+ <div className="w-48 border-r border-white/5 flex flex-col justify-center px-6 bg-[#731fff]/5 relative overflow-hidden">
358
+ <div className="absolute inset-0 bg-gradient-to-br from-[#731fff]/10 to-transparent pointer-events-none" />
359
+ <div className="flex items-center gap-1.5 mb-1">
360
+ <Activity className="w-3 h-3 text-[#731fff]" />
361
+ <span className="text-[9px] uppercase font-bold tracking-widest text-[#731fff]">Moderator</span>
362
+ </div>
363
+ <h3 className="font-bold text-lg text-white">Gemma-4</h3>
364
+ <span className="text-[10px] text-neutral-500 mt-0.5">
365
+ Consensus: {consensus >= 0 ? '+' : ''}
366
+ {consensus.toFixed(2)}
367
+ </span>
368
+ </div>
369
+ <div className="flex-1 p-6 relative flex flex-col justify-center">
370
+ <p className="font-mono text-[13px] text-neutral-300 leading-relaxed max-w-3xl">
371
+ <span className="text-[#731fff] mr-2 opacity-70">&gt;</span>
372
+ {council?.rationale ?? (observation ? 'Awaiting moderator synthesis.' : 'Run a task to see live moderator output.')}
373
+ <span className="inline-block w-2.5 h-4 bg-[#731fff] align-middle ml-1 animate-pulse" />
374
+ </p>
375
+ </div>
376
+ <div className="w-72 border-l border-white/5 flex flex-col items-stretch justify-center bg-[#00FF41]/5 backdrop-blur-sm px-4 gap-2">
377
+ <div className="flex items-center gap-2">
378
+ <select
379
+ value={side}
380
+ onChange={(e) => setSide(e.target.value as Side)}
381
+ className="bg-black/40 border border-white/10 rounded text-[11px] font-mono px-2 py-1 text-white"
382
+ >
383
+ <option value="buy">BUY</option>
384
+ <option value="sell">SELL</option>
385
+ <option value="hold">HOLD</option>
386
+ </select>
387
+ <input
388
+ type="number"
389
+ min={0}
390
+ value={qty}
391
+ onChange={(e) => setQty(Math.max(0, parseInt(e.target.value || '0', 10)))}
392
+ className="w-20 bg-black/40 border border-white/10 rounded text-[11px] font-mono px-2 py-1 text-white"
393
+ />
394
+ </div>
395
+ <button
396
+ onClick={() => void submitTrade(side, qty)}
397
+ disabled={submitDisabled}
398
+ className="bg-[#00FF41] disabled:opacity-40 disabled:cursor-not-allowed text-black font-black text-sm px-6 py-2 rounded-md shadow-[0_0_20px_rgba(0,255,65,0.3)] hover:scale-105 transition-transform"
399
+ >
400
+ {loading ? 'EXECUTING…' : `${side.toUpperCase()} ${side === 'hold' ? '' : qty}`}
401
+ </button>
402
+ </div>
403
+ </div>
404
+ </div>
405
+ );
406
+ };
407
+
408
+ // ----------------------------------------------------------------- CouncilView
409
+
410
+ const SPECIALIST_VISUALS: Record<
411
+ string,
412
+ { icon: any; color: string; render: () => any }
413
+ > = {
414
+ chart_pattern: {
415
+ icon: Zap,
416
+ color: '[#00FF41]',
417
+ render: () => (
418
+ <div className="h-20 w-full bg-black/40 rounded border border-white/5 relative overflow-hidden flex items-end p-2 pb-0">
419
+ <svg className="w-full h-full" viewBox="0 0 100 40" preserveAspectRatio="none">
420
+ <path d="M0,35 Q10,32 20,38 T40,20 T60,25 T100,5" fill="none" stroke="#00FF41" strokeWidth="2" />
421
+ <path d="M0,35 Q10,32 20,38 T40,20 T60,25 T100,5 V40 H0 Z" fill="url(#chart-grad)" />
422
+ <defs>
423
+ <linearGradient id="chart-grad" x1="0" x2="0" y1="0" y2="1">
424
+ <stop offset="0%" stopColor="#00FF41" stopOpacity="0.2" />
425
+ <stop offset="100%" stopColor="#00FF41" stopOpacity="0" />
426
+ </linearGradient>
427
+ </defs>
428
+ </svg>
429
+ </div>
430
+ ),
431
+ },
432
+ indicator: {
433
+ icon: LineChart,
434
+ color: '[#00FF41]',
435
+ render: () => (
436
+ <div className="space-y-2 py-2">
437
+ {[
438
+ { l: 'RSI', v: 62, c: '#00FF41' },
439
+ { l: 'MACD', v: 70, c: '#00FF41' },
440
+ { l: 'ATR', v: 35, c: 'white' },
441
+ ].map((b, i) => (
442
+ <div key={i} className="flex items-center gap-2">
443
+ <span className="text-[8px] font-bold text-neutral-500 w-10">{b.l}</span>
444
+ <div className="flex-1 h-1.5 bg-white/5 rounded-full overflow-hidden">
445
+ <div className="h-full bg-current transition-all" style={{ width: `${b.v}%`, color: b.c }} />
446
+ </div>
447
+ </div>
448
+ ))}
449
+ </div>
450
+ ),
451
+ },
452
+ news: {
453
+ icon: Newspaper,
454
+ color: '[#00FF41]',
455
+ render: () => (
456
+ <div className="space-y-2 max-h-20 overflow-hidden relative">
457
+ <div className="flex gap-2 items-start opacity-90">
458
+ <div className="w-1 h-1 rounded-full bg-[#00FF41] mt-1.5" />
459
+ <span className="text-[10px] text-neutral-400">Latest sentiment-tagged headlines.</span>
460
+ </div>
461
+ <div className="flex gap-2 items-start opacity-70">
462
+ <div className="w-1 h-1 rounded-full bg-[#00FF41] mt-1.5" />
463
+ <span className="text-[10px] text-neutral-400">Tone aggregated by NewsAgent.</span>
464
+ </div>
465
+ <div className="absolute inset-0 bg-gradient-to-b from-transparent to-[#1c1b1b]" />
466
+ </div>
467
+ ),
468
+ },
469
+ forum_sentiment: {
470
+ icon: Users,
471
+ color: 'neutral-400',
472
+ render: () => (
473
+ <div className="h-20 w-full bg-black/40 rounded border border-white/5 relative p-2 overflow-hidden">
474
+ <div className="absolute top-[20%] left-[70%] w-3 h-3 bg-[#00FF41] rounded-full blur-[2px] opacity-60" />
475
+ <div className="absolute top-[40%] left-[60%] w-2 h-2 bg-[#00FF41] rounded-full blur-[1px] opacity-40" />
476
+ <div className="absolute top-[10%] left-[80%] w-4 h-4 bg-[#00FF41] rounded-full blur-[3px] opacity-80" />
477
+ <div className="absolute bottom-2 left-2 right-2 flex justify-between text-[7px] font-bold text-neutral-700 border-t border-white/5 pt-1">
478
+ <span>BEAR</span>
479
+ <span>BULL</span>
480
+ </div>
481
+ </div>
482
+ ),
483
+ },
484
+ peer_commodity: {
485
+ icon: TrendingUp,
486
+ color: '[#00FF41]',
487
+ render: () => (
488
+ <div className="grid grid-cols-2 gap-2 h-20">
489
+ {[
490
+ { l: 'Peer 1', v: '+1.2% ▲' },
491
+ { l: 'Peer 2', v: '+0.4%' },
492
+ { l: 'Peer 3', v: '-0.1%' },
493
+ { l: 'Comm.', v: '+0.7%' },
494
+ ].map((m, i) => (
495
+ <div key={i} className="bg-white/5 rounded p-1.5 flex flex-col justify-center">
496
+ <span className="text-[7px] font-bold text-neutral-600 uppercase">{m.l}</span>
497
+ <span className="text-[10px] font-mono text-neutral-300">{m.v}</span>
498
+ </div>
499
+ ))}
500
+ </div>
501
+ ),
502
+ },
503
+ geopolitics: {
504
+ icon: Globe,
505
+ color: 'neutral-400',
506
+ render: () => (
507
+ <div className="grid grid-cols-2 gap-2 h-20">
508
+ {[
509
+ { l: 'DXY', v: '104.2 ▲' },
510
+ { l: '10Y', v: '4.32% ▲' },
511
+ { l: 'SPX', v: '-0.2%' },
512
+ { l: 'VIX', v: '14.1' },
513
+ ].map((m, i) => (
514
+ <div key={i} className="bg-white/5 rounded p-1.5 flex flex-col justify-center">
515
+ <span className="text-[7px] font-bold text-neutral-600 uppercase">{m.l}</span>
516
+ <span className="text-[10px] font-mono text-neutral-300">{m.v}</span>
517
+ </div>
518
+ ))}
519
+ </div>
520
+ ),
521
+ },
522
+ seasonal_trend: {
523
+ icon: Calendar,
524
+ color: '[#00FF41]',
525
+ render: () => (
526
+ <div className="h-20 w-full bg-black/40 rounded border border-white/5 relative p-2 flex items-end gap-1">
527
+ {[30, 45, 35, 60, 55, 70, 50, 65, 75, 60, 80, 72].map((h, i) => (
528
+ <div key={i} className="flex-1 bg-[#00FF41]/40 rounded-t-sm" style={{ height: `${h}%` }} />
529
+ ))}
530
+ <div className="absolute top-1 right-2 text-[8px] font-bold text-neutral-600 uppercase tracking-widest">12mo avg</div>
531
+ </div>
532
+ ),
533
+ },
534
+ };
535
+
536
+ const CouncilView = ({ env }: { env: StockerEnv }) => {
537
+ const { observation, council } = env;
538
+ const consensus = council && council.votes.length > 0
539
+ ? council.votes.reduce((a, v) => a + v.signal, 0) / council.votes.length
540
+ : 0;
541
+ const consensusLabel =
542
+ consensus > 0.2 ? 'BULLISH' : consensus < -0.2 ? 'BEARISH' : 'NEUTRAL';
543
+ const consensusColor =
544
+ consensus > 0.2 ? 'text-[#00FF41]' : consensus < -0.2 ? 'text-[#ffb4ab]' : 'text-neutral-400';
545
+
546
+ return (
547
+ <div className="flex-1 flex flex-col gap-6">
548
+ <header>
549
+ <h1 className="text-3xl font-bold tracking-tighter text-white mb-2">Council Deep-Dive</h1>
550
+ <p className="text-sm text-neutral-400 max-w-2xl">
551
+ Real-time analysis and consensus generation across 7 autonomous specialized agents.
552
+ Target asset:{' '}
553
+ <span className="text-[#00FF41] font-mono bg-[#00FF41]/10 px-1.5 py-0.5 rounded border border-[#00FF41]/20">
554
+ {observation?.ticker ?? '—'}
555
+ </span>
556
+ </p>
557
+ </header>
558
+
559
+ <div className="grid grid-cols-1 xl:grid-cols-3 gap-4 flex-1">
560
+ {/* Alpha Moderator */}
561
+ <div className="col-span-1 glass-panel rounded-xl flex flex-col relative overflow-hidden group border-[#731fff]/30 shadow-[0_0_30px_rgba(115,31,255,0.1)]">
562
+ <div className="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-[#731fff] to-transparent opacity-50" />
563
+ <div className="p-4 border-b border-white/5 flex justify-between items-start z-10">
564
+ <div className="flex items-center gap-2">
565
+ <Brain className="w-5 h-5 text-[#731fff]" />
566
+ <h3 className="text-lg font-bold text-white">Alpha Moderator</h3>
567
+ </div>
568
+ <span className="text-[10px] font-bold text-[#731fff] border border-[#731fff]/30 px-2 py-1 rounded bg-[#731fff]/10">
569
+ CONSENSUS
570
+ </span>
571
+ </div>
572
+
573
+ <div className="p-6 flex-1 flex flex-col justify-between z-10">
574
+ <div className="flex flex-col items-center py-6">
575
+ <div className="relative w-40 h-20 overflow-hidden flex items-end justify-center mb-2">
576
+ <div className="absolute top-0 w-40 h-40 rounded-full border-[12px] border-white/5 border-b-transparent border-r-transparent rotate-45" />
577
+ <div
578
+ className={`absolute top-0 w-40 h-40 rounded-full border-[12px] ${
579
+ consensus > 0.2
580
+ ? 'border-[#00FF41]'
581
+ : consensus < -0.2
582
+ ? 'border-[#ffb4ab]'
583
+ : 'border-neutral-400'
584
+ } border-b-transparent border-r-transparent opacity-80`}
585
+ style={{
586
+ transform: `rotate(${45 + Math.max(-1, Math.min(1, consensus)) * 135}deg)`,
587
+ clipPath: 'polygon(50% 50%, 0% 0%, 100% 0%)',
588
+ }}
589
+ />
590
+ <div className="flex flex-col items-center translate-y-2">
591
+ <span className="font-mono text-3xl text-white font-bold">
592
+ {consensus >= 0 ? '+' : ''}
593
+ {consensus.toFixed(2)}
594
+ </span>
595
+ <span className={`text-[10px] font-bold tracking-widest mt-1 uppercase ${consensusColor}`}>
596
+ {consensusLabel}
597
+ </span>
598
+ </div>
599
+ </div>
600
+ <div className="w-full flex justify-between px-4 font-mono text-[9px] text-neutral-500">
601
+ <span>-1.0 (BEAR)</span>
602
+ <span>0.0</span>
603
+ <span>+1.0 (BULL)</span>
604
+ </div>
605
+ </div>
606
+
607
+ <div className="space-y-2">
608
+ <span className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest">Moderator Synthesis</span>
609
+ <p className="text-sm text-neutral-400 leading-relaxed">
610
+ {council?.rationale ??
611
+ 'Run a task in Gallery to populate moderator synthesis from the live council.'}
612
+ </p>
613
+ {council && (
614
+ <p className="text-[10px] text-[#731fff] font-mono mt-2">
615
+ Action: {council.action.side.toUpperCase()}
616
+ {council.action.side !== 'hold' ? ` ${council.action.quantity}` : ''}
617
+ </p>
618
+ )}
619
+ </div>
620
+ </div>
621
+ <div className="absolute -bottom-20 -right-20 w-64 h-64 bg-[#731fff]/10 blur-[100px] rounded-full pointer-events-none" />
622
+ </div>
623
+
624
+ {/* Specialist cards */}
625
+ {Object.entries(SPECIALIST_VISUALS).map(([roleName, visual]) => {
626
+ const vote = council?.votes.find((v) => v.name === roleName);
627
+ const stat = vote ? `${vote.signal >= 0 ? '+' : ''}${vote.signal.toFixed(2)}` : '—';
628
+ const Icon = visual.icon;
629
+ return (
630
+ <AgentCard
631
+ key={roleName}
632
+ title={SPECIALIST_DISPLAY[roleName] ?? roleName}
633
+ icon={Icon}
634
+ stat={stat}
635
+ color={visual.color}
636
+ >
637
+ {visual.render()}
638
+ <p className="text-xs text-neutral-500 leading-snug">
639
+ {vote?.rationale ?? 'Awaiting council vote — run a task in Gallery.'}
640
+ </p>
641
+ </AgentCard>
642
+ );
643
+ })}
644
+ </div>
645
+ </div>
646
+ );
647
+ };
648
+
649
+ // ---------------------------------------------------------------- TrainingView
650
+
651
+ const MetricCard = ({
652
+ label,
653
+ value,
654
+ trend,
655
+ }: {
656
+ label: string;
657
+ value: string;
658
+ trend?: string;
659
+ }) => (
660
+ <div className={`glass-panel p-4 rounded-lg flex flex-col justify-center ${trend ? 'border-l-2 border-[#00FF41]/30' : ''}`}>
661
+ <span className={`text-[10px] font-bold uppercase tracking-widest mb-1 ${trend ? 'text-[#00FF41]' : 'text-neutral-500'}`}>{label}</span>
662
+ <span className={`text-xl font-bold flex items-center ${trend ? 'text-[#00FF41]' : 'text-white'}`}>
663
+ {value}
664
+ {trend && (
665
+ <span className="text-xs ml-1 flex items-center">
666
+ {trend} <ChevronUp className="w-3 h-3 ml-0.5" />
667
+ </span>
668
+ )}
669
+ </span>
670
+ </div>
671
+ );
672
+
673
+ const TrainingView = () => {
674
+ const [metrics, setMetrics] = useState<TrainingMetrics | null>(null);
675
+
676
+ useEffect(() => {
677
+ let cancel = false;
678
+ api
679
+ .trainingMetrics()
680
+ .then((m) => {
681
+ if (!cancel) setMetrics(m);
682
+ })
683
+ .catch(() => {
684
+ if (!cancel) setMetrics({ status: 'no_runs', summary: [], mean_alpha_pct: 0 });
685
+ });
686
+ return () => {
687
+ cancel = true;
688
+ };
689
+ }, []);
690
+
691
+ // Placeholder logs — there's no JSON log on disk, kept as decorative.
692
+ const logs = [
693
+ { type: 'SYS', msg: 'Initializing GRPO policy trainer…' },
694
+ { type: 'SYS', msg: 'Loading historical state tensors [BATCH_SIZE=256]…' },
695
+ { type: 'OPT', msg: 'Step 4480: Adv = 0.124, Kl_div = 0.003', color: 'text-purple-400' },
696
+ { type: 'REWARD', msg: 'Epoch 44 evaluation: Mean R = +1.24%', color: 'text-[#00FF41]' },
697
+ { type: 'SYS', msg: 'Checkpoint saved.', pulse: true },
698
+ ];
699
+
700
+ const best = metrics?.summary?.length
701
+ ? metrics.summary.reduce((a, b) => (b.alpha_pct > a.alpha_pct ? b : a))
702
+ : null;
703
+ const worst = metrics?.summary?.length
704
+ ? metrics.summary.reduce((a, b) => (b.alpha_pct < a.alpha_pct ? b : a))
705
+ : null;
706
+
707
+ return (
708
+ <div className="flex-1 flex flex-col gap-4 overflow-hidden">
709
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4 shrink-0">
710
+ <MetricCard label="Run" value={metrics?.run_name ?? '—'} />
711
+ <MetricCard
712
+ label="Mean Alpha %"
713
+ value={metrics ? `${metrics.mean_alpha_pct.toFixed(2)}%` : '—'}
714
+ trend={metrics && metrics.mean_alpha_pct > 0 ? 'up' : undefined}
715
+ />
716
+ <MetricCard
717
+ label="Best Task"
718
+ value={best ? `${best.task_id} (${best.alpha_pct.toFixed(1)}%)` : '—'}
719
+ />
720
+ <MetricCard
721
+ label="Worst Task"
722
+ value={worst ? `${worst.task_id} (${worst.alpha_pct.toFixed(1)}%)` : '—'}
723
+ />
724
+ </div>
725
+
726
+ <div className="flex-1 flex flex-col md:flex-row gap-4 overflow-hidden min-h-0">
727
+ {/* Left: status + logs */}
728
+ <div className="w-full md:w-1/3 flex flex-col gap-4 h-full min-h-0">
729
+ <div className="glass-panel rounded-lg p-6 shrink-0 relative overflow-hidden group">
730
+ <div className="absolute inset-0 bg-gradient-to-br from-[#731fff]/10 to-transparent pointer-events-none" />
731
+ <div className="flex items-center justify-between border-b border-white/5 pb-3 mb-4">
732
+ <h3 className="text-lg font-bold text-white">GRPO Status</h3>
733
+ <span
734
+ className={`px-2 py-1 text-[10px] font-bold rounded border ${
735
+ metrics?.status === 'completed'
736
+ ? 'bg-[#00FF41]/20 text-[#00FF41] border-[#00FF41]/40'
737
+ : 'bg-neutral-500/20 text-neutral-400 border-neutral-500/40'
738
+ }`}
739
+ >
740
+ {metrics?.status === 'completed' ? 'EVAL DONE' : 'NO RUNS'}
741
+ </span>
742
+ </div>
743
+ <div className="space-y-4">
744
+ <div className="flex justify-between text-xs">
745
+ <span className="text-neutral-500">Tasks evaluated</span>
746
+ <span className="text-white font-mono">{metrics?.summary.length ?? 0}</span>
747
+ </div>
748
+ <div className="flex justify-between text-xs">
749
+ <span className="text-neutral-500">LoRA Adapters</span>
750
+ <span className="text-[#731fff] font-mono">Base (no LoRA loaded)</span>
751
+ </div>
752
+ <div className="flex justify-between text-xs">
753
+ <span className="text-neutral-500">Run directory</span>
754
+ <span className="text-white font-mono truncate">{metrics?.run_name ?? '—'}</span>
755
+ </div>
756
+ </div>
757
+ </div>
758
+
759
+ <div className="glass-panel rounded-lg flex-1 flex flex-col overflow-hidden border-[#00FF41]/20">
760
+ <div className="border-b border-white/5 px-4 py-2 bg-white/5 flex justify-between items-center text-[10px] font-bold uppercase tracking-widest text-neutral-500">
761
+ <span>Terminal Logs (decorative)</span>
762
+ <div className="w-2 h-2 rounded-full bg-[#00FF41] animate-pulse" />
763
+ </div>
764
+ <div className="flex-1 p-4 overflow-y-auto space-y-1 font-mono text-[11px] text-neutral-400">
765
+ {logs.map((log, i) => (
766
+ <div key={i} className="flex gap-2">
767
+ <span className={`${log.type === 'SYS' ? 'text-blue-400' : log.color || 'text-neutral-500'}`}>
768
+ [{log.type}]
769
+ </span>
770
+ <span className={log.pulse ? 'animate-pulse' : ''}>
771
+ {log.msg}
772
+ {log.pulse ? ' _' : ''}
773
+ </span>
774
+ </div>
775
+ ))}
776
+ </div>
777
+ </div>
778
+ </div>
779
+
780
+ {/* Right: real curves from training/runs */}
781
+ <div className="w-full md:w-2/3 flex flex-col gap-4 h-full overflow-y-auto pr-1">
782
+ <AgentCard title="Reward Curve" icon={TrendingUp} color="[#00FF41]" active>
783
+ <div className="flex-1 h-full min-h-[220px] relative mt-2 flex items-center justify-center bg-black/30 rounded">
784
+ {metrics?.reward_curve_png ? (
785
+ <img
786
+ src={metrics.reward_curve_png}
787
+ alt="Reward curve"
788
+ className="max-h-[260px] max-w-full object-contain"
789
+ />
790
+ ) : (
791
+ <span className="text-[11px] text-neutral-500 font-mono">
792
+ No run yet — run <code>python training/eval_rollout.py --mock</code>.
793
+ </span>
794
+ )}
795
+ </div>
796
+ </AgentCard>
797
+
798
+ <AgentCard title="Portfolio Curve" icon={BarChart3} color="[#731fff]">
799
+ <div className="flex-1 h-full min-h-[220px] relative mt-2 flex items-center justify-center bg-black/30 rounded">
800
+ {metrics?.portfolio_curve_png ? (
801
+ <img
802
+ src={metrics.portfolio_curve_png}
803
+ alt="Portfolio curve"
804
+ className="max-h-[260px] max-w-full object-contain"
805
+ />
806
+ ) : (
807
+ <span className="text-[11px] text-neutral-500 font-mono">No run yet.</span>
808
+ )}
809
+ </div>
810
+ </AgentCard>
811
+
812
+ {metrics?.summary?.length ? (
813
+ <div className="glass-panel rounded-lg p-4">
814
+ <h4 className="text-xs font-bold uppercase tracking-widest text-neutral-500 mb-3">Per-task summary</h4>
815
+ <table className="w-full text-[11px] font-mono">
816
+ <thead className="text-neutral-500 text-left">
817
+ <tr>
818
+ <th className="py-1">Task</th>
819
+ <th className="py-1 text-right">Alpha %</th>
820
+ <th className="py-1 text-right">Final</th>
821
+ <th className="py-1 text-right">Buy &amp; Hold</th>
822
+ </tr>
823
+ </thead>
824
+ <tbody>
825
+ {metrics.summary.map((row) => (
826
+ <tr key={row.task_id} className="border-t border-white/5">
827
+ <td className="py-1.5 text-white">{row.task_id}</td>
828
+ <td
829
+ className={`py-1.5 text-right ${
830
+ row.alpha_pct >= 0 ? 'text-[#00FF41]' : 'text-[#ffb4ab]'
831
+ }`}
832
+ >
833
+ {row.alpha_pct.toFixed(2)}%
834
+ </td>
835
+ <td className="py-1.5 text-right text-neutral-300">
836
+ ${row.final_portfolio.toLocaleString()}
837
+ </td>
838
+ <td className="py-1.5 text-right text-neutral-400">
839
+ ${row.buy_and_hold.toLocaleString()}
840
+ </td>
841
+ </tr>
842
+ ))}
843
+ </tbody>
844
+ </table>
845
+ </div>
846
+ ) : null}
847
+ </div>
848
+ </div>
849
+ </div>
850
+ );
851
+ };
852
+
853
+ // ---------------------------------------------------------------- GalleryView
854
+
855
+ const TASK_META: Record<string, { difficulty: string; name: string; color: string }> = {
856
+ task_easy: { difficulty: 'Easy', name: 'Steady Regime', color: '[#00FF41]' },
857
+ task_medium: { difficulty: 'Medium', name: 'Choppy Sideways', color: '[#731fff]' },
858
+ task_hard: { difficulty: 'Hard', name: 'Drawdown / Snapback', color: '[#ffb4ab]' },
859
+ };
860
+
861
+ const TaskCard = ({
862
+ taskId,
863
+ bars,
864
+ onDeploy,
865
+ active,
866
+ }: {
867
+ taskId: string;
868
+ bars: OhlcvBar[] | null;
869
+ onDeploy: () => void;
870
+ active: boolean;
871
+ }) => {
872
+ const meta = TASK_META[taskId] ?? { difficulty: '?', name: taskId, color: '[#00FF41]' };
873
+ const ticker = bars?.[0] ? '' : '';
874
+ const inEpisode = bars?.filter((b) => b.in_episode) ?? [];
875
+ const dates =
876
+ inEpisode.length > 0
877
+ ? `${inEpisode[0].time} → ${inEpisode[inEpisode.length - 1].time}`
878
+ : 'Loading…';
879
+
880
+ // Compute a 9-bucket sparkline from in-episode closes
881
+ let sparkBars: number[] = [];
882
+ if (inEpisode.length > 0) {
883
+ const closes = inEpisode.map((b) => b.close);
884
+ const min = Math.min(...closes);
885
+ const max = Math.max(...closes);
886
+ const range = max - min || 1;
887
+ const buckets = 9;
888
+ const step = closes.length / buckets;
889
+ sparkBars = Array.from({ length: buckets }, (_, i) => {
890
+ const slice = closes.slice(Math.floor(i * step), Math.floor((i + 1) * step) || (i + 1));
891
+ const avg = slice.reduce((a, c) => a + c, 0) / Math.max(1, slice.length);
892
+ return ((avg - min) / range) * 100;
893
+ });
894
+ }
895
+
896
+ return (
897
+ <div
898
+ className={`group glass-panel rounded-xl p-6 flex flex-col relative overflow-hidden transition-all hover:bg-white/[0.08] hover:shadow-[0_0_30px_rgba(0,255,65,0.05)] ${
899
+ active ? `border-${meta.color}/50` : 'border-white/5'
900
+ } hover:border-${meta.color}/50`}
901
+ >
902
+ <div
903
+ className={`absolute inset-0 bg-gradient-to-br from-${meta.color}/5 to-transparent pointer-events-none group-hover:from-${meta.color}/15 transition-all`}
904
+ />
905
+
906
+ <div className="flex justify-between items-start mb-4 relative z-10">
907
+ <div
908
+ className={`px-2 py-1 bg-${meta.color}/10 border border-${meta.color}/30 text-${meta.color} text-[10px] font-bold uppercase tracking-widest rounded flex items-center gap-1.5`}
909
+ >
910
+ <div className={`w-1.5 h-1.5 rounded-full bg-${meta.color}`} />
911
+ {meta.difficulty}
912
+ </div>
913
+ <span className="text-lg font-bold text-white font-mono">{bars?.[0] ? bars[0].time.slice(0, 4) : ''}</span>
914
+ </div>
915
+
916
+ <div className="mb-6 relative z-10">
917
+ <h3 className="text-xl font-bold text-white mb-1">{meta.name}</h3>
918
+ <div className="flex items-center gap-2 text-[10px] text-neutral-500 font-mono">
919
+ <Monitor className="w-3 h-3" />
920
+ {dates}
921
+ </div>
922
+ </div>
923
+
924
+ <div className="h-24 w-full bg-black/40 rounded-lg border border-white/5 mb-8 flex items-end px-2 pb-2 gap-1 overflow-hidden relative z-10">
925
+ <div className="absolute inset-x-0 bottom-0 h-[1px] bg-white/5" />
926
+ {sparkBars.length > 0 ? (
927
+ sparkBars.map((h, i) => (
928
+ <motion.div
929
+ key={i}
930
+ initial={{ height: 0 }}
931
+ animate={{ height: `${h}%` }}
932
+ className={`flex-1 rounded-t-[2px] transition-all bg-${meta.color}/40 group-hover:bg-${meta.color}/70`}
933
+ />
934
+ ))
935
+ ) : (
936
+ <div className="w-full text-center text-[10px] text-neutral-600 font-mono">Loading bars…</div>
937
+ )}
938
+ </div>
939
+
940
+ <button
941
+ onClick={onDeploy}
942
+ className={`w-full py-3 rounded-lg border border-white/10 flex items-center justify-center gap-2 text-xs font-bold uppercase tracking-widest transition-all relative z-10 hover:text-${meta.color} hover:border-${meta.color}/50 hover:bg-${meta.color}/5 group/btn`}
943
+ >
944
+ <Zap className="w-4 h-4 group-hover/btn:fill-current" />
945
+ {active ? 'Active' : 'Deploy Agent'}
946
+ </button>
947
+ </div>
948
+ );
949
+ };
950
+
951
+ const GalleryView = ({
952
+ env,
953
+ onDeploy,
954
+ }: {
955
+ env: StockerEnv;
956
+ onDeploy: (taskId: string) => void;
957
+ }) => {
958
+ const [bars, setBars] = useState<Record<string, OhlcvResponse>>({});
959
+
960
+ useEffect(() => {
961
+ let cancel = false;
962
+ Promise.all(env.tasks.map((t) => api.ohlcv(t).then((r) => [t, r] as const))).then((pairs) => {
963
+ if (cancel) return;
964
+ const next: Record<string, OhlcvResponse> = {};
965
+ for (const [t, r] of pairs) next[t] = r;
966
+ setBars(next);
967
+ });
968
+ return () => {
969
+ cancel = true;
970
+ };
971
+ }, [env.tasks]);
972
+
973
+ return (
974
+ <div className="flex-1 flex flex-col gap-8">
975
+ <header className="space-y-3">
976
+ <div className="flex items-center gap-2 text-[#00FF41]">
977
+ <Brain className="w-5 h-5" />
978
+ <span className="text-[10px] uppercase font-bold tracking-[0.2em]">Agent Environment</span>
979
+ </div>
980
+ <h1 className="text-5xl font-black text-white tracking-tighter">Task Gallery</h1>
981
+ <p className="text-sm text-neutral-400 max-w-2xl leading-relaxed">
982
+ Select a historical market regime to deploy and evaluate your AI trading agent's performance in isolated, controlled conditions.
983
+ </p>
984
+ </header>
985
+
986
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
987
+ {env.tasks.map((t) => (
988
+ <TaskCard
989
+ key={t}
990
+ taskId={t}
991
+ bars={bars[t]?.bars ?? null}
992
+ active={env.taskId === t}
993
+ onDeploy={() => onDeploy(t)}
994
+ />
995
+ ))}
996
+ </div>
997
+ </div>
998
+ );
999
+ };
1000
+
1001
+ // --------------------------------------------------------------- PortfolioView
1002
+
1003
+ const PortfolioView = ({ env }: { env: StockerEnv }) => {
1004
+ const { observation, envState, startingCash } = env;
1005
+
1006
+ if (!observation || !envState) {
1007
+ return (
1008
+ <div className="flex-1 flex items-center justify-center">
1009
+ <p className="text-sm text-neutral-500">Run a task in Gallery to populate your portfolio.</p>
1010
+ </div>
1011
+ );
1012
+ }
1013
+
1014
+ const ticker = observation.ticker;
1015
+ const price = observation.price;
1016
+ const balance = observation.position;
1017
+ const value = balance * price;
1018
+ const portfolio = envState.portfolio_value;
1019
+ const cash = envState.cash;
1020
+ const pnl = portfolio - startingCash;
1021
+ const pnlPct = startingCash > 0 ? (pnl / startingCash) * 100 : 0;
1022
+
1023
+ const equityShare = portfolio > 0 ? (value / portfolio) * 100 : 0;
1024
+ const cashShare = portfolio > 0 ? (cash / portfolio) * 100 : 0;
1025
+ const reservedShare = Math.max(0, 100 - equityShare - cashShare);
1026
+
1027
+ // 24h change from the last two prices in price_history
1028
+ const ph = observation.price_history;
1029
+ const change24h =
1030
+ ph.length >= 2 ? ((ph[ph.length - 1] - ph[ph.length - 2]) / ph[ph.length - 2]) * 100 : 0;
1031
+
1032
+ // Sparkline = last 7 closes
1033
+ const spark = ph.slice(-7);
1034
+ const sMin = Math.min(...spark);
1035
+ const sMax = Math.max(...spark);
1036
+ const sRange = sMax - sMin || 1;
1037
+ const sparkPct = spark.map((p) => ((p - sMin) / sRange) * 100);
1038
+
1039
+ // Risk profile = action diversity
1040
+ const actions = envState.action_history;
1041
+ const totalActions = actions.length || 1;
1042
+ const tradeCount = actions.filter((a) => a.side !== 'hold').length;
1043
+ const riskScore = Math.min(100, Math.round((tradeCount / totalActions) * 100));
1044
+ const riskLabel =
1045
+ riskScore < 30 ? 'Conservative' : riskScore < 60 ? 'Moderate' : 'Aggressive';
1046
+
1047
+ return (
1048
+ <div className="flex-1 flex flex-col gap-6">
1049
+ <header className="flex justify-between items-end">
1050
+ <div className="space-y-1">
1051
+ <span className="text-[10px] font-bold text-[#00FF41] uppercase tracking-widest">Active Position</span>
1052
+ <h1 className="text-4xl font-bold text-white tracking-tight">Holdings &amp; P&amp;L</h1>
1053
+ </div>
1054
+ <div className="flex gap-4">
1055
+ <div className="glass-panel p-3 px-5 rounded-lg border-l-2 border-[#00FF41]">
1056
+ <span className="text-[9px] text-neutral-500 uppercase font-bold block mb-1">Total Balance</span>
1057
+ <span className="text-xl font-bold text-white font-mono tracking-tight">
1058
+ ${portfolio.toLocaleString(undefined, { maximumFractionDigits: 2 })}
1059
+ </span>
1060
+ </div>
1061
+ <div
1062
+ className={`glass-panel p-3 px-5 rounded-lg border-l-2 ${
1063
+ pnl >= 0 ? 'border-[#00FF41]' : 'border-[#ffb4ab]'
1064
+ }`}
1065
+ >
1066
+ <span className="text-[9px] text-neutral-500 uppercase font-bold block mb-1">Total P&amp;L</span>
1067
+ <span
1068
+ className={`text-xl font-bold font-mono tracking-tight ${
1069
+ pnl >= 0 ? 'text-[#00FF41]' : 'text-[#ffb4ab]'
1070
+ }`}
1071
+ >
1072
+ {pnl >= 0 ? '+' : ''}${pnl.toFixed(2)} ({pnlPct >= 0 ? '+' : ''}{pnlPct.toFixed(2)}%)
1073
+ </span>
1074
+ </div>
1075
+ </div>
1076
+ </header>
1077
+
1078
+ <div className="glass-panel rounded-xl overflow-hidden border-white/5">
1079
+ <table className="w-full text-left border-collapse">
1080
+ <thead>
1081
+ <tr className="bg-white/5 border-b border-white/10">
1082
+ <th className="px-6 py-4 text-[10px] font-bold text-neutral-500 uppercase tracking-widest">Asset</th>
1083
+ <th className="px-6 py-4 text-[10px] font-bold text-neutral-500 uppercase tracking-widest text-right">Price</th>
1084
+ <th className="px-6 py-4 text-[10px] font-bold text-neutral-500 uppercase tracking-widest text-right">24h Change</th>
1085
+ <th className="px-6 py-4 text-[10px] font-bold text-neutral-500 uppercase tracking-widest text-right">Balance</th>
1086
+ <th className="px-6 py-4 text-[10px] font-bold text-neutral-500 uppercase tracking-widest text-right">Value</th>
1087
+ <th className="px-6 py-4 text-[10px] font-bold text-neutral-500 uppercase tracking-widest text-center">7D Performance</th>
1088
+ </tr>
1089
+ </thead>
1090
+ <tbody className="divide-y divide-white/5 font-mono text-sm">
1091
+ <tr className="hover:bg-white/[0.04] transition-colors group">
1092
+ <td className="px-6 py-5">
1093
+ <div className="flex items-center gap-3">
1094
+ <div className="w-8 h-8 rounded bg-white/5 border border-white/10 flex items-center justify-center font-black group-hover:text-[#00FF41] transition-colors">
1095
+ {ticker[0]}
1096
+ </div>
1097
+ <div className="flex flex-col">
1098
+ <span className="text-white font-bold">{ticker}</span>
1099
+ <span className="text-[10px] text-neutral-500 font-sans tracking-tight">NASDAQ</span>
1100
+ </div>
1101
+ </div>
1102
+ </td>
1103
+ <td className="px-6 py-5 text-right font-bold text-neutral-300">${price.toFixed(2)}</td>
1104
+ <td
1105
+ className={`px-6 py-5 text-right font-bold ${
1106
+ change24h >= 0 ? 'text-[#00FF41]' : 'text-[#ffb4ab]'
1107
+ }`}
1108
+ >
1109
+ {change24h >= 0 ? '+' : ''}
1110
+ {change24h.toFixed(2)}%
1111
+ </td>
1112
+ <td className="px-6 py-5 text-right text-neutral-400">{balance} {ticker}</td>
1113
+ <td className="px-6 py-5 text-right text-white font-bold">
1114
+ ${value.toLocaleString(undefined, { maximumFractionDigits: 2 })}
1115
+ </td>
1116
+ <td className="px-6 py-5">
1117
+ <div className="h-6 w-24 mx-auto flex items-end gap-1 px-1">
1118
+ {sparkPct.map((v, j) => (
1119
+ <div
1120
+ key={j}
1121
+ className="flex-1 rounded-t-[1px] opacity-60"
1122
+ style={{
1123
+ height: `${v}%`,
1124
+ backgroundColor: change24h >= 0 ? '#00FF41' : '#ffb4ab',
1125
+ }}
1126
+ />
1127
+ ))}
1128
+ </div>
1129
+ </td>
1130
+ </tr>
1131
+ </tbody>
1132
+ </table>
1133
+ </div>
1134
+
1135
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
1136
+ <div className="glass-panel rounded-xl p-6 relative overflow-hidden group">
1137
+ <div className="absolute top-0 right-0 p-4 opacity-10">
1138
+ <BarChart3 className="w-12 h-12" />
1139
+ </div>
1140
+ <h4 className="text-xs font-bold text-neutral-500 uppercase tracking-widest mb-4">Risk Profile</h4>
1141
+ <div className="flex items-center gap-4">
1142
+ <div
1143
+ className="w-16 h-16 rounded-full border-4 border-white/5 border-t-[#00FF41] relative"
1144
+ style={{ transform: `rotate(${(riskScore / 100) * 360 - 90}deg)` }}
1145
+ >
1146
+ <div
1147
+ className="absolute inset-0 flex items-center justify-center"
1148
+ style={{ transform: `rotate(${-((riskScore / 100) * 360 - 90)}deg)` }}
1149
+ >
1150
+ <span className="text-sm font-bold text-white">{riskScore}/100</span>
1151
+ </div>
1152
+ </div>
1153
+ <div>
1154
+ <p className="text-sm font-bold text-white mb-1 tracking-tight">{riskLabel}</p>
1155
+ <p className="text-[10px] text-neutral-400 leading-snug">
1156
+ {tradeCount} of {totalActions} steps were trades.
1157
+ </p>
1158
+ </div>
1159
+ </div>
1160
+ </div>
1161
+
1162
+ <div className="glass-panel rounded-xl p-6 col-span-2 relative overflow-hidden group">
1163
+ <h4 className="text-xs font-bold text-neutral-500 uppercase tracking-widest mb-4">Trade Allocation</h4>
1164
+ <div className="flex items-end h-16 gap-1.5 px-1 py-1">
1165
+ {[
1166
+ { l: 'Equities', v: equityShare, c: '#00FF41' },
1167
+ { l: 'Cash', v: cashShare, c: '#731fff' },
1168
+ { l: 'Reserved', v: reservedShare, c: 'rgba(255,255,255,0.1)' },
1169
+ ].map((a, i) => (
1170
+ <div key={i} className="flex-1 flex flex-col items-center">
1171
+ <div
1172
+ className="w-full bg-current opacity-30 rounded-t-sm"
1173
+ style={{ height: `${a.v}%`, color: a.c }}
1174
+ />
1175
+ <span className="text-[8px] font-bold text-neutral-600 mt-1 uppercase tracking-tighter">
1176
+ {a.l} ({a.v.toFixed(0)}%)
1177
+ </span>
1178
+ </div>
1179
+ ))}
1180
+ </div>
1181
+ </div>
1182
+ </div>
1183
+ </div>
1184
+ );
1185
+ };
1186
+
1187
+ // ----------------------------------------------------------- IntelligenceView
1188
+
1189
+ const SENTIMENT_SCORE: Record<string, number> = {
1190
+ positive: 1,
1191
+ bullish: 1,
1192
+ neutral: 0,
1193
+ negative: -1,
1194
+ bearish: -1,
1195
+ };
1196
+
1197
+ const IntelligenceView = ({ env }: { env: StockerEnv }) => {
1198
+ const { observation } = env;
1199
+
1200
+ const positiveHeadlines = (observation?.headlines ?? [])
1201
+ .filter((h) => (SENTIMENT_SCORE[h.sentiment_label?.toLowerCase()] ?? 0) > 0);
1202
+ const negativeHeadlines = (observation?.headlines ?? [])
1203
+ .filter((h) => (SENTIMENT_SCORE[h.sentiment_label?.toLowerCase()] ?? 0) < 0);
1204
+
1205
+ const peerEntries = observation?.peers?.peers ?? [];
1206
+ const macroEntries = observation?.macro ?? [];
1207
+
1208
+ const signals = [
1209
+ positiveHeadlines[0] && {
1210
+ t: 'Top Headline',
1211
+ msg: positiveHeadlines[0].headline,
1212
+ time: positiveHeadlines[0].date,
1213
+ color: '#00FF41',
1214
+ },
1215
+ positiveHeadlines[1] && {
1216
+ t: 'Secondary',
1217
+ msg: positiveHeadlines[1].headline,
1218
+ time: positiveHeadlines[1].date,
1219
+ color: '#00FF41',
1220
+ },
1221
+ negativeHeadlines[0] && {
1222
+ t: 'Risk Note',
1223
+ msg: negativeHeadlines[0].headline,
1224
+ time: negativeHeadlines[0].date,
1225
+ color: '#ffb4ab',
1226
+ },
1227
+ peerEntries.length > 0 && {
1228
+ t: 'Peer Watch',
1229
+ msg: `${observation?.ticker} vs ${peerEntries
1230
+ .slice(0, 3)
1231
+ .map((p) => `${p.peer_ticker}=${p.peer_close?.toFixed(2) ?? '?'}`)
1232
+ .join(', ')}`,
1233
+ time: observation?.date ?? '',
1234
+ color: '#731fff',
1235
+ },
1236
+ macroEntries[0] && {
1237
+ t: 'Macro',
1238
+ msg: `${macroEntries[0].country} — ${macroEntries[0].headline}`,
1239
+ time: macroEntries[0].date,
1240
+ color: '#731fff',
1241
+ },
1242
+ ].filter(Boolean) as Array<{ t: string; msg: string; time: string; color: string }>;
1243
+
1244
+ // Sentiment hub: mean of headline sentiment scores
1245
+ const headlines = observation?.headlines ?? [];
1246
+ const meanTone = headlines.length
1247
+ ? headlines.reduce((a, h) => a + (SENTIMENT_SCORE[h.sentiment_label?.toLowerCase()] ?? 0), 0) /
1248
+ headlines.length
1249
+ : 0;
1250
+ const toneScale = Math.round(((meanTone + 1) / 2) * 100); // 0..100
1251
+ const sentimentLabel =
1252
+ meanTone > 0.2 ? 'BULL' : meanTone < -0.2 ? 'BEAR' : 'NEUTRAL';
1253
+ const peerBeta = peerEntries.length ? '~1.00' : '—';
1254
+
1255
+ return (
1256
+ <div className="flex-1 flex flex-col gap-6">
1257
+ <header className="space-y-1">
1258
+ <h1 className="text-4xl font-bold text-white tracking-tight flex items-center gap-3">
1259
+ Intelligence Nexus
1260
+ <div className="flex gap-1">
1261
+ {[...Array(3)].map((_, i) => (
1262
+ <div
1263
+ key={i}
1264
+ className="w-1 h-4 bg-[#00FF41] animate-pulse"
1265
+ style={{ animationDelay: `${i * 0.2}s` }}
1266
+ />
1267
+ ))}
1268
+ </div>
1269
+ </h1>
1270
+ <p className="text-sm text-neutral-400">
1271
+ Aggregating headlines, peer rotation, and macro signals from the current observation.
1272
+ </p>
1273
+ </header>
1274
+
1275
+ <div className="flex-1 flex gap-6 min-h-0">
1276
+ {/* Neural map */}
1277
+ <div className="flex-1 glass-panel rounded-2xl relative overflow-hidden flex items-center justify-center bg-black/60 p-12">
1278
+ <div className="absolute inset-0 bg-aurora opacity-30 pointer-events-none" />
1279
+ <div className="absolute inset-0 scanline opacity-20 pointer-events-none" />
1280
+
1281
+ <svg className="w-full h-full opacity-60" viewBox="0 0 400 300">
1282
+ <defs>
1283
+ <filter id="glow">
1284
+ <feGaussianBlur stdDeviation="2" result="coloredBlur" />
1285
+ <feMerge>
1286
+ <feMergeNode in="coloredBlur" />
1287
+ <feMergeNode in="SourceGraphic" />
1288
+ </feMerge>
1289
+ </filter>
1290
+ </defs>
1291
+ <g stroke="white" strokeWidth="0.5" strokeOpacity="0.1">
1292
+ <line x1="200" y1="150" x2="100" y2="80" />
1293
+ <line x1="200" y1="150" x2="300" y2="80" />
1294
+ <line x1="200" y1="150" x2="100" y2="220" />
1295
+ <line x1="200" y1="150" x2="300" y2="220" />
1296
+ <line x1="100" y1="80" x2="50" y2="150" />
1297
+ <line x1="100" y1="220" x2="50" y2="150" />
1298
+ <line x1="300" y1="80" x2="350" y2="150" />
1299
+ <line x1="300" y1="220" x2="350" y2="150" />
1300
+ </g>
1301
+ <circle cx="200" cy="150" r="10" fill="#731fff" filter="url(#glow)">
1302
+ <animate attributeName="r" values="10;12;10" dur="3s" repeatCount="indefinite" />
1303
+ </circle>
1304
+ {[
1305
+ { x: 100, y: 80, c: '#00FF41' },
1306
+ { x: 300, y: 80, c: '#00FF41' },
1307
+ { x: 100, y: 220, c: '#ffb4ab' },
1308
+ { x: 300, y: 220, c: '#00FF41' },
1309
+ { x: 50, y: 150, c: '#731fff' },
1310
+ { x: 350, y: 150, c: '#731fff' },
1311
+ ].map((n, i) => (
1312
+ <g key={i}>
1313
+ <circle cx={n.x} cy={n.y} r="4" fill={n.c} filter="url(#glow)">
1314
+ <animate attributeName="opacity" values="0.4;1;0.4" dur={`${2 + i}s`} repeatCount="indefinite" />
1315
+ </circle>
1316
+ <circle cx={n.x} cy={n.y} r="8" fill="none" stroke={n.c} strokeWidth="0.5" strokeOpacity="0.3">
1317
+ <animate attributeName="r" values="8;15;8" dur={`${4 + i}s`} repeatCount="indefinite" />
1318
+ </circle>
1319
+ </g>
1320
+ ))}
1321
+ </svg>
1322
+
1323
+ <div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
1324
+ <Brain className="w-16 h-16 text-[#731fff] opacity-20" />
1325
+ <span className="font-mono text-[10px] text-[#731fff] tracking-[0.5em] mt-4 uppercase">
1326
+ Neural Consensus Active
1327
+ </span>
1328
+ </div>
1329
+ </div>
1330
+
1331
+ {/* Alpha signals — derived from observation */}
1332
+ <div className="w-96 flex flex-col gap-4 overflow-hidden shrink-0">
1333
+ <h3 className="text-xs font-bold text-neutral-500 uppercase tracking-[0.2em] px-2 flex items-center justify-between">
1334
+ Alpha Signals
1335
+ <Activity className="w-3 h-3 text-[#00FF41]" />
1336
+ </h3>
1337
+ <div className="flex-1 overflow-y-auto space-y-4 pr-2">
1338
+ {signals.length > 0 ? (
1339
+ signals.map((s, i) => (
1340
+ <div
1341
+ key={i}
1342
+ className="glass-panel p-4 rounded-xl border-l-2 hover:bg-white/[0.04] transition-all cursor-default group"
1343
+ style={{ borderColor: s.color }}
1344
+ >
1345
+ <div className="flex justify-between items-center mb-2">
1346
+ <span className="text-xs font-bold uppercase tracking-widest" style={{ color: s.color }}>
1347
+ {s.t}
1348
+ </span>
1349
+ <span className="text-[10px] text-neutral-500">{s.time}</span>
1350
+ </div>
1351
+ <p className="text-xs text-neutral-400 leading-relaxed font-mono group-hover:text-neutral-300 transition-colors">
1352
+ {s.msg}
1353
+ </p>
1354
+ </div>
1355
+ ))
1356
+ ) : (
1357
+ <div className="text-[11px] text-neutral-500 font-mono px-2">
1358
+ {observation ? 'No headlines/peers/macro for this step.' : 'Run a task to populate signals.'}
1359
+ </div>
1360
+ )}
1361
+ </div>
1362
+ </div>
1363
+ </div>
1364
+
1365
+ <div className="h-24 glass-panel rounded-xl flex items-center px-8 border-[#00FF41]/10 bg-[#00FF41]/[0.02]">
1366
+ <div className="flex-1 flex flex-col gap-1">
1367
+ <span className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest">Sentiment Hub</span>
1368
+ <div className="flex items-center gap-4">
1369
+ <div className="flex items-baseline gap-2">
1370
+ <span className="text-2xl font-bold text-white">{toneScale}</span>
1371
+ <span
1372
+ className={`text-xs font-bold uppercase tracking-widest ${
1373
+ sentimentLabel === 'BULL'
1374
+ ? 'text-[#00FF41]'
1375
+ : sentimentLabel === 'BEAR'
1376
+ ? 'text-[#ffb4ab]'
1377
+ : 'text-neutral-400'
1378
+ }`}
1379
+ >
1380
+ {sentimentLabel}
1381
+ </span>
1382
+ </div>
1383
+ </div>
1384
+ </div>
1385
+ <div className="flex items-center gap-12">
1386
+ {[
1387
+ { l: 'News Tone', v: meanTone.toFixed(2) },
1388
+ { l: 'Sentiment', v: sentimentLabel },
1389
+ { l: 'Peer Beta', v: peerBeta },
1390
+ ].map((m, i) => (
1391
+ <div key={i} className="flex flex-col items-center">
1392
+ <span className="text-[9px] font-bold text-neutral-600 uppercase mb-1">{m.l}</span>
1393
+ <span className="text-xs font-mono font-bold text-neutral-300">{m.v}</span>
1394
+ </div>
1395
+ ))}
1396
+ </div>
1397
+ </div>
1398
+ </div>
1399
+ );
1400
+ };
1401
+
1402
+ // ------------------------------------------------------------------------- App
1403
+
1404
+ type Tab = 'Terminal' | 'Council' | 'Training' | 'Gallery' | 'Portfolio' | 'Intelligence';
1405
+
1406
+ export default function App() {
1407
+ const [activeTab, setActiveTab] = useState<Tab>('Terminal');
1408
+ const env = useStockerEnv();
1409
+
1410
+ const onDeploy = (taskId: string) => {
1411
+ void env.selectTask(taskId);
1412
+ setActiveTab('Terminal');
1413
+ };
1414
+
1415
+ const renderView = () => {
1416
+ switch (activeTab) {
1417
+ case 'Terminal':
1418
+ return <TerminalView env={env} />;
1419
+ case 'Council':
1420
+ return <CouncilView env={env} />;
1421
+ case 'Training':
1422
+ return <TrainingView />;
1423
+ case 'Gallery':
1424
+ return <GalleryView env={env} onDeploy={onDeploy} />;
1425
+ case 'Portfolio':
1426
+ return <PortfolioView env={env} />;
1427
+ case 'Intelligence':
1428
+ return <IntelligenceView env={env} />;
1429
+ default:
1430
+ return <TerminalView env={env} />;
1431
+ }
1432
+ };
1433
+
1434
+ return (
1435
+ <div className="flex min-h-screen bg-[#050505] text-[#e5e2e1] font-sans selection:bg-[#00FF41]/20">
1436
+ <div className="bg-aurora fixed inset-0 pointer-events-none z-0" />
1437
+ <div className="fixed inset-0 pointer-events-none z-50 opacity-[0.03] scanline" />
1438
+
1439
+ {/* Sidebar */}
1440
+ <nav className="w-64 fixed left-0 top-0 h-screen bg-black/40 backdrop-blur-3xl border-r border-white/5 flex flex-col pt-20 pb-8 z-40 transition-all">
1441
+ <div className="px-6 mb-8 flex items-center gap-3">
1442
+ <div className="w-10 h-10 rounded-lg bg-white/5 border border-white/10 flex items-center justify-center relative overflow-hidden group">
1443
+ <Cpu className="w-6 h-6 text-[#00FF41] opacity-70 group-hover:scale-110 transition-transform" />
1444
+ <div className="absolute inset-0 bg-gradient-to-br from-[#00FF41]/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
1445
+ </div>
1446
+ <div>
1447
+ <h2 className="font-bold text-sm tracking-widest text-[#e5e2e1]">ALPHA ENGINE</h2>
1448
+ <p className="text-[10px] text-[#00FF41] font-mono tracking-widest mt-0.5 uppercase">
1449
+ {env.taskId ?? 'Booting…'}
1450
+ </p>
1451
+ </div>
1452
+ </div>
1453
+
1454
+ <div className="flex-1 px-3 space-y-1">
1455
+ <SidebarItem icon={Monitor} label="Terminal" active={activeTab === 'Terminal'} onClick={() => setActiveTab('Terminal')} />
1456
+ <SidebarItem icon={Users} label="Council" active={activeTab === 'Council'} onClick={() => setActiveTab('Council')} />
1457
+ <SidebarItem icon={TrendingUp} label="Training" active={activeTab === 'Training'} onClick={() => setActiveTab('Training')} />
1458
+ <SidebarItem icon={Library} label="Gallery" active={activeTab === 'Gallery'} onClick={() => setActiveTab('Gallery')} />
1459
+ <SidebarItem icon={Wallet} label="Portfolio" active={activeTab === 'Portfolio'} onClick={() => setActiveTab('Portfolio')} />
1460
+ <SidebarItem icon={Brain} label="Intelligence" active={activeTab === 'Intelligence'} onClick={() => setActiveTab('Intelligence')} />
1461
+ </div>
1462
+
1463
+ <div className="px-3 mt-auto space-y-1 border-t border-white/5 pt-4">
1464
+ <button className="w-full flex items-center gap-3 px-4 py-2 rounded-lg text-neutral-500 hover:bg-white/5 hover:text-[#00FF41] transition-all">
1465
+ <FileText className="w-4 h-4" />
1466
+ <span className="text-xs font-medium">Docs</span>
1467
+ </button>
1468
+ <button className="w-full flex items-center gap-3 px-4 py-2 rounded-lg text-neutral-500 hover:bg-white/5 hover:text-[#00FF41] transition-all">
1469
+ <LifeBuoy className="w-4 h-4" />
1470
+ <span className="text-xs font-medium">Support</span>
1471
+ </button>
1472
+ </div>
1473
+
1474
+ <div className="px-6 mt-6">
1475
+ <div
1476
+ className={`p-3 rounded-lg border flex items-center gap-3 ${
1477
+ env.error
1478
+ ? 'bg-[#ffb4ab]/5 border-[#ffb4ab]/20'
1479
+ : env.loading
1480
+ ? 'bg-[#731fff]/5 border-[#731fff]/20'
1481
+ : 'bg-[#00FF41]/5 border-[#00FF41]/20'
1482
+ }`}
1483
+ >
1484
+ <div
1485
+ className={`w-2 h-2 rounded-full animate-pulse ${
1486
+ env.error ? 'bg-[#ffb4ab]' : env.loading ? 'bg-[#731fff]' : 'bg-[#00FF41]'
1487
+ }`}
1488
+ />
1489
+ <div className="flex flex-col">
1490
+ <span
1491
+ className={`text-[10px] font-bold uppercase tracking-widest ${
1492
+ env.error ? 'text-[#ffb4ab]' : env.loading ? 'text-[#731fff]' : 'text-[#00FF41]'
1493
+ }`}
1494
+ >
1495
+ {env.error ? 'API Error' : env.loading ? 'Working…' : 'System Ready'}
1496
+ </span>
1497
+ <span className="text-[8px] text-neutral-500 font-mono truncate max-w-[160px]">
1498
+ {env.error ?? `task: ${env.taskId ?? '—'}`}
1499
+ </span>
1500
+ </div>
1501
+ </div>
1502
+ </div>
1503
+ </nav>
1504
+
1505
+ {/* Main Content */}
1506
+ <div className="flex-1 ml-64 flex flex-col relative z-10">
1507
+ <header className="h-14 fixed top-0 right-0 left-64 bg-black/40 backdrop-blur-xl border-b border-white/5 px-6 flex items-center justify-between z-50">
1508
+ <div className="flex items-center gap-4">
1509
+ <span className="text-xl font-black tracking-widest text-[#00FF41] drop-shadow-[0_0_8px_rgba(0,255,65,0.4)]">STOCKER AI</span>
1510
+ </div>
1511
+
1512
+ <div className="flex items-center gap-6">
1513
+ <div className="relative group hidden md:block">
1514
+ <Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-neutral-500 group-focus-within:text-[#00FF41] transition-colors" />
1515
+ <input
1516
+ type="text"
1517
+ placeholder="Search parameters..."
1518
+ className="bg-white/5 border border-white/10 rounded-full py-1.5 pl-10 pr-4 text-xs font-mono text-[#e5e2e1] focus:outline-none focus:border-[#00FF41]/50 w-64 transition-all"
1519
+ />
1520
+ </div>
1521
+
1522
+ <div className="flex items-center gap-4">
1523
+ <button className="text-neutral-500 hover:text-white transition-colors relative">
1524
+ <Bell className="w-5 h-5" />
1525
+ <span className="absolute -top-1 -right-1 w-2 h-2 bg-[#00FF41] rounded-full" />
1526
+ </button>
1527
+ <button className="text-neutral-500 hover:text-white transition-colors">
1528
+ <CreditCard className="w-5 h-5" />
1529
+ </button>
1530
+ <button className="text-neutral-500 hover:text-white transition-colors">
1531
+ <User className="w-5 h-5" />
1532
+ </button>
1533
+ </div>
1534
+ </div>
1535
+ </header>
1536
+
1537
+ <main className="pt-14 p-6 flex-1 flex flex-col min-h-screen">
1538
+ <AnimatePresence mode="wait">
1539
+ <motion.div
1540
+ key={activeTab}
1541
+ initial={{ opacity: 0, y: 10 }}
1542
+ animate={{ opacity: 1, y: 0 }}
1543
+ exit={{ opacity: 0, y: -10 }}
1544
+ transition={{ duration: 0.2 }}
1545
+ className="flex-1 flex flex-col"
1546
+ >
1547
+ {renderView()}
1548
+ </motion.div>
1549
+ </AnimatePresence>
1550
+ </main>
1551
+ </div>
1552
+ </div>
1553
+ );
1554
+ }
frontend/src/api.ts ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // TypeScript mirrors of the Pydantic models in app/models.py.
2
+ // Keep these in sync if the backend contract changes.
3
+
4
+ import { useCallback, useEffect, useState } from 'react';
5
+
6
+ export type Side = 'buy' | 'sell' | 'hold';
7
+
8
+ export interface MarketObservation {
9
+ ticker: string;
10
+ date: string;
11
+ price: number;
12
+ price_history: number[];
13
+ fundamentals: Record<string, unknown>;
14
+ cash: number;
15
+ position: number;
16
+ portfolio_value: number;
17
+ task_id: string;
18
+ step_number: number;
19
+ total_steps: number;
20
+ chart_path: string;
21
+ headlines: Array<{ date: string; headline: string; source: string; sentiment_label: string }>;
22
+ forum_excerpts: Array<{ date: string; subreddit: string; score: number; post_text: string }>;
23
+ indicators: Record<string, number | null>;
24
+ peers: {
25
+ peers: Array<{ peer_ticker: string; peer_close: number | null }>;
26
+ commodity?: string | null;
27
+ commodity_price?: number | null;
28
+ };
29
+ macro: Array<{ date: string; country: string; headline: string; policy_signal: string }>;
30
+ }
31
+
32
+ export interface EnvironmentState {
33
+ task_id: string;
34
+ current_step: number;
35
+ total_steps: number;
36
+ done: boolean;
37
+ cash: number;
38
+ position: number;
39
+ portfolio_value: number;
40
+ action_history: Array<{ side: Side; quantity: number }>;
41
+ reward_history: number[];
42
+ }
43
+
44
+ export interface SpecialistVote {
45
+ name: string;
46
+ signal: number;
47
+ confidence: number;
48
+ rationale: string;
49
+ }
50
+
51
+ export interface CouncilDecision {
52
+ votes: SpecialistVote[];
53
+ action: { side: Side; quantity: number };
54
+ rationale: string;
55
+ }
56
+
57
+ export interface OhlcvBar {
58
+ time: string;
59
+ open: number;
60
+ high: number;
61
+ low: number;
62
+ close: number;
63
+ volume: number;
64
+ in_episode: boolean;
65
+ }
66
+
67
+ export interface OhlcvResponse {
68
+ task_id: string;
69
+ ticker: string;
70
+ bars: OhlcvBar[];
71
+ }
72
+
73
+ export interface TrainingMetrics {
74
+ status: 'completed' | 'no_runs';
75
+ run_name?: string;
76
+ summary: Array<{
77
+ task_id: string;
78
+ total_reward: number;
79
+ final_portfolio: number;
80
+ buy_and_hold: number;
81
+ alpha_pct: number;
82
+ }>;
83
+ mean_alpha_pct: number;
84
+ reward_curve_png?: string | null;
85
+ portfolio_curve_png?: string | null;
86
+ }
87
+
88
+ // ---------- fetch helpers --------------------------------------------------
89
+
90
+ async function jget<T>(url: string): Promise<T> {
91
+ const r = await fetch(url);
92
+ if (!r.ok) throw new Error(`${url}: ${r.status}`);
93
+ return r.json() as Promise<T>;
94
+ }
95
+
96
+ async function jpost<T>(url: string, body: unknown): Promise<T> {
97
+ const r = await fetch(url, {
98
+ method: 'POST',
99
+ headers: { 'Content-Type': 'application/json' },
100
+ body: JSON.stringify(body),
101
+ });
102
+ if (!r.ok) throw new Error(`${url}: ${r.status}`);
103
+ return r.json() as Promise<T>;
104
+ }
105
+
106
+ export const api = {
107
+ meta: () => jget<{ tasks: string[]; name: string; version: string }>('/meta'),
108
+ reset: (task_id: string) =>
109
+ jpost<{ observation: MarketObservation; info: Record<string, unknown> }>(
110
+ '/reset',
111
+ { task_id },
112
+ ),
113
+ step: (side: Side, quantity: number) =>
114
+ jpost<{ observation: MarketObservation; reward: number; done: boolean; info: Record<string, unknown> }>(
115
+ '/step',
116
+ { side, quantity },
117
+ ),
118
+ state: () => jget<EnvironmentState>('/state'),
119
+ ohlcv: (task_id: string) => jget<OhlcvResponse>(`/ohlcv?task_id=${encodeURIComponent(task_id)}`),
120
+ council: () => jget<CouncilDecision>('/council'),
121
+ trainingMetrics: () => jget<TrainingMetrics>('/training/metrics'),
122
+ };
123
+
124
+ // ---------- shared state hook ---------------------------------------------
125
+
126
+ export interface StockerEnv {
127
+ tasks: string[];
128
+ taskId: string | null;
129
+ observation: MarketObservation | null;
130
+ envState: EnvironmentState | null;
131
+ council: CouncilDecision | null;
132
+ ohlcv: OhlcvResponse | null;
133
+ /** Starting cash for the active task, captured from /reset info. */
134
+ startingCash: number;
135
+ loading: boolean;
136
+ error: string | null;
137
+ selectTask: (taskId: string) => Promise<void>;
138
+ submitTrade: (side: Side, quantity: number) => Promise<void>;
139
+ }
140
+
141
+ export function useStockerEnv(): StockerEnv {
142
+ const [tasks, setTasks] = useState<string[]>([]);
143
+ const [taskId, setTaskId] = useState<string | null>(null);
144
+ const [observation, setObservation] = useState<MarketObservation | null>(null);
145
+ const [envState, setEnvState] = useState<EnvironmentState | null>(null);
146
+ const [council, setCouncil] = useState<CouncilDecision | null>(null);
147
+ const [ohlcv, setOhlcv] = useState<OhlcvResponse | null>(null);
148
+ const [startingCash, setStartingCash] = useState<number>(10000);
149
+ const [loading, setLoading] = useState<boolean>(false);
150
+ const [error, setError] = useState<string | null>(null);
151
+
152
+ const selectTask = useCallback(async (next: string) => {
153
+ setLoading(true);
154
+ setError(null);
155
+ try {
156
+ const reset = await api.reset(next);
157
+ setTaskId(next);
158
+ setObservation(reset.observation);
159
+ const sc = Number(reset.info?.starting_cash);
160
+ if (Number.isFinite(sc) && sc > 0) setStartingCash(sc);
161
+ const [bars, st] = await Promise.all([api.ohlcv(next), api.state()]);
162
+ setOhlcv(bars);
163
+ setEnvState(st);
164
+ try {
165
+ setCouncil(await api.council());
166
+ } catch (e) {
167
+ // council can fail silently — UI shows empty state
168
+ setCouncil(null);
169
+ console.warn('council fetch failed', e);
170
+ }
171
+ } catch (e: unknown) {
172
+ const msg = e instanceof Error ? e.message : String(e);
173
+ setError(msg);
174
+ } finally {
175
+ setLoading(false);
176
+ }
177
+ }, []);
178
+
179
+ const submitTrade = useCallback(async (side: Side, quantity: number) => {
180
+ setLoading(true);
181
+ setError(null);
182
+ try {
183
+ const stepRes = await api.step(side, quantity);
184
+ setObservation(stepRes.observation);
185
+ const st = await api.state();
186
+ setEnvState(st);
187
+ if (!stepRes.done) {
188
+ try {
189
+ setCouncil(await api.council());
190
+ } catch (e) {
191
+ setCouncil(null);
192
+ console.warn('council fetch failed', e);
193
+ }
194
+ }
195
+ } catch (e: unknown) {
196
+ const msg = e instanceof Error ? e.message : String(e);
197
+ setError(msg);
198
+ } finally {
199
+ setLoading(false);
200
+ }
201
+ }, []);
202
+
203
+ // bootstrap: load tasks, then auto-select first
204
+ useEffect(() => {
205
+ let cancelled = false;
206
+ api
207
+ .meta()
208
+ .then((m) => {
209
+ if (cancelled) return;
210
+ setTasks(m.tasks);
211
+ if (m.tasks.length > 0) {
212
+ void selectTask(m.tasks[0]);
213
+ }
214
+ })
215
+ .catch((e: unknown) => {
216
+ const msg = e instanceof Error ? e.message : String(e);
217
+ setError(msg);
218
+ });
219
+ return () => {
220
+ cancelled = true;
221
+ };
222
+ }, [selectTask]);
223
+
224
+ return {
225
+ tasks,
226
+ taskId,
227
+ observation,
228
+ envState,
229
+ council,
230
+ ohlcv,
231
+ startingCash,
232
+ loading,
233
+ error,
234
+ selectTask,
235
+ submitTrade,
236
+ };
237
+ }
238
+
239
+ // Display-name + role keyword mapping for the seven specialists.
240
+ // Backend `name` values come from app/council/specialists.py.
241
+ export const SPECIALIST_DISPLAY: Record<string, string> = {
242
+ chart_pattern: 'Chart Pattern',
243
+ seasonal_trend: 'Seasonal',
244
+ indicator: 'Indicator',
245
+ news: 'News',
246
+ forum_sentiment: 'Forum',
247
+ peer_commodity: 'Peer',
248
+ geopolitics: 'Geo',
249
+ };
250
+
251
+ export function statusFromSignal(signal: number): 'green' | 'red' | 'gray' {
252
+ if (signal > 0.1) return 'green';
253
+ if (signal < -0.1) return 'red';
254
+ return 'gray';
255
+ }
frontend/src/index.css ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&family=JetBrains+Mono:wght@400;500;700&display=swap');
2
+ @import "tailwindcss";
3
+
4
+ @theme {
5
+ --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
6
+ --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
7
+ }
8
+
9
+ @layer base {
10
+ body {
11
+ @apply bg-[#050505] text-[#e5e2e1] antialiased min-h-screen overflow-x-hidden;
12
+ }
13
+ }
14
+
15
+ @layer components {
16
+ .glass-panel {
17
+ @apply bg-white/5 backdrop-blur-xl border border-white/10;
18
+ }
19
+
20
+ .terminal-text {
21
+ @apply font-mono text-sm tracking-tight;
22
+ }
23
+
24
+ .neon-green {
25
+ @apply text-[#00FF41] drop-shadow-[0_0_8px_rgba(0,255,65,0.4)];
26
+ }
27
+
28
+ .neon-border {
29
+ @apply border-[#00FF41]/30 hover:border-[#00FF41]/60 transition-colors;
30
+ }
31
+
32
+ .bg-aurora {
33
+ background: radial-gradient(circle at 15% 50%, rgba(115, 31, 255, 0.08) 0%, transparent 50%),
34
+ radial-gradient(circle at 85% 30%, rgba(0, 255, 65, 0.05) 0%, transparent 50%);
35
+ background-attachment: fixed;
36
+ }
37
+ }
38
+
39
+ @keyframes scanline {
40
+ 0% { transform: translateY(-100%); }
41
+ 100% { transform: translateY(100%); }
42
+ }
43
+
44
+ .scanline::after {
45
+ content: "";
46
+ position: absolute;
47
+ top: 0;
48
+ left: 0;
49
+ width: 100%;
50
+ height: 2px;
51
+ background: rgba(0, 255, 65, 0.05);
52
+ animation: scanline 8s linear infinite;
53
+ pointer-events: none;
54
+ }
frontend/src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import {StrictMode} from 'react';
2
+ import {createRoot} from 'react-dom/client';
3
+ import App from './App.tsx';
4
+ import './index.css';
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ );
frontend/tsconfig.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "experimentalDecorators": true,
5
+ "useDefineForClassFields": false,
6
+ "module": "ESNext",
7
+ "lib": [
8
+ "ES2022",
9
+ "DOM",
10
+ "DOM.Iterable"
11
+ ],
12
+ "skipLibCheck": true,
13
+ "moduleResolution": "bundler",
14
+ "isolatedModules": true,
15
+ "moduleDetection": "force",
16
+ "allowJs": true,
17
+ "jsx": "react-jsx",
18
+ "paths": {
19
+ "@/*": [
20
+ "./*"
21
+ ]
22
+ },
23
+ "allowImportingTsExtensions": true,
24
+ "noEmit": true
25
+ }
26
+ }
frontend/vite.config.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tailwindcss from '@tailwindcss/vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import path from 'path';
4
+ import {defineConfig} from 'vite';
5
+
6
+ export default defineConfig(() => ({
7
+ plugins: [react(), tailwindcss()],
8
+ resolve: {
9
+ alias: {
10
+ '@': path.resolve(__dirname, '.'),
11
+ },
12
+ },
13
+ server: {
14
+ hmr: process.env.DISABLE_HMR !== 'true',
15
+ proxy: {
16
+ '/health': 'http://localhost:8000',
17
+ '/meta': 'http://localhost:8000',
18
+ '/reset': 'http://localhost:8000',
19
+ '/step': 'http://localhost:8000',
20
+ '/state': 'http://localhost:8000',
21
+ '/ohlcv': 'http://localhost:8000',
22
+ '/council': 'http://localhost:8000',
23
+ '/training': 'http://localhost:8000',
24
+ },
25
+ },
26
+ }));
scripts/build_ideal_profit.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Precompute per-task ideal profit trajectories used by the grader.
3
+
4
+ For each task in TASK_META we simulate a perfect-foresight policy on the
5
+ episode price series: at every step we go max-long if the next bar is up
6
+ and flat otherwise. The resulting per-step cumulative PnL fraction is
7
+ written to data/ideal_profits/<task_id>.json. The grader (per-step
8
+ performance component) reads these sidecars and compares actual PnL to
9
+ the ideal trajectory.
10
+
11
+ Transaction cost is applied to the ideal trajectory at the same rate the
12
+ env uses, so the gap the model sees reflects only foresight quality.
13
+
14
+ Run:
15
+ uv run python scripts/build_ideal_profit.py
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ ROOT = Path(__file__).resolve().parent.parent
24
+ sys.path.insert(0, str(ROOT))
25
+
26
+ from app.config import settings # noqa: E402
27
+ from app.core.tasks import TASK_META, list_task_ids # noqa: E402
28
+ from app.data import loader # noqa: E402
29
+
30
+ OUTPUT_DIR = ROOT / "data" / "ideal_profits"
31
+
32
+
33
+ def simulate_perfect_foresight(
34
+ prices: list[float],
35
+ starting_cash: float,
36
+ txn_cost_rate: float,
37
+ ) -> tuple[list[float], list[float]]:
38
+ """Multi-transaction perfect foresight on a single ticker.
39
+
40
+ Returns (portfolio_curve, pnl_pct_curve) where each list has len(prices)
41
+ entries and represents the value (and pnl fraction) at the end of each
42
+ step after that step's action has been applied.
43
+ """
44
+ cash = float(starting_cash)
45
+ position = 0
46
+ portfolio_curve: list[float] = []
47
+ pnl_curve: list[float] = []
48
+
49
+ n = len(prices)
50
+ for i in range(n):
51
+ price = prices[i]
52
+ going_up = (i + 1 < n) and (prices[i + 1] > price)
53
+
54
+ if going_up and position == 0:
55
+ # Affordable shares after paying txn cost: solve qty * price * (1 + r) <= cash.
56
+ qty = int(cash // (price * (1.0 + txn_cost_rate)))
57
+ if qty > 0:
58
+ notional = qty * price
59
+ cash -= notional + notional * txn_cost_rate
60
+ position += qty
61
+ elif (not going_up) and position > 0:
62
+ notional = position * price
63
+ cash += notional - notional * txn_cost_rate
64
+ position = 0
65
+
66
+ port_value = cash + position * price
67
+ portfolio_curve.append(port_value)
68
+ pnl_curve.append((port_value - starting_cash) / starting_cash)
69
+
70
+ return portfolio_curve, pnl_curve
71
+
72
+
73
+ def build_for_task(task_id: str) -> dict:
74
+ meta = TASK_META[task_id]
75
+ rows = loader.episode_rows(task_id)
76
+ if rows.empty:
77
+ raise RuntimeError(
78
+ f"No episode data for {task_id}. Run scripts/build_dataset.py first."
79
+ )
80
+ prices = rows["close"].astype(float).tolist()
81
+ starting_cash = float(meta["starting_cash"])
82
+
83
+ portfolio_curve, pnl_curve = simulate_perfect_foresight(
84
+ prices=prices,
85
+ starting_cash=starting_cash,
86
+ txn_cost_rate=settings.transaction_cost_rate,
87
+ )
88
+
89
+ return {
90
+ "task_id": task_id,
91
+ "ticker": meta["ticker"],
92
+ "starting_cash": starting_cash,
93
+ "transaction_cost_rate": settings.transaction_cost_rate,
94
+ "ideal_pnl_pct_series": [round(v, 6) for v in pnl_curve],
95
+ "ideal_pnl_pct_total": round(pnl_curve[-1], 6) if pnl_curve else 0.0,
96
+ "ideal_portfolio_curve": [round(v, 4) for v in portfolio_curve],
97
+ }
98
+
99
+
100
+ def main() -> int:
101
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
102
+ for task_id in list_task_ids():
103
+ payload = build_for_task(task_id)
104
+ out = OUTPUT_DIR / f"{task_id}.json"
105
+ out.write_text(json.dumps(payload, indent=2))
106
+ n = len(payload["ideal_pnl_pct_series"])
107
+ print(
108
+ f"{task_id:<14} steps={n:>3} "
109
+ f"ideal_total={payload['ideal_pnl_pct_total']:+.4f} "
110
+ f"-> {out.relative_to(ROOT)}"
111
+ )
112
+ return 0
113
+
114
+
115
+ if __name__ == "__main__":
116
+ sys.exit(main())
tests/test_api.py CHANGED
@@ -34,3 +34,38 @@ def test_state_roundtrip():
34
  s = client.get("/state").json()
35
  r = client.post("/state", json=s)
36
  assert r.status_code == 200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  s = client.get("/state").json()
35
  r = client.post("/state", json=s)
36
  assert r.status_code == 200
37
+
38
+
39
+ def test_ohlcv_returns_bars():
40
+ r = client.get("/ohlcv", params={"task_id": "task_easy"})
41
+ assert r.status_code == 200
42
+ body = r.json()
43
+ assert body["ticker"] == "AAPL"
44
+ assert len(body["bars"]) > 0
45
+ bar = body["bars"][0]
46
+ assert {"time", "open", "high", "low", "close", "volume", "in_episode"} <= bar.keys()
47
+ assert bar["high"] >= bar["low"]
48
+
49
+
50
+ def test_ohlcv_unknown_task_404():
51
+ r = client.get("/ohlcv", params={"task_id": "nope"})
52
+ assert r.status_code == 404
53
+
54
+
55
+ def test_council_after_reset():
56
+ client.post("/reset", json={"task_id": "task_easy"})
57
+ r = client.get("/council")
58
+ assert r.status_code == 200
59
+ body = r.json()
60
+ assert len(body["votes"]) == 7
61
+ assert body["action"]["side"] in {"buy", "sell", "hold"}
62
+
63
+
64
+ def test_training_metrics_returns_summary():
65
+ r = client.get("/training/metrics")
66
+ assert r.status_code == 200
67
+ body = r.json()
68
+ assert body["status"] in {"completed", "no_runs"}
69
+ if body["status"] == "completed":
70
+ assert len(body["summary"]) >= 1
71
+ assert "alpha_pct" in body["summary"][0]
tests/test_graders.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Unit tests for the grader (compute_step_reward).
2
+
3
+ Covers the asymmetric performance shape, the inflation drag component, and
4
+ the env-level transaction cost deduction.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from app.config import Settings
9
+ from app.core.environment import StockerEnv
10
+ from app.core.graders import compute_step_reward
11
+ from app.models import TradeAction
12
+
13
+
14
+ def _settings(**overrides) -> Settings:
15
+ base = dict(
16
+ transaction_cost_rate=0.001,
17
+ annual_inflation_rate=0.05,
18
+ reward_weight_performance=0.7,
19
+ reward_weight_inflation=0.3,
20
+ )
21
+ base.update(overrides)
22
+ return Settings(**base)
23
+
24
+
25
+ def _call(
26
+ *,
27
+ actual_pnl_pct: float,
28
+ ideal_at_step: float,
29
+ ideal_total: float,
30
+ step_index: int = 5,
31
+ total_steps: int = 50,
32
+ starting_cash: float = 10000.0,
33
+ invalid: bool = False,
34
+ settings: Settings | None = None,
35
+ ):
36
+ settings = settings or _settings()
37
+ series = [0.0] * total_steps
38
+ series[step_index] = ideal_at_step
39
+ new_portfolio = starting_cash * (1.0 + actual_pnl_pct)
40
+ return compute_step_reward(
41
+ action=TradeAction(side="hold", quantity=0),
42
+ new_portfolio=new_portfolio,
43
+ starting_cash=starting_cash,
44
+ invalid=invalid,
45
+ step_index=step_index,
46
+ total_steps=total_steps,
47
+ ideal_pnl_pct_series=series,
48
+ ideal_pnl_pct_total=ideal_total,
49
+ settings=settings,
50
+ )
51
+
52
+
53
+ def test_at_ideal_step_yields_high_performance():
54
+ # actual real ~= ideal at this step -> gap ~= 0 -> perf ~= 1
55
+ result = _call(actual_pnl_pct=0.05, ideal_at_step=0.05, ideal_total=0.20)
56
+ assert result.breakdown["performance_factor"] > 0.95
57
+ assert result.score > 0.5
58
+
59
+
60
+ def test_far_behind_ideal_yields_punishment():
61
+ # actual < ideal by much more than scale -> perf = -1
62
+ result = _call(actual_pnl_pct=-0.05, ideal_at_step=0.30, ideal_total=0.30)
63
+ assert result.breakdown["performance_factor"] == -1.0
64
+ assert result.breakdown["weighted_performance"] < 0
65
+
66
+
67
+ def test_outperforming_ideal_yields_bonus():
68
+ # gap < 0 -> perf > 1.0 (env clip will cap, but raw breakdown shows the bonus)
69
+ result = _call(actual_pnl_pct=0.10, ideal_at_step=0.05, ideal_total=0.10)
70
+ assert result.breakdown["performance_factor"] > 1.0
71
+ assert result.breakdown["gap"] < 0
72
+
73
+
74
+ def test_inflation_factor_is_negative_with_positive_pnl():
75
+ # Real < nominal under positive inflation, so inflation_factor < 0
76
+ result = _call(
77
+ actual_pnl_pct=0.05,
78
+ ideal_at_step=0.05,
79
+ ideal_total=0.20,
80
+ step_index=252, # one year in -> noticeable inflation drag
81
+ total_steps=300,
82
+ )
83
+ assert result.breakdown["inflation_factor"] < 0
84
+ assert result.breakdown["real_pnl_pct"] < result.breakdown["nominal_pnl_pct"]
85
+
86
+
87
+ def test_invalid_action_subtracts_penalty():
88
+ fine = _call(actual_pnl_pct=0.05, ideal_at_step=0.05, ideal_total=0.20)
89
+ bad = _call(actual_pnl_pct=0.05, ideal_at_step=0.05, ideal_total=0.20, invalid=True)
90
+ assert bad.score < fine.score
91
+ assert "invalid_action_penalty" in bad.breakdown
92
+
93
+
94
+ def test_buy_deducts_transaction_cost_from_cash():
95
+ """End-to-end env check: a buy at 0.1% transaction cost shaves cash."""
96
+ env = StockerEnv(task_id="task_easy")
97
+ env.reset()
98
+ cash_before = env.state().cash
99
+ price = env._prices[0]
100
+ qty = 5
101
+ env.step({"side": "buy", "quantity": qty})
102
+ cash_after = env.state().cash
103
+
104
+ notional = qty * price
105
+ expected_cost = notional * 1.001 # 0.1% txn cost
106
+ assert abs((cash_before - cash_after) - expected_cost) < 1e-4
107
+
108
+
109
+ def test_sell_deducts_transaction_cost_from_proceeds():
110
+ env = StockerEnv(task_id="task_easy")
111
+ env.reset()
112
+ qty = 5
113
+ env.step({"side": "buy", "quantity": qty})
114
+ cash_after_buy = env.state().cash
115
+ sell_price = env._prices[1] # next-step price (env advanced after the buy)
116
+ env.step({"side": "sell", "quantity": qty})
117
+ cash_after_sell = env.state().cash
118
+
119
+ notional = qty * sell_price
120
+ expected_proceeds = notional * 0.999
121
+ assert abs((cash_after_sell - cash_after_buy) - expected_proceeds) < 1e-4
uv.lock ADDED
The diff for this file is too large to render. See raw diff