Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ ACCT_HODL= # HODL tier account (alerts only, never auto-trades)

# ── White-label config ───────────────────────────────────────────────────────
LEVI_CONFIG_PATH=./levi_config.json
LEVI_WORKSPACE_ROOT=./workspace
LEVI_DEFAULT_EXECUTION_MODE=paper_trading
LEVI_DEFAULT_TRADING_MODE=swing_trading

# ── Execution ────────────────────────────────────────────────────────────────
AUTO_EXECUTE=false # true = no confirmation prompts (after 30d paper)
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ dist/
.venv/
*.pyc
.pytest_cache/
/workspace/
*.egg-info/
.DS_Store
69 changes: 69 additions & 0 deletions PHASE_1_STEP_1_IMPLEMENTATION_REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Phase 1, Step 1 Implementation Report

## Summary

Implemented the five approved foundations: validated user trading profiles, deterministic mode routing, isolated user workspace initialization, a vendor-neutral evidence contract and registry, and the pre-analysis `What You Need` contract. Added `POST /api/what-you-need` to the existing FastAPI application without changing LEVI's paper-trading, alert-mode, deterministic risk, execution, specialist-agent, model-routing, or dashboard behavior.

## Files created

- `levi/__init__.py`
- `levi/profiles/__init__.py`
- `levi/profiles/models.py`
- `levi/modes/__init__.py`
- `levi/modes/router.py`
- `levi/workspace/__init__.py`
- `levi/workspace/initializer.py`
- `levi/evidence/__init__.py`
- `levi/evidence/models.py`
- `levi/evidence/registry.py`
- `levi/contracts/__init__.py`
- `levi/contracts/what_you_need.py`
- `tests/test_phase_1_step_1.py`
- `docs/PHASE_1_STEP_1.md`
- `PHASE_1_STEP_1_IMPLEMENTATION_REPORT.md`

## Files modified

- `.env.example` — added only the three approved LEVI workspace/default-mode values.
- `.gitignore` — excluded the development `workspace/` directory.
- `README.md` — added a link to the phase document.
- `bot/status_api.py` — added the request model, evidence registry, and `POST /api/what-you-need` route.
- `requirements.txt` — made the Pydantic dependency explicit.
- `tests/test_levi_integration.py` — replaced a fixed, expired mock option date with a deterministic future date.

## Tests and checks run

1. `uv run --with-requirements requirements.txt python -m pytest tests/test_phase_1_step_1.py -q`
- Result: 11 passed.
2. `uv run --with-requirements requirements.txt python -m pytest tests -q`
- Final result: 31 passed.
3. `uv run --with-requirements requirements.txt python -m compileall -q levi bot`
- Result: passed.
4. FastAPI import/route assertion for `POST /api/what-you-need`
- Result: passed.
5. `git diff --check`
- Result: passed.

## Test note

The first full-suite run found one stale existing fixture: `tests/test_levi_integration.py` used a fixed July 19, 2026 option expiration, which was expired when the suite ran on July 20, 2026. The fixture now generates an expiration 21 days from the test date. Runtime trading behavior was not changed.

## Blockers

None.

## Assumptions

- A validated profile stored at `${LEVI_WORKSPACE_ROOT}/users/{user_id}/PROFILE.json` is the current user-profile source for this step because authentication and database integration are explicitly excluded.
- The evidence registry is intentionally in-process for this contract-first step; durable evidence persistence and parsers belong to the next approved step.
- Non-negative account value and buying power in the validated profile count as available profile evidence. Market and chart evidence must still come from registered, traceable evidence records.
- User IDs are path-safe identifiers and may not contain path separators.
- No user workspace is initialized implicitly by the API; installation/onboarding code must explicitly call the initializer.

## Git diff summary

The implementation adds the isolated `levi/` package, one focused test module, one phase document, and this report. Existing runtime changes are limited to the single FastAPI endpoint and explicit Pydantic dependency. Configuration changes are limited to the three approved environment defaults and ignoring local workspace data. No trading, alert, risk, execution, agent, model-routing, or dashboard source file was changed.

## Exact next recommended step

Stop after this commit. After review and explicit approval, begin Phase 1, Step 2 by connecting the evidence ingestion lifecycle to durable storage and implementing the separately approved parser scope against the contracts shipped here.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,4 +273,6 @@ Part of the **JECI Group AI Infrastructure**.

> **"Markets reward discipline more than intelligence. LEVI is designed so discipline happens first."**

**Paper trade first. Not financial advice.**
**Paper trade first. Not financial advice.**

Foundation documentation: [Phase 1, Step 1](docs/PHASE_1_STEP_1.md)
30 changes: 29 additions & 1 deletion bot/status_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@
from datetime import datetime, timezone
from dataclasses import asdict

from fastapi import FastAPI
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

