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

Installation

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

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

Then fetch dependencies:

mix deps.get

Usage

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

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:

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 keyDefaultPurpose
RiskEngine.RiskCalculator, :window10Number 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_threshold20.0Drawdown % that triggers an alert
RiskEngine.RiskBroadcaster, :volatility_threshold5_000.0Volatility that triggers an alert
RiskEngine.Simulator, :tick_interval1_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:

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

Running tests

mix test

Running docs

Documentation is generated with ExDoc:

mix docs

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

open doc/index.html

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

Formatting

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:

    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.

License

MIT — see LICENSE for details.