← Back to list

More OCaml for Institutional Quantitative Trading

Part II: Building Our First Trading Domain Types

Sofien Kaabar, CFA · 2026-06-09 18:46 · 0 claps · 9.2 min read paywalled
#data-science #cryptocurrency #coding #ocaml
Open on Medium ↗
Wiki topics: ML · Machine Learning CRY · Crypto & Web3 💻 · Programming 🔬 · Science · General

More OCaml for Institutional Quantitative Trading

Part II: Building Our First Trading Domain Types

In Part I, we made the case for OCaml in institutional quantitative trading.

The basic argument was simple: trading systems are full of states, rules, and edge cases. OCaml gives us a way to model those things clearly, then lets the compiler help us keep the model honest.

Now we are going to get more practical.

This article is not a full OCaml tutorial. It is a guided first pass through the parts of OCaml that matter most when you are building trading software:

  • Values
  • Functions
  • Types
  • Variants
  • Records
  • Pattern matching
  • Options
  • Simple domain modeling

By the end, we will have the beginning of a small trading-domain model: sides, prices, quantities, orders, signals, and risk decisions.

Nothing here is production-ready. That is not the point yet.

The point is to start thinking in OCaml.

Start With Values

OCaml uses let to bind names to values.

let max_position = 1000
let symbol = "AAPL"
let risk_enabled = true

OCaml infers the types automatically.

So max_position is an int, symbol is a string, and risk_enabled is a bool.

You can add explicit type annotations when they help readability:

let max_position : int = 1000
let symbol : string = "AAPL"

Most of the time, you do not need to write the types everywhere. OCaml will infer them.

That is one of the first pleasant surprises for people coming from languages like Java or C++.

OCaml is statically typed, but it does not usually feel verbose.

A Small But Important Detail: Integers and Floats

OCaml keeps integers and floats separate.

Integer addition uses +.

let shares = 100 + 50

Float addition uses +..

let price = 101.25 +. 0.10

Same for subtraction, multiplication, and division:

let a = 10 - 3
let b = 10.0 -. 3.0

let c = 10 * 3
let d = 10.0 *. 3.0
let e = 10 / 3
let f = 10.0 /. 3.0

At first this feels fussy.

In finance, it is useful.

You do not want the language casually mixing counts, prices, ratios, and floating-point values without making the conversion explicit.

For example:

let notional price quantity =
  price *. float_of_int quantity

Here quantity is an integer, but we convert it to a float before multiplying by price.

That tiny bit of explicitness is a feature, not a tax.

🚨🚨Get your Quant Atlas Free trial (no credit card required).

Sign up takes ~9 seconds before you have unconditional 10-day access to 100+ market forecasts.

Start Your Free Trial

Functions Are Small and Direct

A function in OCaml can be very compact:

let double x =
  x * 2

let notional price quantity =
  price *. float_of_int quantity

A trading example:

This function takes a price and a quantity, then returns the notional value.

let trade_value = notional 101.25 200

The result is:

20250.0

You can also annotate the function:

let notional (price : float) (quantity : int) : float =
  price *. float_of_int quantity

That says:

  • price is a float
  • quantity is an int
  • the result is a float

In a small example, the annotation is optional. In a trading codebase, annotations can be helpful at important boundaries: strategy inputs, risk checks, execution instructions, and external data interfaces.

Now Let’s Stop Passing Raw Floats Around

The function above works, but it is too loose.

let notional price quantity =
  price *. float_of_int quantity

What is price?

A last price? A mid price? A limit price? A mark? A fair value?

What is quantity?

Shares? Contracts? Lots? Child order size? Target position?

In early research code, raw numbers are fine. In production-facing trading logic, raw numbers can become dangerous.

So let’s introduce small domain types.

type price = Price of float
type quantity = Quantity of int

Now we can rewrite notional:

let notional (Price px) (Quantity qty) =
  px *. float_of_int qty

This looks a little strange if you are new to OCaml.

Price and Quantity are constructors. They wrap raw values in domain meaning.

So instead of calling:

notional 101.25 200

We call:

notional (Price 101.25) (Quantity 200)

This is more explicit.

It says: this float is being used as a price, and this integer is being used as a quantity.

That matters when code grows.

Modeling Order Side

Let’s define a simple order side.

type side =
  | Buy
  | Sell

This means a value of type side can only be Buy or Sell.

No "BUY", "buy", "B", "bid", or typo hiding in a string.

Now we can write a function over side:

let side_to_string side =
  match side with
  | Buy -> "buy"
  | Sell -> "sell"

The match expression is pattern matching.

It says: look at this value, then handle each possible shape.

Here there are only two cases. If we forget one, the compiler can warn us.

That is one of OCaml’s great habits: it makes missing cases visible.

Modeling Signals

Trading strategies usually produce some kind of intent.

At the simplest level, a strategy might say:

type signal =
  | Long
  | Short
  | Flat

Now we can convert a signal into a target position.

