{"id":15950530,"url":"https://github.com/buntec/derifree","last_synced_at":"2025-04-04T04:41:19.063Z","repository":{"id":214371300,"uuid":"725233844","full_name":"buntec/derifree","owner":"buntec","description":"Derivative pricing :heart: free monads","archived":false,"fork":false,"pushed_at":"2024-03-07T09:52:58.000Z","size":4267,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-09T16:12:38.646Z","etag":null,"topics":["derivatives","functional-programming","quantitative-finance","scala"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/buntec.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-11-29T18:03:50.000Z","updated_at":"2024-06-03T07:28:41.000Z","dependencies_parsed_at":"2024-01-19T14:45:30.570Z","dependency_job_id":"2f6339e7-ae97-4bb6-9400-e5d771123966","html_url":"https://github.com/buntec/derifree","commit_stats":{"total_commits":124,"total_committers":1,"mean_commits":124.0,"dds":0.0,"last_synced_commit":"abea6f47606af293571ec4bb6f0a918ade37c6c6"},"previous_names":["buntec/derifree"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/buntec%2Fderifree","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/buntec%2Fderifree/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/buntec%2Fderifree/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/buntec%2Fderifree/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/buntec","download_url":"https://codeload.github.com/buntec/derifree/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247123094,"owners_count":20887259,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["derivatives","functional-programming","quantitative-finance","scala"],"created_at":"2024-10-07T12:59:32.443Z","updated_at":"2025-04-04T04:41:19.033Z","avatar_url":"https://github.com/buntec.png","language":"Scala","readme":"# Derifree\n\nDerivative pricing for the ~~lactose~~ side-effect intolerant.\n\nDerifree is an experimental library exploring the use of free monads to\nimplement a contract definition language for (equity) derivatives.\n\n*This is work in progress!*\n\n## Intro\n\nA contract is encoded as a value of type `dsl.RV[A]` where\n`dsl` is an instance of `derifree.Dsl[T]` for some time-like `T` (e.g,. `java.time.Instant`).\nThe type `A` is typically `Unit` and we provide a convenient alias:\n\n```scala\ndsl.ContingentClaim = dsl.RV[Unit]\n```\n\nReturning `Unit` makes sense if you think of a contract as\nhaving the \"side-effect\" of exchanging a sequence of cash flows\nbetween the long and the short party over some time horizon.\nThere isn't a meaningful value to return, in general.\nA generic return type `A` can still be useful. In that case `RV[A]`\nshould be interpreted as a random variable taking value in `A`.\nThis is used internally, e.g., to compute factors in the Longstaff-Schwartz regression.\n\nThe type `dsl.RV` is a free monad. In particular, values are sequenced using `flatMap`,\nallowing us to write contracts using for-comprehensions (see the examples below).\n\n\n## Rules\n\nWhen writing contracts using the derifree DSL, some rules need to be followed\nto ensure pricing doesn't fail or, worse, return incorrect results.\n\n1. The occurrences of any of the keywords\n(`cashflow`, `spot`, etc.) should be unconditional.\nNote that this does not preclude things like conditional cash flows.\nIndeed, to model a conditional cash flow `a` at time `t`,\nwe write `cashflow(if p then a else 0.0, ccy, t)` instead of\n`Monad[RV].whenA(p)(cashflow(a, ccy, t))`.\nTo give another example, if we need a certain spot observation\n`spot(\"ACME\", t)` only conditionally, we simply\nignore its result when it isn't needed.\n\n    The reason for this rule is that we want to be able to\n    collect all meta information about the contract\n    (spot/discount observations, barriers, callability, etc)\n    by evaluating the contract *once* using\n    an *arbitrary* realization of the underlying assets.\n\n    For the read-like keywords (`spot`, `hitProb`, `survivalProb`)\n    this means simply ignoring their result when it isn't needed.\n    For write-like keywords (`cashflow`, `callable`, `puttable`)\n    this means using a neutral value (`0.0` or `None`).\n\n    (Side note: another approach would be to \"discover\" this\n    information at pricing time and restart the pricing\n    until we arrive at a \"fix point\". This would\n    come at a loss in efficiency, however.)\n\n2. Causality: the DSL doesn't prevent you from\nhaving a cash flow or call/early exercise at time `t1` depend on\nan observation at time `t2 \u003e t1`.\nIt's the user's responsibility to ensure all relationships are causal.\n\n## Examples\n\n```scala\nimport cats.*\nimport cats.syntax.all.*\nimport derifree.*\nimport derifree.literals.*\nimport derifree.syntax.*\n\n// use any time type with a `TimeLike` instance\nval dsl = Dsl[java.time.Instant]\nimport dsl.*\n\nval refTime = i\"2023-12-27T18:00:00Z\"\nval expiry = refTime.plusDays(365)\nval settle = expiry.plusDays(2)\n\nval europeanCall = for\n  s0 \u003c- spot(\"AAPL\", refTime)\n  s \u003c- spot(\"AAPL\", expiry)\n  _ \u003c- cashflow(max(s / s0 - 1, 0.0), Ccy.USD, settle)\nyield ()\n\nval europeanUpAndOutCall = for\n  s0 \u003c- spot(\"AAPL\", refTime)\n  s \u003c- spot(\"AAPL\", expiry)\n  p \u003c- survivalProb(\n    Barrier.Discrete(\n      Barrier.Direction.Up,\n      Map(\"AAPL\" -\u003e List((expiry, 1.5 * s0)))\n    )\n  )\n  _ \u003c- cashflow(p * max(s / s0 - 1, 0.0), Ccy.USD, settle)\nyield ()\n\nval europeanPut = for\n  s0 \u003c- spot(\"AAPL\", refTime)\n  s \u003c- spot(\"AAPL\", expiry)\n  _ \u003c- cashflow(max(1 - s / s0, 0.0), Ccy.USD, settle)\nyield ()\n\nval bermudanPut = for\n  s0 \u003c- spot(\"AAPL\", refTime)\n  _ \u003c- List(90, 180, 270, 360)\n    .map(refTime.plusDays)\n    .traverse_(d =\u003e\n      spot(\"AAPL\", d).flatMap(s =\u003e\n        exercisable(max(1 - s / s0, 0.0).some.filter(_ \u003e 0.0), Ccy.USD, d)\n      )\n    )\n  s \u003c- spot(\"AAPL\", expiry)\n  _ \u003c- cashflow(max(1 - s / s0, 0.0), Ccy.USD, settle)\nyield ()\n\nval quantoEuropeanCall = for\n  s0 \u003c- spot(\"AAPL\", refTime)\n  s \u003c- spot(\"AAPL\", expiry)\n  _ \u003c- cashflow(max(s / s0 - 1, 0.0), Ccy.EUR, settle)\nyield ()\n\nval worstOfContinuousDownAndInPut = for\n  s1_0 \u003c- spot(\"AAPL\", refTime)\n  s2_0 \u003c- spot(\"MSFT\", refTime)\n  s3_0 \u003c- spot(\"GOOG\", refTime)\n  s1 \u003c- spot(\"AAPL\", expiry)\n  s2 \u003c- spot(\"MSFT\", expiry)\n  s3 \u003c- spot(\"GOOG\", expiry)\n  p \u003c- hitProb(\n    Barrier.Continuous(\n      Barrier.Direction.Down,\n      Map(\"AAPL\" -\u003e 0.8 * s1_0, \"MSFT\" -\u003e 0.8 * s2_0, \"GOOG\" -\u003e 0.8 * s3_0),\n      from = refTime,\n      to = expiry\n    )\n  )\n  _ \u003c- cashflow(p * max(0.0, 1 - min(s1 / s1_0, s2 / s2_0, s3 / s3_0)), Ccy.USD, settle)\nyield ()\n\nval worstOfEuropeanDownAndInPut = for\n  s1_0 \u003c- spot(\"AAPL\", refTime)\n  s2_0 \u003c- spot(\"MSFT\", refTime)\n  s3_0 \u003c- spot(\"GOOG\", refTime)\n  s1 \u003c- spot(\"AAPL\", expiry)\n  s2 \u003c- spot(\"MSFT\", expiry)\n  s3 \u003c- spot(\"GOOG\", expiry)\n  p \u003c- hitProb(\n    Barrier.Discrete(\n      Barrier.Direction.Down,\n      Map(\n        \"AAPL\" -\u003e List((expiry, 0.8 * s1_0)),\n        \"MSFT\" -\u003e List((expiry, 0.8 * s2_0)),\n        \"GOOG\" -\u003e List((expiry, 0.8 * s3_0))\n      )\n    )\n  )\n  _ \u003c- cashflow(p * max(0.0, 1 - min(s1 / s1_0, s2 / s2_0, s3 / s3_0)), Ccy.USD, settle)\nyield ()\n\nval barrierReverseConvertible =\n  val relBarrier = 0.7\n  val couponTimes = List(90, 180, 270, 360).map(refTime.plusDays)\n  for\n    s1_0 \u003c- spot(\"AAPL\", refTime)\n    s2_0 \u003c- spot(\"MSFT\", refTime)\n    s3_0 \u003c- spot(\"GOOG\", refTime)\n    s1 \u003c- spot(\"AAPL\", expiry)\n    s2 \u003c- spot(\"MSFT\", expiry)\n    s3 \u003c- spot(\"GOOG\", expiry)\n    p \u003c- hitProb(\n      Barrier.Continuous(\n        Barrier.Direction.Down,\n        Map(\n          \"AAPL\" -\u003e relBarrier * s1_0,\n          \"MSFT\" -\u003e relBarrier * s2_0,\n          \"GOOG\" -\u003e relBarrier * s3_0\n        ),\n        from = refTime,\n        to = expiry\n      )\n    )\n    _ \u003c- couponTimes.traverse_(t =\u003e cashflow(5.0, Ccy.USD, t))\n    _ \u003c- cashflow(\n      100 * (1 - p * max(0.0, 1 - min(s1 / s1_0, s2 / s2_0, s3 / s3_0))),\n      Ccy.USD,\n      settle\n    )\n  yield ()\n\nval callableBarrierReverseConvertible =\n  val relBarrier = 0.7\n  val callTimes = List(90, 180, 270, 360).map(refTime.plusDays)\n  val couponTimes = List(90, 180, 270, 360).map(refTime.plusDays)\n  for\n    s1_0 \u003c- spot(\"AAPL\", refTime)\n    s2_0 \u003c- spot(\"MSFT\", refTime)\n    s3_0 \u003c- spot(\"GOOG\", refTime)\n    s1 \u003c- spot(\"AAPL\", expiry)\n    s2 \u003c- spot(\"MSFT\", expiry)\n    s3 \u003c- spot(\"GOOG\", expiry)\n    p \u003c- hitProb(\n      Barrier.Continuous(\n        Barrier.Direction.Down,\n        Map(\n          \"AAPL\" -\u003e relBarrier * s1_0,\n          \"MSFT\" -\u003e relBarrier * s2_0,\n          \"GOOG\" -\u003e relBarrier * s3_0\n        ),\n        from = refTime,\n        to = expiry\n      )\n    )\n    _ \u003c- callTimes.traverse_(t =\u003e callable(100.0.some, Ccy.USD, t))\n    _ \u003c- couponTimes.traverse_(t =\u003e cashflow(5.0, Ccy.USD, t))\n    _ \u003c- cashflow(\n      100 * (1 - p * max(0.0, 1 - min(s1 / s1_0, s2 / s2_0, s3 / s3_0))),\n      Ccy.USD,\n      settle\n    )\n  yield ()\n\nval couponBarrier =\n  val relBarrier = 0.95\n  val couponAmount = 5.0\n  val couponTimes = List(90, 180, 270, 360).map(refTime.plusDays)\n  for\n    s1_0 \u003c- spot(\"AAPL\", refTime)\n    s2_0 \u003c- spot(\"MSFT\", refTime)\n    s3_0 \u003c- spot(\"GOOG\", refTime)\n    s1 \u003c- spot(\"AAPL\", expiry)\n    s2 \u003c- spot(\"MSFT\", expiry)\n    s3 \u003c- spot(\"GOOG\", expiry)\n    _ \u003c- couponTimes.traverse: t =\u003e\n      (spot(\"AAPL\", t), spot(\"MSFT\", t), spot(\"GOOG\", t))\n        .mapN((s1_t, s2_t, s3_t) =\u003e min(s1_t / s1_0, s2_t / s2_0, s3_t / s3_0) \u003e relBarrier)\n        .flatMap: isAbove =\u003e\n          cashflow(if isAbove then couponAmount else 0.0, Ccy.USD, t)\n  yield ()\n\nval couponBarrierWithMemoryEffect =\n  val relBarrier = 0.95\n  val couponAmount = 5.0\n  val couponTimes = List(90, 180, 270, 360).map(refTime.plusDays)\n  for\n    s1_0 \u003c- spot(\"AAPL\", refTime)\n    s2_0 \u003c- spot(\"MSFT\", refTime)\n    s3_0 \u003c- spot(\"GOOG\", refTime)\n    s1 \u003c- spot(\"AAPL\", expiry)\n    s2 \u003c- spot(\"MSFT\", expiry)\n    s3 \u003c- spot(\"GOOG\", expiry)\n    _ \u003c- couponTimes.foldLeftM(0.0): (acc, t) =\u003e\n      (spot(\"AAPL\", t), spot(\"MSFT\", t), spot(\"GOOG\", t))\n        .mapN((s1_t, s2_t, s3_t) =\u003e min(s1_t / s1_0, s2_t / s2_0, s3_t / s3_0) \u003e relBarrier)\n        .flatMap: isAbove =\u003e\n          cashflow(if isAbove then acc + couponAmount else 0.0, Ccy.USD, t)\n            .as(if isAbove then 0 else acc + couponAmount)\n  yield ()\n\n// let's price using a simple Black-Scholes model\n\nval discount = YieldCurve.fromContinuouslyCompoundedRate(0.05.rate, refTime)\n\nval aapl = models.blackscholes.Asset(\n  \"AAPL\",\n  Ccy.USD,\n  Forward(195.0, divs = Nil, discount = discount, borrow = YieldCurve.zero),\n  0.23.vol\n)\n\nval msft = models.blackscholes.Asset(\n  \"MSFT\",\n  Ccy.USD,\n  Forward(370.0, divs = Nil, discount = discount, borrow = YieldCurve.zero),\n  0.25.vol\n)\n\nval goog = models.blackscholes.Asset(\n  \"GOOG\",\n  Ccy.USD,\n  Forward(135.0, divs = Nil, discount = discount, borrow = YieldCurve.zero),\n  0.29.vol\n)\n\nval correlations =\n  Map(\n    (\"AAPL\", \"MSFT\") -\u003e 0.7,\n    (\"AAPL\", \"GOOG\") -\u003e 0.6,\n    (\"MSFT\", \"GOOG\") -\u003e 0.65,\n    (\"EURUSD\", \"AAPL\") -\u003e -0.2\n  )\n\n// for quantos\nval fxVols = Map(CcyPair(Ccy.EUR, Ccy.USD) -\u003e 0.07.vol)\n\nval dirNums = Sobol.directionNumbers(10000).toTry.get\n\nval sim = models.blackscholes.simulator(\n  TimeGrid.Factory.almostEquidistant(YearFraction.oneDay),\n  NormalGen.Factory.sobol(dirNums),\n  refTime,\n  List(aapl, msft, goog),\n  correlations,\n  discount,\n  Map.empty\n)\n\n// the number of Monte Carlo simulations\nval nSims = 32767\n```\n\n```scala\neuropeanCall.fairValue(sim, nSims)\n// res0: Either[Error, PV] = Right(value = 0.11573966972403645)\n\n// should be cheaper than plain European call\neuropeanUpAndOutCall.fairValue(sim, nSims)\n// res1: Either[Error, PV] = Right(value = 0.08550169593261212)\n\neuropeanPut.fairValue(sim, nSims)\n// res2: Either[Error, PV] = Right(value = 0.06699973963703337)\n\n// should be more expensive than European put\nbermudanPut.fairValue(sim, nSims)\n// res3: Either[Error, PV] = Right(value = 0.07095113729727867)\n\n// what are the probabilities of early exercise?\nbermudanPut.putProbabilities(sim, nSims)\n// res4: Either[Error, Map[Instant, Double]] = Right(\n//   value = Map(\n//     2024-09-22T18:00:00Z -\u003e 0.130283516953032,\n//     2024-03-26T18:00:00Z -\u003e 0.05578783532212287,\n//     2024-12-21T18:00:00Z -\u003e 0.07791375469222084,\n//     2024-06-24T18:00:00Z -\u003e 0.11328470717490158\n//   )\n// )\n\nquantoEuropeanCall.fairValue(sim, nSims)\n// res5: Either[Error, PV] = Left(\n//   value = Error(message = \"missing vol for USDEUR\")\n// )\n\nworstOfContinuousDownAndInPut.fairValue(sim, nSims)\n// res6: Either[Error, PV] = Right(value = 0.11952072431608426)\n\n// should be cheaper than continuous barrier\nworstOfEuropeanDownAndInPut.fairValue(sim, nSims)\n// res7: Either[Error, PV] = Right(value = 0.09498951398431003)\n\nbarrierReverseConvertible.fairValue(sim, nSims)\n// res8: Either[Error, PV] = Right(value = 106.09064030012115)\n\n// should be cheaper than non-callable BRC\ncallableBarrierReverseConvertible.fairValue(sim, nSims)\n// res9: Either[Error, PV] = Right(value = 105.51905901142916)\n\n// what are the probabilities of being called?\ncallableBarrierReverseConvertible.callProbabilities(sim, nSims)\n// res10: Either[Error, Map[Instant, Double]] = Right(\n//   value = Map(\n//     2024-09-22T18:00:00Z -\u003e 0.15588854640339367,\n//     2024-03-26T18:00:00Z -\u003e 0.0011597033600878933,\n//     2024-12-21T18:00:00Z -\u003e 0.16977446821497238,\n//     2024-06-24T18:00:00Z -\u003e 0.003143406476027711\n//   )\n// )\n\n// should be cheaper than sum of discounted coupons\ncouponBarrier.fairValue(sim, nSims)\n// res11: Either[Error, PV] = Right(value = 8.314781740288916)\n\n// should be more expensive than w/o memory, still cheaper than discounted sum of coupons\ncouponBarrierWithMemoryEffect.fairValue(sim, nSims)\n// res12: Either[Error, PV] = Right(value = 10.370530640687656)\n```\n\n## Building\n\n`README.md` in the root directory is generated from `./docs/readme.md`\nby running `sbt docs/mdoc`. The example code in `./docs/readme.md` is a copy\nof the code in `./examples/src/main/scala/examples/readme.scala`\nand should be kept in sync.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbuntec%2Fderifree","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbuntec%2Fderifree","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbuntec%2Fderifree/lists"}