# 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.