Imported from kaufmann-dev/portfolio-arena (
AGENTS.md). Install upstream withnpx skills add kaufmann-dev/portfolio-arena. Copyright stays with the author.
Repository Instructions
Portfolio Arena: a FastAPI + SQLAlchemy backend (backend/, PostgreSQL) serving a Svelte 5
(runes) + Vite + TS SPA (frontend/). Python 3.12 (.python-version), Node 24 (.nvmrc).
Build and Verification
- One venv at the repo root:
python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt. - Backend tests:
cd backend && ../.venv/bin/python -m pytest. Tests spin up a throwaway Postgres directly with rootless Podman, or setTEST_DATABASE_URLto reuse a database. Massive is stubbed — no test hits the network. - Lint/format (ruff, config in
backend/pyproject.toml, line length 110):cd backend && ../.venv/bin/ruff check . && ../.venv/bin/ruff format .. - Frontend:
cd frontend && npm run test && npm run check && npm run build(unit tests, svelte-check, and production build). - Running the backend needs
DATABASE_URL,MASSIVE_API_KEY,ARENA_PUBLIC_URL, and the four requiredARENA_OIDC_*variables (seeREADME.mdDevelopment).
Project Structure
-
backend/app/services/valuation.pyis the deterministic correctness core: pure functions, no wall-clock reads (callers pass every boundary), same inputs → identical output. NAVs are never stored; every request recomputes from allocations + cached price series. Keep it pure. -
API routers:
backend/app/api/public.py(read-only, no auth, rate-limited),backend/app/api/admin.py(writes, guarded byDepends(require_admin)),auth.py,keys.py(API-key management, browser-admin-session only). Response shaping is shared inbackend/app/services/serialize.py. -
Allocation and core admin write/integrity logic lives in
backend/app/services/admin_ops.py(raisingAdminOpError); the admin router and MCP tools are thin callers. Evaluator configuration, scheduling, queue, lease, and run lifecycle logic lives inbackend/app/services/evaluator.py. Put rules in the relevant service, not in a router. Keep the shared queue lock and database constraint enforcing one queued/running/cancellation-pending evaluation per portfolio across workers. Check for an existing decision before scheduling and claiming; all decision outcomes satisfy their session, and scheduled retries retain their original session. -
Reset deletes all portfolio decisions and evaluation runs while preserving configuration. Individual history deletion removes the decision and its run together, including locked results. Use the shared lifecycle lock order so deleted runs cannot recreate decisions through late submissions.
-
Position handoff notes are public and shown through collapsed disclosures.
-
Admin-only holding
entry_price/current_pricefields are gated behind theadmin=Trueflag inservices/serialize.py. Never expose them frompublic.py. -
backend/app/evaluator/runs the integrated Codex, Muse Code, OpenCode, and Antigravity workers. Each harness has separate authentication and health; the concurrency setting applies separately to each harness across its workers. Muse checks its authenticated catalog for readiness without importing model records. Model definitions and harness capabilities are administrator-managed for every harness. OpenCode models are manually configured withprovider/modelexecution IDs and optional custom variants; native discovery checks readiness and execution support without importing model records. Harness definitions declare fixed or custom reasoning-effort controls. OpenCode keeps native provider configuration and login in its own XDG directories underOPENCODE_HOME; preserve provider keys for OpenCode and filter credentials inside the individual Codex and Muse adapters. Antigravity uses native stream-json stdin/stdout with a JSON schema and isolated--gemini_dirunderAGY_HOME. Send one user message per attempt, preserve native login, allow only research tools, and require one completed result with validatedstructured_output; print timeouts can reportSUCCESSwith exit code zero and must still fail. Model IDs are manually configured using native IDs, including base IDs with separate low/medium/high effort. Discovery checks readiness without requiring exact catalog membership or importing records; native execution validates model selection. Effort-suffixed IDs must match the agent's effort. Its generation schema omits the nullable reason enum unsupported by Gemini; keep the shared proposal schema and strict finalProposalvalidation unchanged. Fence private worker control, submit, and fail requests with the claimedattempt_count. Workers exclude database and OIDC secrets from their deployment credentials and talk to private/api/internal/evaluatorroutes using an in-memory bearer token generated bybackend/app/production.py. Keep queue claims and atomic submissions private; public MCP tools control evaluator settings and actions but never execute worker leases. -
MCP server:
backend/app/mcp_server/(FastMCP, mounted at/mcpinmain.py). Register synchronous database tools withthreaded_toolto keep them off the ASGI event loop. It exposes the operational app surface as API-key-authenticated tools (Authorization: Bearer <key>orX-API-Key, no anonymous access). API-key management and prompt revision history/restore remain browser-admin-only. Unused prompts may be deleted unless a recorded run references a revision. Settings prompt history and restore are also browser-admin-only. Shared prompt saves (including MCP) append immutableSettingPromptVersionrows only for changed text; restores append a new revision. LockSettingrows in key order and commit active values and snapshots together. Numeric sizing settings are not versioned. Seed v1 from the frozen pre-September-14 Git baseline and v2 from saved values; never overwrite existing history. Imported timestamps are import times, not historical edit times. Other tools serialize withadmin=Truesince the endpoint is key-gated. Keys are stored as SHA-256 hashes in theapi_keystable (security.pyhelpers). -
ArenaVersionscopes comparisons and independently gates evaluation. Visibility and price refresh do not depend on evaluation being enabled. Paused versions cap managed valuation and its benchmark at one trading day after the last effective decision, at the portfolio execution boundary. Enabled versions have no cap; resuming includes all returns during the pause. Global/portfolio switches do not affect this cap, and rebuilt horizons are unchanged. Portfolios have an open/close execution boundary that locks permanently at the first decision. API market boundaries are{timestamp, phase}values; NAV points addnav. Keep ordinary audit timestamps as strings. -
Both modes submit selected weights totaling
min(100, count × maximum position weight); unused allocation follows the direction-matched SPY reference outside ticker limits. Empty decisions are successful abstentions, require explanation, and never trigger retries. Managed abstentions replace existing holdings; rebuilt abstentions remain zero-alpha observations without closing older cohorts. Keep the automatic reference separate from selected SPY positions in serialized holdings. -
Rebuilt analytics use forty half-session horizons H0.5–H20 at 100% exposure with per-portfolio tuning. The explicit optimization objective selects from the cached policy grid independently of table sorting. There are no Meta, archive, transaction-cost or common-policy paths.
Database and Migrations
backend/app/models.pymirrors the Alembic migrations inbackend/alembic/versions/. A schema change requires both a model edit and a new numbered migration (e.g.0004_*.py, withdown_revision= the previous revision id). Migrations run on FastAPI startup via the application lifespan; do not rely on model changes alone.
Testing
- Tests live in
backend/tests/(pytest). Shared fixtures inconftest.py:client,admin_headers,sample_agent,sample_prompt,sample_portfolio; each test truncates and reseeds all tables.stub_massiveprovides a fixed symbol universe (SPY, RSP, AAPL, MSFT, EURUSD=X, GC=F, BTC-USD) with deterministic prices — use those symbols. - Use
backdate_allocation(backend/tests/util.py) to make an allocation locked/valued, since real allocations can never be backdated through the API.
Frontend Conventions
- API response types in
frontend/src/lib/api/types.tsare hand-maintained to mirror the backend serializers; update them whenever a serializer's shape changes. - Use the existing
apiJson/postJson/patchJson/delhelpers infrontend/src/lib/api/client.ts; format withlib/format.tshelpers. - Format Svelte/TS with Prettier:
cd frontend && npm run format(check-only:npm run format:check). - When writing or refactoring
.svelte/.svelte.tsfiles, use thesvelte-code-writerandsvelte-core-bestpracticesskills and thesvelteMCP (list-sections,get-documentation, andsvelte-autofixer— run the autofixer until clean before finishing).
Svelte 5 rune conventions
This is a Svelte 5 runes project. Prefer fine-grained reactivity over effects.
$props()for inputs;$stateonly for values that drive the template/a$derived/an effect. Use$state.rawfor large objects or API responses that are reassigned wholesale, not mutated.$derived(or$derived.byfor multi-line) for anything computed — never an$effectthat writes derived state.- Avoid
$effect. Reach for it only to sync with a genuinely external, non-Svelte concern (e.g. writingdata-themetodocument). React to changes at the event boundary (onclick,onValueChange) or with getter/setterbind:value={() => ..., (v) => ...}instead. - Reusable markup: snippets +
{@render ...}. Keyed{#each}with stable ids — never the index. - New code only: no
export let,$:,<slot>,<svelte:component>/<svelte:self>,use:action, oron:event directives. Use$props,$derived, snippets,{@render},<Self>imports,{@attach}, andonclick-style handlers.
Git
- Commit subjects follow Conventional Commits (
feat(scope): …,fix,refactor,chore,docs).