Metadata-Version: 2.4
Name: oak-domain-investments
Version: 0.3.0
Summary: Investments domain plugin for the OakQuant timber substrate (watchlist capability migrated from grove core; more to follow).
Author-email: Pumulo Sikaneta <pumulo@gmail.com>
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: timber-common>=0.6.16
Provides-Extra: dev
Requires-Dist: fastapi>=0.110; extra == 'dev'
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# oak-domain-investments

The **investments** domain plugin for OakQuant's [Timber](https://pypi.org/project/timber-common/) substrate — discoverable, 3-layer, migrating in **one capability at a time**.

[![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](./LICENSE)
[![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/)

## What it is

Investment logic is moving **out of OakQuant's grove/timber core** and **into this package**, deliberately one capability at a time so each move is small, isolated, and low-risk. This README documents the **current** state and is expected to grow as more capabilities land.

The package is a discoverable plugin: it advertises itself through entry points and registers into the host's shared registries (Timber's model/service registries, grove's app, acorn's tool registry) — never by editing the core libraries. See the `oak-domain-plugins` skill for the wider plugin shape.

**First migrated capability: the watchlist** (capability #1). It exercises all three layers of the plugin system end to end.

## The 3-layer shape

A single installed package plugs into three OakQuant layers, one entry point per group. Each host discovers only its own group and imports only that entry point's module, so the timber layer never pulls in grove/FastAPI or acorn.

| Layer  | Entry-point group | Class                      | Contributes |
|--------|-------------------|----------------------------|-------------|
| timber | `timber.domains`  | `InvestmentsDomain`        | `WatchlistItem` ORM model + `WatchlistService`, registered at init (Step 8.5) into Timber's model/service registries |
| grove  | `grove.domains`   | `InvestmentsGroveDomain`   | The watchlist HTTP router, mounted on grove's app at startup |
| acorn  | `acorn.domains`   | `InvestmentsAcornDomain`   | A `get_watchlist` agent tool, registered into acorn's Oracle at startup |

The grove and acorn plugins delegate to the **same** `WatchlistService` that the timber layer registers (reached through Timber's `service_registry.domain("investments")`), so there is one implementation behind all three surfaces.

## Capability #1: the watchlist

Symbols a user tracks without necessarily owning them.

### Model — `WatchlistItem` (`oak_domain_investments/models.py`)

A `common.models.base.Base` model (`TimestampMixin`) for table `watchlist_items`, migrated field-for-field from grove's `config/models/watchlist_models.yaml` so existing rows keep working:

- `id` (UUID str PK), `user_id` (FK → `users.id`), `symbol` (uppercase ticker), `name`, `notes`
- `alert_above` / `alert_below` (price alert thresholds)
- `status` — `active` | `removed` (the service soft-deletes via status)
- Unique index on `(user_id, symbol)`; index on `(user_id, status)`

Exported as `DOMAIN_MODELS` and registered before Step 9 `create_all`, so the table exists.

### Service — `WatchlistService` (`oak_domain_investments/services/watchlist_service.py`)

Takes its DB from the injected `ctx.db`; enriches with Timber's `stock_data_service` on a best-effort basis. The per-tier `max_watchlist` limit is **not** here — it's a grove-billing concern enforced by the grove router.

- `get_watchlist(user_id, include_prices=True)` — active items, with optional current prices + `alert_triggered` state
- `add_to_watchlist(user_id, symbol, name=None, notes=None)` — idempotent on active `(user, symbol)`; resolves company name best-effort
- `remove_from_watchlist(item_id)` — soft-delete (`status="removed"`)
- `update_alerts(item_id, alert_above=None, alert_below=None)` — set/clear alerts (a non-positive value clears)

### Grove HTTP surface (`oak_domain_investments/grove_plugin.py`)

`InvestmentsGroveDomain` mounts a router that delegates to `WatchlistService` and applies the best-effort `max_watchlist` tier gate on add:

- `GET    {prefix}` — list the user's watchlist
- `POST   {prefix}` — add a symbol
- `DELETE {prefix}/{item_id}` — remove (soft-delete)
- `PUT    {prefix}/{item_id}/alerts` — set/clear price alerts

**Phase 1** mounts at `/investments/watchlist` (a non-conflicting path while grove core's `/api/v3/watchlist` still exists). **Phase 2** — after the core route is removed — repoints this to the canonical `/api/v3/watchlist`.

### Acorn agent tool (`oak_domain_investments/acorn_plugin.py`)

`InvestmentsAcornDomain` contributes a `get_watchlist(user_id)` tool (`RiskTier.READ`) to acorn's Oracle. It reads `WatchlistItem` directly through Timber's `db_service` (a synchronous, user-scoped read, no HTTP round-trip) and returns the active watchlist as markdown.

### Migration status

`InvestmentsInfoService` (`services/info_service.py`) reports what has migrated: `MIGRATED_CAPABILITIES = ["watchlist"]`. This list grows as portfolio, thresholds, and other capabilities move in. Investment logic that hasn't migrated yet still lives in timber core (`common/services/data_fetcher/`, `data_processor/portfolio_metrics.py`, `vendors/plaid_service.py`).

## How discovery works

Each host loads only its own entry-point group:

- `timber.domains` → `oak_domain_investments:InvestmentsDomain`
- `grove.domains`  → `oak_domain_investments.grove_plugin:InvestmentsGroveDomain`
- `acorn.domains`  → `oak_domain_investments.acorn_plugin:InvestmentsAcornDomain`

Once the package is installed, discovery is automatic via these entry points. For local development against checkouts, each host also honors an env-var fallback (e.g. `TIMBER_DOMAIN_PLUGINS=oak_domain_investments:InvestmentsDomain`) so the plugin loads without a reinstall.

## Install

```bash
pip install oak-domain-investments
```

Requires Python 3.11+ and `timber-common>=0.6.16`. The import package is `oak_domain_investments`.

Develop / test against a sibling timber checkout:

```bash
# from the repo root, with ../timber on the path
PYTHONPATH=../timber:. TIMBER_DOMAIN_PLUGINS=oak_domain_investments:InvestmentsDomain \
  python3 -m pytest tests/ -q
```

## License

Apache-2.0. See [LICENSE](./LICENSE).
