Skip to content

themahmoudzaki/Nebu

Repository files navigation

NEBU Investment App

A locally-hosted halal-DCA research pipeline. FastAPI + LangGraph backend, React + strict-TypeScript frontend (Vite), two interchangeable LLM providers (OpenRouter and LMStudio), and a user-managed portfolio the pipeline reads but never writes.

Project layout

investment-app/
├── apps/
│   └── web/                       # React + strict TS frontend (Vite)
│       ├── src/
│       │   ├── components/        # TopBar, Sidebar, ResultsPanel, …
│       │   ├── hooks/             # useRun (WS lifecycle, ETA tracking)
│       │   ├── lib/               # api, storage, format, eta, nodes
│       │   ├── state/             # Settings context provider
│       │   ├── types/api.ts       # backend-mirroring type definitions
│       │   ├── App.tsx
│       │   └── main.tsx
│       ├── package.json
│       ├── tsconfig.json          # strict + noUncheckedIndexedAccess
│       └── vite.config.ts
├── backend/                       # FastAPI + LangGraph (Python ≥ 3.10)
│   ├── main.py                    # WS server + REST + static mount
│   ├── graph.py                   # LangGraph workflows (one per mode)
│   ├── agents.py                  # System prompts (every prompt sees user portfolio)
│   ├── llm.py                     # OpenRouter / LMStudio client
│   ├── search.py                  # DuckDuckGo (default) + optional Tavily
│   ├── storage.py                 # User portfolio CRUD with atomic writes
│   ├── runs.py                    # Run registry + on-disk history with quota
│   ├── pipeline_config.py         # Halal rules + AUM-tier loader
│   ├── config.py                  # Env-driven runtime configuration
│   ├── security.py                # Auth, run-id validation, SSRF guard, rate limit
│   ├── logging_setup.py           # Structured logging (text or JSON)
│   ├── hardware.py                # RAM/GPU detection + ctx-length recommendation
│   └── py.typed                   # PEP 561 marker
├── data/                          # Persistent portfolio + run-history (created on first run)
├── docs/
│   ├── ARCHITECTURE.md
│   ├── DEPLOYMENT.md
│   └── PUBLISHING.md
├── scripts/
│   ├── start.sh                   # macOS / Linux / WSL launcher
│   └── start.bat                  # Windows launcher
├── tests/                         # pytest smoke + unit tests
├── .github/workflows/ci.yml       # Lint + typecheck + tests on every push
├── compose.yml                    # Single-service compose for laptop / NAS
├── Dockerfile                     # Multi-stage: web-builder + py-builder + runtime
├── pyproject.toml                 # Pinned deps, strict ruff + mypy config
├── .env.example                   # Annotated reference for all NEBU_* env vars
├── .gitignore
├── .dockerignore
├── CHANGELOG.md
├── CONTRIBUTING.md
└── LICENSE                        # MIT

Quick start

# Windows
scripts\start.bat

# macOS / Linux / WSL
chmod +x scripts/start.sh && scripts/start.sh

The launcher prepares .venv, installs the backend, builds the frontend bundle if missing, then starts uvicorn. Open http://127.0.0.1:8000.

Both launchers detect uv and use it when present; otherwise they fall back to python -m venv + pip. Frontend builds need Node.js 20+.

Manual install

# 1. Backend
uv venv .venv
source .venv/bin/activate           # .venv\Scripts\activate.bat on Windows
uv pip install -e ".[dev]"          # add ".[dev]" for tests + lint + mypy

# 2. Frontend
(cd apps/web && npm install && npm run build)

# 3. Run
python -m uvicorn backend.main:app --host 127.0.0.1 --port 8000

Dev workflow

Two terminals, the frontend gets HMR and the backend gets --reload:

# Terminal A
python -m uvicorn backend.main:app --host 127.0.0.1 --port 8000 --reload

# Terminal B  (proxies /api → 127.0.0.1:8000, including WebSockets)
cd apps/web && npm run dev

Open http://127.0.0.1:5173.

Modes

# Mode Pipeline
1 EGP Safe — halal Egyptian funds/ETFs Consolidation → Halal + Macro → Selection → Rebalance + Purification → Audit
2 US Safe — halal US ETFs (SPUS, HLAL, UMMA, SPRE, SPSK) same six layers
3 Single Stock — adversarial Bull/Bear council, gated at $25k AUM Preconditions → Universe → Bull + Bear → Scenario → Selector → Audit
4 Financial Q&A — open question, web-cited answer Plan → Search → Synthesize

The Consolidation Advisor is a Step-0 hard gate. When it returns CONSOLIDATE or DCA_EXISTING, the analysis chain is short-circuited and the Selection is synthesized deterministically from the user's existing halal core. Single-stock mode refuses to run until AUM ≥ $25,000.

Portfolio (user-managed)

