Here I've translated the example to haskell:

data Exchange = Exchange { bic :: String, name :: String }
data Security = Security { description :: String, exchange :: Exchange, isin :: String }
data Stock = Stock { security :: Security }
data Bond = Bond { security :: Security, expiry :: EpochTime }
data Trade = Trade { price :: Decimal, quantity :: Decimal, security :: Security }

Now to add Option:

data Option = Option { security :: Security, call :: Bool, lotSize :: Decimal, maturity :: EpochTime, strikePrice :: Decimal }

And in the example, the problem is that the Option uses Security, which has an isin, which doesn't make sense for Option. In haskell, this is a sort of problem which is typically fixed by adjusting the data types. There are many
ways they could be changed, some will model the domain better than others. Let's just make the same quick fix used in the example, of allowing isin to not be set:

data Security = Security { description :: String, exchange :: Exchange, isin :: Maybe String }

This means that isin is Nothing or Just a String. As soon as this change is made, every place in the program that directly accessed the isin will fail to compile. Fixing the compilation errors will involve adding a case to handle
isin-less Securities.

- foo (Security { isin = i }) =
+ foo (Security { isin = Just i }) = ...
+ foo (Security { isin = Nothing }) = ...

The code does become somewhat ugly with these cases, but you know every case has been covered, and that it will work.

Maybe later it's decided to go back and fix it to use the separation between physical and derivative securities that was originally considered but not done due to lack of time. It could then look like this:

data Security = PhysicalSecurity { description :: String, exchange :: Exchange, isin :: String }
| DerivativeSecurity { description :: String, exchange :: Exchange }

Again this type change would drive a pass through the code, fixing it up to compile.

foo (PhysicalSecurity { isin = i }) = ...
foo (DerivativeSecurity {}) = ...

Again you'll know when you're done because the program will successfully compile. In this case, splitting the data type seems to have led to better,
clearer code. It might be worthwhile to factor out a helper type to simplify the Security type:

data SecurityBase = SecurityBase { description :: String, exchange :: Exchange }
data Security = PhysicalSecurity { base :: SecurityBase, isin :: String }
| DerivativeSecurity { base :: SecurityBase }

Although you may find this complicates other things as you "follow the types"
and change the code to match. There are surely other approaches; so far this has stuck with simple data types, but typeclasses could also be used. You may want to constrain Bonds to using a PhysicalSecurity, and Options to using a DerivativeSecurity, and there are various ways that could be enforced. And so on.

What was surprising to me coming to haskell from a background in loosely typed languages (and lowlevel langs like C) is that the types are not a straightjacket that is set in stone from the start, but ebb and flow as you refine your understanding of the problem domain. What well chosen types in haskell do constrain is the mistakes you want to be prevented from making. These days if I find myself repeatedly making a mistake in my code, I adjust the types to prevent that sort of mistake in the future.

---

Side note: The above code will not compile as written, because it exposes an annoying problem in haskell's record syntax. There are several fields named "security" that conflict with one-another. This is typically dealt with by using
ugly field names (stockSecurity, bondSecurity, tradeSecurity, optionSecurity), or more advanced things like lenses, or by putting the data types in separate modules and using module namespacing.