type position = Position of int
let target_position signal =
  match signal with
  | Long -> Position 100
  | Short -> Position (-100)
  | Flat -> Position 0

This is intentionally simple.

A real target position function might depend on volatility, liquidity, forecast strength, capital, drawdown, borrow, risk limits, and portfolio exposure.

But the shape is already useful.

  • A signal is not a random string.
  • A position is not a naked integer.
  • The conversion is explicit.

That is the OCaml style.

Records: Grouping Related Data

Variants are good for choices. Records are good for grouped fields.

An order request has several pieces of information:

  • Symbol
  • Side
  • Quantity
  • Order type
  • Venue
  • Time in force

Let’s model that.

type symbol = Symbol of string
type venue = Venue of string

type order_type =
  | Market
  | Limit of price
type time_in_force =
  | Day
  | Ioc
  | Fok
type order_request =
  {
    symbol : symbol;
    side : side;
    quantity : quantity;
    order_type : order_type;
    venue : venue;
    time_in_force : time_in_force;
  }

Now we can create an order request:

let order =
  {
    symbol = Symbol "AAPL";
    side = Buy;
    quantity = Quantity 100;
    order_type = Limit (Price 101.25);
    venue = Venue "NASDAQ";
    time_in_force = Day;
  }

This is much clearer than passing six arguments into a function and hoping nobody swaps the order.

Compare this:

submit_order "AAPL" "BUY" 100 "LIMIT" 101.25 "NASDAQ"

With this:

submit_order order

The record tells us what each field means.

That matters when the code is read six months later by someone who did not write it.

That someone may be you.

Pattern Matching on Order Type

Now let’s write a function that describes an order type.

let describe_order_type order_type =
  match order_type with
  | Market -> "market order"
  | Limit (Price px) ->
      "limit order at " ^ string_of_float px

Notice this case:

| Limit (Price px) ->

We are unpacking two layers:

  • Limit tells us the order type
  • Price px gives us the raw float inside the price

This is one reason OCaml feels expressive once it clicks. The pattern itself mirrors the structure of the data.

Options: Handling Missing Data Without Null

Market data is often missing.

  • A last price may not exist.
  • A bid may be unavailable.
  • A feed may be stale.
  • A reference price may fail to load.

Many languages use null for this. OCaml does not. OCaml uses option.

Conceptually, an option is:

type 'a option =
  | None
  | Some of 'a

That means a value is either absent:

None

Or present:

Some value

For example:

let last_price : price option = Some (Price 101.25)
let missing_price : price option = None

Now let’s write a simple signal function.

let signal_from_price maybe_price =
  match maybe_price with
  | None -> Flat
  | Some (Price px) ->
      if px > 100.0 then Long else Flat

This function cannot accidentally ignore missing data.

The type forces us to handle it.

That is exactly the kind of discipline we want in trading software.

A More Trading-Shaped Price State

Sometimes option is enough. Sometimes it is too vague.

For market data, missing is not the only problem. A price can also be stale, crossed, delayed, indicative, or invalid.

So we can model that directly.

type price_state =
  | Valid_price of price
  | Missing_price
  | Stale_price
  | Invalid_price of string

Now a strategy can respond properly:

let signal_from_price_state price_state =
  match price_state with
  | Valid_price (Price px) ->
      if px > 100.0 then Long else Flat
  | Missing_price -> Flat
  | Stale_price -> Flat
  | Invalid_price _reason -> Flat

This is not glamorous code.

It is defensive, boring, and clear.

That is a compliment.

A lot of institutional trading infrastructure needs more boring clarity and fewer clever shortcuts.

Risk Decisions Should Be Explicit

A risk check should not quietly return a boolean and make everyone guess what happened.

This is too thin:

let check_risk order =
  true

What does false mean?

  • Position too large?
  • Symbol restricted?
  • Venue disabled?
  • Price too far from reference?
  • Strategy shut down?
  • Missing borrow?
  • Portfolio exposure breach?

Let’s create a better type.

type risk_rejection =
  | Position_limit_exceeded
  | Restricted_symbol
  | Venue_disabled
  | Price_too_far_from_reference
  | Missing_reference_price

type risk_decision =
  | Approved of order_request
  | Rejected of risk_rejection

Now a risk check has to say what happened.

let check_order_size max_qty order =
  let (Quantity qty) = order.quantity in
  if qty <= max_qty then
    Approved order
  else
    Rejected Position_limit_exceeded

This is simple, but already much better than true or false.

The result carries meaning.

Combining Signal, Order Construction, and Risk

Let’s connect a few pieces.

First, convert a signal into an order request.

let order_from_signal symbol venue signal =
  match signal with
  | Long ->
      Some
        {
          symbol;
          side = Buy;
          quantity = Quantity 100;
          order_type = Market;
          venue;
          time_in_force = Day;
        }
  | Short ->
      Some
        {
          symbol;
          side = Sell;
          quantity = Quantity 100;
          order_type = Market;
          venue;
          time_in_force = Day;
        }
  | Flat ->
      None