from levi.contracts.what_you_need import build_what_you_need
from levi.evidence.registry import EvidenceRegistry
from levi.workspace.initializer import load_user_profile

log = logging.getLogger("JECI.api")

Expand All @@ -18,6 +23,13 @@

# shared state written by the bot, read by the API
_shared: dict = {"report": None, "signals": {}, "trades": [], "blocklist": []}
evidence_registry = EvidenceRegistry()


class WhatYouNeedRequest(BaseModel):
user_id: str
request_type: str
ticker: str | None = None


@app.on_event("startup")
Expand Down Expand Up @@ -80,6 +92,22 @@ def get_trades():
return {"open_trades": _shared["trades"], "blocklist": _shared["blocklist"]}


@app.post("/api/what-you-need")
def what_you_need(request: WhatYouNeedRequest):
try:
profile = load_user_profile(request.user_id)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail="user profile not found") from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return build_what_you_need(
profile=profile,
registry=evidence_registry,
request_type=request.request_type,
ticker=request.ticker,
)


if __name__ == "__main__":
import uvicorn
uvicorn.run("bot.status_api:app", host="0.0.0.0", port=int(os.getenv("PORT", "8000")))
67 changes: 67 additions & 0 deletions docs/PHASE_1_STEP_1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# LEVI Phase 1, Step 1 — Foundation

This step adds the minimum user-specific foundation without changing LEVI's existing paper-trading, alert-mode, risk-moat, consensus, model-routing, or dashboard behavior.

## What was added

- A validated user trading profile in `levi/profiles/models.py`.
- Deterministic mode policies in `levi/modes/router.py`.
- Configurable, isolated user workspaces in `levi/workspace/initializer.py`.
- Vendor-neutral evidence models and an in-process registry in `levi/evidence/`.
- The pre-analysis `WhatYouNeed` gate in `levi/contracts/what_you_need.py`.
- `POST /api/what-you-need` in the existing FastAPI application at `bot/status_api.py`.

## Supported modes

| Mode | Instruments | Holding policy |
| --- | --- | --- |
| Day trading | Options, Polymarket | Intraday |
| Swing trading | Options, Polymarket | Multi-day to multi-week |
| Investing / holding | Stocks | Long term |

Paper trading is the default execution mode. Automatic live execution is not part of this contract.

## Profile validation

Every profile requires a user ID, display name, trading mode, instrument type, execution mode, and deterministic risk fields. Investing/holding accepts stocks only; options and Polymarket are rejected. Day and swing modes accept options or Polymarket. Loss and position limits are validated, and the public risk-per-trade default is 1%.

## User workspace

`LEVI_WORKSPACE_ROOT` controls the storage root and defaults to `./workspace` for development. Each user receives:

```text
users/{user_id}/
├── MEMORY.md
├── MOOD.md
├── BEHAVIOR.md
├── PROFILE.json
└── evidence/
```

The repository ignores the development workspace. Production deployments should set `LEVI_WORKSPACE_ROOT` to durable storage outside the application checkout. `PROFILE.json` contains only validated profile fields and no credential fields.

## Evidence contract

`EvidenceRecord` supports screenshots, CSV, Excel, PDF, tables, charts, graphs, broker statements, portfolio exports, options chains, trade journals, text notes, and live feeds. The registry supports registration, user/ticker/type/recent queries, warning listing, and ownership checks. `EvidenceParser` is the extension interface; parsing is intentionally not implemented in this step.

## API endpoint

`POST /api/what-you-need`

```json
{
"user_id": "string",
"request_type": "trade_analysis",
"ticker": "SPY"
}
```

The endpoint loads the user's validated workspace profile, resolves the mode policy, inspects registered evidence, and reports required, optional, available, and missing inputs. Missing required inputs set `can_proceed` to `false`; optional inputs never block progress. This endpoint does not launch analysis or specialist agents.

## Known exclusions

Authentication, Supabase, broker integrations or changes, live feeds, OCR and file parsing, mobile/PWA packaging, dashboard redesign, model or routing changes, fine-tuning, new strategies, and automated or Polymarket execution are excluded.

## Next step

After approval of this foundation, Phase 1, Step 2 should connect the evidence ingestion lifecycle to durable storage and parser implementations while preserving the contracts introduced here.
1 change: 1 addition & 0 deletions levi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""LEVI application foundation contracts."""
5 changes: 5 additions & 0 deletions levi/contracts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Public LEVI response contracts."""

from .what_you_need import WhatYouNeed, build_what_you_need

