Skip to main content

README.md

# RiskEngine

A real-time portfolio risk engine built on OTP. It ingests trades, tracks
per-portfolio cash exposure, volatility, and drawdown, and broadcasts risk
snapshots and threshold alerts over `Phoenix.PubSub`.


https://github.com/user-attachments/assets/3b64e6c2-2890-4e60-a704-0cc567862154


## Features

- **Per-portfolio processes** — each portfolio is an isolated `GenServer`,
  started on demand and supervised independently, so one crashing portfolio
  never affects another.
- **Risk math** — running cash exposure, trade-notional volatility, and
  drawdown from peak exposure, recomputed after every trade
  (`RiskEngine.RiskCalculator`).
- **Live broadcasting** — risk snapshots and threshold alerts are published
  over `Phoenix.PubSub` (`RiskEngine.RiskBroadcaster`) for any subscriber to
  consume.
- **Built-in trade simulator** — `RiskEngine.Simulator` generates random
  trades out of the box, so the system is observable without wiring up a
  real feed.

## Installation

Add `risk_engine` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:risk_engine, "~> 0.1.0"}
  ]
end
```

Then fetch dependencies:

```console
mix deps.get
```

## Usage

Submit a trade — the owning portfolio process is started automatically the
first time its id is seen:

```elixir
trade = %{portfolio_id: "acct_1", symbol: "AAPL", side: :buy, qty: 10, price: 189.32}

RiskEngine.TradeIngestion.submit(trade)
#=> :ok

RiskEngine.Portfolio.get_state("acct_1")
#=> %{id: "acct_1", cash_exposure: 1893.2, peak_exposure: 1893.2, ...}
```

Subscribe to risk updates and alerts over PubSub:

```elixir
Phoenix.PubSub.subscribe(RiskEngine.PubSub, "portfolio:risk")
Phoenix.PubSub.subscribe(RiskEngine.PubSub, "alerts")

receive do
  {:risk_update, snapshot} -> IO.inspect(snapshot)
  {:alert, alert} -> IO.inspect(alert)
end
```

## Configuration

Risk thresholds, topics, and the built-in simulator are configurable per
environment in `config/config.exs`:

| Config key                                    | Default                                   | Purpose                                    |
| ---------------------------------------------- | ------------------------------------------ | ------------------------------------------- |
| `RiskEngine.RiskCalculator, :window`           | `10`                                       | Number of recent trades used for volatility |
| `RiskEngine.RiskBroadcaster, :risk_topic`      | `"portfolio:risk"`                         | PubSub topic for risk snapshots             |
| `RiskEngine.RiskBroadcaster, :alerts_topic`    | `"alerts"`                                 | PubSub topic for threshold alerts           |
| `RiskEngine.RiskBroadcaster, :drawdown_threshold`   | `20.0`                                | Drawdown % that triggers an alert           |
| `RiskEngine.RiskBroadcaster, :volatility_threshold` | `5_000.0`                             | Volatility that triggers an alert           |
| `RiskEngine.Simulator, :tick_interval`         | `1_000` (ms)                               | How often the simulator submits a trade     |
| `RiskEngine.Simulator, :portfolio_ids`         | `["acct_1", "acct_2", "acct_3"]`           | Portfolio ids the simulator trades against  |
| `RiskEngine.Simulator, :symbols`               | `["AAPL", "TSLA", "GOOG", "MSFT"]`         | Symbols the simulator trades                |

## Project structure

```
risk_engine/
├── config/
│   └── config.exs                   # Runtime config: thresholds, topics, simulator
├── lib/
│   ├── risk_engine.ex
│   └── risk_engine/
│       ├── application.ex           # Supervision tree
│       ├── portfolio.ex             # Client API for a portfolio process
│       ├── portfolio/
│       │   └── server.ex            # GenServer callbacks for portfolio state
│       ├── portfolio_supervisor.ex  # DynamicSupervisor, one process per portfolio
│       ├── risk_calculator.ex       # Pure volatility/drawdown math
│       ├── risk_broadcaster.ex      # PubSub topics, thresholds, broadcasting
│       ├── simulator.ex             # Random trade generator for dev/demo
│       └── trade_ingestion.ex       # Validates and ingests trades
├── test/
│   ├── risk_engine/                 # One test file per lib module
│   └── test_helper.exs
├── CHANGELOG.md
├── LICENSE
├── mix.exs
└── README.md
```

## Development

Clone the repo and fetch dependencies:

```console
git clone https://github.com/Null-logic-0/risk_engine.git
cd risk_engine
mix deps.get
```

### Running tests

```console
mix test
```

### Running docs

Documentation is generated with [ExDoc](https://github.com/elixir-lang/ex_doc):

```console
mix docs
```

This builds HTML documentation into `doc/index.html` — open it directly in
a browser, or serve it locally:

```console
open doc/index.html
```

Once published, the same docs are available at
<https://hexdocs.pm/risk_engine>.

### Formatting

```console
mix format --check-formatted
```

## Contributing

Contributions are welcome. To submit a change:

1. Fork the repository and create a topic branch off `main`.
2. Make your change, adding or updating tests under `test/` to cover it.
3. Run the checks locally before opening a PR:

   ```console
   mix format --check-formatted
   mix compile --warnings-as-errors
   mix test
   ```

4. Open a pull request describing the change and the reasoning behind it.

Please keep pull requests focused — prefer several small, reviewable PRs
over one large one. Bug reports and feature requests are welcome via
[GitHub Issues](https://github.com/Null-logic-0/risk_engine/issues).

## License

MIT — see [LICENSE](LICENSE) for details.