The return type is:

order_request option

Why?

Because Flat does not create an order.

That is the point of option: absence is part of the type.

Now we can apply risk only when there is an order.

let decide_order symbol venue price_state =
  let signal = signal_from_price_state price_state in
  match order_from_signal symbol venue signal with
  | None -> None
  | Some order -> Some (check_order_size 1000 order)

This function returns:

risk_decision option

That means:

  • None: no order was needed
  • Some (Approved order): order passed ris
  • Some (Rejected reason): order was blocked

That is a lot of business meaning in a small amount of code.

And because it is typed, the compiler helps keep that meaning consistent.

Making Output Human-Readable

Eventually, we need to report decisions.

Let’s turn risk rejections into strings.

let string_of_risk_rejection rejection =
  match rejection with
  | Position_limit_exceeded -> "position limit exceeded"
  | Restricted_symbol -> "restricted symbol"
  | Venue_disabled -> "venue disabled"
  | Price_too_far_from_reference -> "price too far from reference"
  | Missing_reference_price -> "missing reference price"

Now risk decisions:

let string_of_risk_decision decision =
  match decision with
  | Approved _order -> "approved"
  | Rejected rejection ->
      "rejected: " ^ string_of_risk_rejection rejection

And the full decision:

let string_of_decision decision =
  match decision with
  | None -> "no order"
  | Some risk_decision -> string_of_risk_decision risk_decision

This may feel repetitive, but it is useful repetition.

Every possible case is visible. Every business outcome gets handled.

If we later add a new rejection reason, OCaml can tell us which functions need updating.

That is a powerful maintenance tool.

What We Have So Far

We now have a tiny typed trading model:

type price = Price of float
type quantity = Quantity of int
type symbol = Symbol of string
type venue = Venue of string

type side =
  | Buy
  | Sell
type signal =
  | Long
  | Short
  | Flat
type position = Position of int
type order_type =
  | Market
  | Limit of price
type time_in_force =
  | Day
  | Ioc
  | Fok
type order_request =
  {
    symbol : symbol;
    side : side;
    quantity : quantity;
    order_type : order_type;
    venue : venue;
    time_in_force : time_in_force;
  }
type price_state =
  | Valid_price of price
  | Missing_price
  | Stale_price
  | Invalid_price of string
type risk_rejection =
  | Position_limit_exceeded
  | Restricted_symbol
  | Venue_disabled
  | Price_too_far_from_reference
  | Missing_reference_price
type risk_decision =
  | Approved of order_request
  | Rejected of risk_rejection

This is not much code.

But it already gives us a strong foundation.

We have separated:

  • Raw values from domain concepts
  • Valid prices from missing or stale prices
  • Signals from orders
  • Orders from risk decisions
  • Approved outcomes from rejected outcomes

That separation is the beginning of good trading-system design.

A Python Comparison

In Python, we might model an order like this:

order = {
    "symbol": "AAPL",
    "side": "BUY",
    "quantity": 100,
    "order_type": "LIMIT",
    "price": 101.25,
    "venue": "NASDAQ",
    "time_in_force": "DAY",
}

This is quick and flexible.

But flexibility cuts both ways.

These are all possible:

order["side"] = "BYY"
order["quantity"] = "100"
order["order_type"] = "MARKET"
order["price"] = None

Python will allow them unless we add validation, tests, schemas, or runtime checks.

In OCaml, many of those mistakes are harder to express in the first place.

  • A side is Buy or Sell.
  • A quantity wraps an integer.
  • A Limit order carries a price.
  • A Market order does not.
  • A missing price has to be handled explicitly.

That does not make OCaml automatically better for every task.

It does make it very attractive when correctness and maintainability matter.

The Habit to Build

When writing OCaml, try asking this question before writing functions:

What are the valid states of this domain?

Then write the types.

Only after that, write the functions.

For example, instead of starting with:

let handle_order order = ...

Start with:

type order_status =
  | New
  | Partially_filled of quantity
  | Filled
  | Cancelled
  | Rejected of string

Then write:

let handle_order_status status =
  match status with
  | New -> ...
  | Partially_filled qty -> ...
  | Filled -> ...
  | Cancelled -> ...
  | Rejected reason -> ...

This is the shift. You are modeling a domain.

For institutional quantitative trading, that domain is rich, messy, and full of expensive edge cases. So model it carefully.


메타데이터
post_id
e0d1c8d7601f
slug
more-ocaml-for-institutional-quantitative-trading-e0d1c8d7601f
url
https://medium.com/@kaabar-sofien/more-ocaml-for-institutional-quantitative-trading-e0d1c8d7601f
canonical_url
https://medium.com/@kaabar-sofien/more-ocaml-for-institutional-quantitative-trading-e0d1c8d7601f
author_url
https://medium.com/@kaabar-sofien
status
ok
fetched_at
2026-07-10 03:40:03