__all__ = ["WhatYouNeed", "build_what_you_need"]
82 changes: 82 additions & 0 deletions levi/contracts/what_you_need.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Pre-analysis evidence gate."""

from __future__ import annotations

from dataclasses import dataclass

from levi.evidence.models import EvidenceRecord, EvidenceType
from levi.evidence.registry import EvidenceRegistry
from levi.modes.router import resolve_mode_policy
from levi.profiles.models import UserTradingProfile


@dataclass
class WhatYouNeed:
request_type: str
required_items: list[str]
optional_items: list[str]
already_available: list[str]
missing_items: list[str]
can_proceed: bool


def _items_from_evidence(evidence: EvidenceRecord) -> set[str]:
items = {str(item) for item in evidence.metadata.get("evidence_items", [])}
if evidence.evidence_type is EvidenceType.LIVE_FEED:
items.update({"current spot", "timestamp", "volume"})
elif evidence.evidence_type is EvidenceType.OPTIONS_CHAIN:
items.update({"options chain", "expiration"})
elif evidence.evidence_type is EvidenceType.PORTFOLIO_EXPORT:
items.update({"open positions", "current position or desired position"})
elif evidence.evidence_type is EvidenceType.BROKER_STATEMENT:
items.update({"open positions", "current position or desired position"})
if evidence.evidence_type in {EvidenceType.CHART, EvidenceType.GRAPH}:
timeframe = (evidence.timeframe or "").lower()
chart_names = {
"5m": "5-minute chart", "5-minute": "5-minute chart",
"15m": "15-minute chart", "15-minute": "15-minute chart",
"4h": "4-hour chart", "4-hour": "4-hour chart",
"1d": "daily chart", "daily": "daily chart",
}
if timeframe in chart_names:
items.add(chart_names[timeframe])
return items


def build_what_you_need(
profile: UserTradingProfile,
registry: EvidenceRegistry,
request_type: str,
ticker: str | None = None,
) -> WhatYouNeed:
policy = resolve_mode_policy(profile)
available = {"trading mode", "risk profile"}
if profile.account_value >= 0:
available.add("account value")
if profile.buying_power >= 0:
available.add("buying power")
if ticker:
available.add("ticker")

user_records = registry.by_user(profile.user_id)
records = [
evidence for evidence in user_records
if not ticker
or not evidence.ticker_symbols
or ticker.upper() in {symbol.upper() for symbol in evidence.ticker_symbols}
]
for evidence in records:
available.update(_items_from_evidence(evidence))

required = list(policy.required_evidence)
optional = list(policy.preferred_evidence)
already = [item for item in required + optional if item in available]
missing = [item for item in required if item not in available]
return WhatYouNeed(
request_type=request_type,
required_items=required,
optional_items=optional,
already_available=already,
missing_items=missing,
can_proceed=not missing,
)
6 changes: 6 additions & 0 deletions levi/evidence/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Unified user evidence contracts."""

from .models import EvidenceParser, EvidenceRecord, EvidenceType, ParsedEvidence
from .registry import EvidenceRegistry

__all__ = ["EvidenceParser", "EvidenceRecord", "EvidenceRegistry", "EvidenceType", "ParsedEvidence"]
58 changes: 58 additions & 0 deletions levi/evidence/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Vendor-neutral evidence records and parser extension point."""

from __future__ import annotations

from datetime import datetime, timezone
from enum import Enum
from typing import Any, Protocol

from pydantic import BaseModel, Field


class EvidenceType(str, Enum):
SCREENSHOT = "screenshot"
CSV = "csv"
EXCEL = "excel"
PDF = "pdf"
TABLE = "table"
CHART = "chart"
GRAPH = "graph"
BROKER_STATEMENT = "broker_statement"
PORTFOLIO_EXPORT = "portfolio_export"
OPTIONS_CHAIN = "options_chain"
TRADE_JOURNAL = "trade_journal"
TEXT_NOTE = "text_note"
LIVE_FEED = "live_feed"


class ParsedEvidence(BaseModel):
evidence_id: str
payload: dict[str, Any] = Field(default_factory=dict)
warnings: list[str] = Field(default_factory=list)


class EvidenceRecord(BaseModel):
evidence_id: str = Field(min_length=1)
user_id: str = Field(min_length=1)
evidence_type: EvidenceType
source_name: str = Field(min_length=1)
filename: str | None = None
mime_type: str | None = None
uploaded_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
captured_at: datetime | None = None
ticker_symbols: list[str] = Field(default_factory=list)
account_name: str | None = None
timeframe: str | None = None
raw_location: str | None = None
parsed_payload: dict[str, Any] | None = None
confidence: float = Field(default=1.0, ge=0, le=1)
warnings: list[str] = Field(default_factory=list)
metadata: dict[str, Any] = Field(default_factory=dict)


class EvidenceParser(Protocol):
def supports(self, evidence: EvidenceRecord) -> bool:
...

def parse(self, evidence: EvidenceRecord) -> ParsedEvidence:
...
Loading
Loading