More OCaml for Institutional Quantitative Trading
Part II: Building Our First Trading Domain Types
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.
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:
priceis a floatquantityis 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:
Limittells us the order typePrice pxgives 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 neededSome (Approved order): order passed risSome (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
sideisBuyorSell. - A
quantitywraps an integer. - A
Limitorder carries a price. - A
Marketorder 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