The sidebar Treasury is the single source of truth for what you hold, stored at data/portfolio.json. The pipeline never writes to it.

  • You add positions manually after you've placed a real trade.
  • Every mode reads your portfolio fresh on every request.
  • FX is captured per-trade — there is no global mutable FX setting.
  • Each position carries: name, ticker, type, market, qty, unit cost, currency, fx_rate_at_purchase (required for non-USD), halal status, and notes.

Position records survive a partial-write crash thanks to a tmp-file + os.replace write path with a data/portfolio.backup.json snapshot.

LLM providers

Both providers go through the same OpenAI-compatible chat/completions endpoint, so prompts and graph wiring are identical.

OpenRouter

  1. Get an API key from https://openrouter.ai/keys.
  2. Settings drawer → OpenRouter → paste the key. The key lives in sessionStorage by default (cleared on tab close); tick "Remember key on this device" to mirror it to localStorage.

LMStudio (local)

  1. Install LMStudio, download a JSON-friendly model.
  2. Developer → Start Server on port 1234.
  3. Settings drawer → LMStudio → set model id and base URL (http://localhost:1234/v1). Click Detect for a context-length recommendation based on your RAM/VRAM.

The backend's SSRF guard restricts LMStudio base_url to loopback + private LAN ranges. Override with NEBU_LMSTUDIO_HOSTS if you have a remote gateway on a public host.

Production posture

The default is local-only and conservative; opt into wider exposure explicitly. See docs/DEPLOYMENT.md for the full env matrix.

  • Bind: 127.0.0.1:8000 by default. Set NEBU_HOST=0.0.0.0 to expose on the LAN — and set NEBU_AUTH_TOKEN first.
  • CORS limited to the bound origin by default.
  • TrustedHost auto-enabled when NEBU_HOST is non-localhost.
  • Rate limit per-IP — 6 runs/minute and 30 probes/minute by default.
  • Run-id validation: every {run_id} is hex-only — no path traversal.
  • Atomic writes: portfolio + run history use tmp + rename.
  • CSP served on every response. Frame-ancestors none, scripts 'self', fonts whitelisted to Google Fonts.
  • Logging: human-readable text by default; set NEBU_LOG_JSON=1 for line-delimited JSON.

Quality gates

# Python
ruff check backend/
mypy backend/                      # strict on the backend package
pytest -q

# Frontend
cd apps/web
npm run typecheck                  # tsc -b --noEmit, strict TS
npm run lint
npm run build

CI runs the same set on every push — see .github/workflows/ci.yml.

Docker

docker compose up -d
docker compose logs -f

The compose file binds 127.0.0.1:8000 on the host. Edit the port mapping and set NEBU_AUTH_TOKEN before exposing publicly. The nebu-data volume holds the portfolio + run history.

API surface

GET    /                                 React bundle
GET    /api/health                       liveness + writability probe (no auth)
GET    /api/modes                        mode metadata
GET    /api/openrouter-models            curated dropdown of common models
POST   /api/probe                        round-trip test of an LLM config
GET    /api/hardware                     RAM/GPU/CPU snapshot
GET    /api/lmstudio/models              models loaded in LMStudio
POST   /api/recommend-context            hardware-aware ctx-length suggestion
GET    /api/portfolio                    read user portfolio + aggregates
POST   /api/portfolio/positions          add a position
PUT    /api/portfolio/positions/{id}     update a position
DELETE /api/portfolio/positions/{id}     remove a position
PUT    /api/portfolio/settings           update base_currency / notes
PUT    /api/portfolio/prices             bulk price update
PUT    /api/portfolio/fx                 bulk FX update
GET    /api/portfolio/concentration      AUM tier + breaches (no LLM)
POST   /api/runs                         start a pipeline run
GET    /api/runs                         in-memory active runs
GET    /api/runs/history                 on-disk finished-run index
DELETE /api/runs/history                 wipe history
GET    /api/runs/{run_id}                run + events
POST   /api/runs/{run_id}/cancel         cancel an in-flight run
GET    /api/runs/{run_id}/export?format=md  Markdown export
DELETE /api/runs/{run_id}                delete a finished run
WS     /api/runs/{run_id}/stream         live event stream (?since=N to resume)

WebSocket events stream in order: pipeline_start → node_start / activity → search → result → done / error. The connection survives reloads because runs live in the backend; the React client re-attaches with a since=N offset.

Optional: parent halal-rules integration

If you keep the app inside the parent investing/ repo, it picks up pipeline/halal-rules.json and injects the canonical AAOIFI / MSCI screens into every Halal-Screener prompt. Drop the folder anywhere else and the app falls back to embedded defaults — it stays portable.

License

MIT — see LICENSE.

About

Locally-hosted halal DCA research pipeline, FastAPI + LangGraph backend, React + TypeScript frontend, multi-mode analysis (EGP/US ETFs, single stocks, Q&A), dual LLM support (OpenRouter + LMStudio), and a user-managed portfolio the AI reads.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors