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 simulatorRiskEngine.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:

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.