From 8946253a862778ad4d446ee2a6c624e91aca3c29 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Wed, 23 Sep 2026 12:54:22 +0200 Subject: [PATCH 1/6] Some position calcs --- vecxt_re/src/PositionReturn.scala | 95 ++++++++++++++++++++++ vecxt_re/src/PriceUnit.scala | 13 +++ vecxt_re/test/src/priceForecast.test.scala | 53 ++++++++++++ vecxt_re/test/src/priceUnit.test.scala | 16 ++++ 4 files changed, 177 insertions(+) create mode 100644 vecxt_re/src/PositionReturn.scala create mode 100644 vecxt_re/src/PriceUnit.scala create mode 100644 vecxt_re/test/src/priceForecast.test.scala create mode 100644 vecxt_re/test/src/priceUnit.test.scala diff --git a/vecxt_re/src/PositionReturn.scala b/vecxt_re/src/PositionReturn.scala new file mode 100644 index 00000000..7bfd71cb --- /dev/null +++ b/vecxt_re/src/PositionReturn.scala @@ -0,0 +1,95 @@ +package vecxt_re + +import java.time.LocalDate +import java.time.temporal.ChronoUnit +import vecxt.all.- + +object PositionCalculations: + /** + * Forecasts the price of a bond which is being pulled to parity assuming a one year time horizon. + * + * This allows us to ignore seasonality, which is assumed to be annual. + * + * @param price the current price, expressed in `priceUnit` + * @param priceUnit the unit `price` is quoted in (e.g. `Pts` for a price of 102 meaning 102% of par) + * @param priceDate the date on which `price` was observed; the forecast is for one year after this date + * @param maturity the maturity date of the bond + * @return the forecast price one year after `priceDate`, in `priceUnit`. If the bond matures within the year, + * this is par. + */ + def priceForecast1Year( + price: Double, + priceUnit: PriceUnit, + priceDate: LocalDate, + maturity: LocalDate + ): Double = + price + pull2Parity1Year(price, priceUnit, priceDate, maturity) + + /** + * The change in price over one year of a bond which is being pulled linearly (by calendar days) to parity at + * maturity. + * + * This allows us to ignore seasonality, which is assumed to be annual. + * + * Uses calendar days, not yield accretion. + * + * @param price the current price, expressed in `priceUnit` + * @param priceUnit the unit `price` is quoted in + * @param priceDate the date on which `price` was observed; the projection is for one year after this date + * @param maturity the maturity date of the bond + * @return the forecast price change (forecast price minus `price`) in `priceUnit`. Positive for a bond priced + * below par, negative above par. If the bond matures within the year, this is the full distance to par. + */ + def pull2Parity1Year( + price: Double, + priceUnit: PriceUnit, + priceDate: LocalDate, + maturity: LocalDate + ): Double = + import scala.math.Ordered.orderingToOrdered + assert(maturity >= priceDate, s"Maturity $maturity and pricing date $priceDate suggest this bond has already matured. It should not pull to parity" ) + + val one = PriceUnit.One.convert(1.0, priceUnit) + val projectionDate = priceDate.plusYears(1L) + + val days2Maturity = ChronoUnit.DAYS.between(priceDate, maturity) + val days2Project = ChronoUnit.DAYS.between(priceDate, projectionDate) + if (maturity < projectionDate) + one - price + else + days2Project.toDouble / days2Maturity.toDouble * ( one - price) + + /** + * Investment is purchased on the price date + * Investment is sold in one years time, it is assumed the market is at a steady state over this period with no material change in risk premia. + * Annual seasonality - i.e. seasonality can be ignored over a single annual time period. + * A deep liquid market which is not affected by purchase / sale + * Investments renew at expiry into an equivalent bond at par, at the same risk spread and expected loss. + * All capital is ultimately returned to investor - impaired bonds would not pull to parity + * The lossVector is _assumed_ to be in units of `One` + * + * @param price the current price, expressed in `priceUnit` + * @param priceUnit the unit `price` is quoted in + * @param priceDate the date on which `price` was observed (the purchase date) + * @param maturity the maturity date of the bond + * @param lossVector - By convention all positive numbers + * @param riskFreeRate the annual risk free rate, expressed in `riskFreeRateUnit` (e.g. 325 in `Bps` is 3.25%) + * @param riskFreeRateUnit the unit `riskFreeRate` is quoted in + * @return one element per entry of `lossVector`: the one year PnL in units of `One`, i.e. risk free interest + * plus pull to parity minus that loss + */ + def pnlForecast1Year( + price: Double, + priceUnit: PriceUnit, + priceDate: LocalDate, + maturity: LocalDate, + lossVector: Array[Double], + riskFreeRate: Double, + riskFreeRateUnit: PriceUnit, + spread: Double, + spreadUnit: PriceUnit + ): Array[Double] = + val riskFreeInterest = riskFreeRateUnit.convert(1.0, PriceUnit.One) * riskFreeRate + val spreadInOne = spreadUnit.convert(spread, PriceUnit.One) + val priceInOne = priceUnit.convert(price, PriceUnit.One) + ( spreadInOne + riskFreeInterest + pull2Parity1Year(priceInOne, PriceUnit.One, priceDate, maturity)) - lossVector diff --git a/vecxt_re/src/PriceUnit.scala b/vecxt_re/src/PriceUnit.scala new file mode 100644 index 00000000..2d4e2d05 --- /dev/null +++ b/vecxt_re/src/PriceUnit.scala @@ -0,0 +1,13 @@ +package vecxt_re + +/** The unit a price or rate is quoted in. `unit` is the value that represents par (100%) in that unit. */ +enum PriceUnit(val unit: Int): + case Bps extends PriceUnit(10000) + case Pts extends PriceUnit(100) + case One extends PriceUnit(1) + + /** Converts `value`, expressed in this unit, into the unit `to`. e.g. `Pts.convert(102, One) == 1.02` */ + def convert[A: Fractional](value: A, to: PriceUnit): A = + import scala.math.Fractional.Implicits.* + value * Fractional[A].fromInt(to.unit) / Fractional[A].fromInt(unit) + \ No newline at end of file diff --git a/vecxt_re/test/src/priceForecast.test.scala b/vecxt_re/test/src/priceForecast.test.scala new file mode 100644 index 00000000..0c7d50ae --- /dev/null +++ b/vecxt_re/test/src/priceForecast.test.scala @@ -0,0 +1,53 @@ +package vecxt_re + +import java.time.LocalDate +import vecxt.all.+ + +class PositionCalculationsSuite extends munit.FunSuite: + + test("one year maturity"): + val reportDate = LocalDate.of(2026, 1, 1) + val forecastPrice = PositionCalculations.priceForecast1Year(102, PriceUnit.Pts, reportDate,reportDate.plusYears(1)) + + assertEqualsDouble(forecastPrice, 100.0, 0.0000001) + + test("Maturity same date"): + val reportDate = LocalDate.of(2026, 1, 1) + val forecastPrice = PositionCalculations.priceForecast1Year(102, PriceUnit.Pts, reportDate,reportDate) + + assertEqualsDouble(forecastPrice, 100.0, 0.0000001) + + test("Early maturity"): + val reportDate = LocalDate.of(2026, 1, 1) + val forecastPrice = PositionCalculations.priceForecast1Year(102, PriceUnit.Pts, reportDate,reportDate.plusMonths(6)) + val forecastPrice2 = PositionCalculations.priceForecast1Year(102, PriceUnit.Pts, reportDate,reportDate.plusDays(1)) + + assertEqualsDouble(forecastPrice, 100.0, 0.0000001) + assertEqualsDouble(forecastPrice2, 100.0, 0.0000001) + + test("refuses already matured"): + val reportDate = LocalDate.of(2026, 1, 1) + intercept[java.lang.AssertionError](PositionCalculations.priceForecast1Year(102, PriceUnit.Pts, reportDate,reportDate.plusYears(-1))) + + test("Interpolation to a point"): + val reportDate = LocalDate.of(2026, 1, 1) + val forecastPrice = PositionCalculations.priceForecast1Year(10200, PriceUnit.Bps, reportDate, reportDate.plusYears(2)) + + assertEqualsDouble(forecastPrice, 10100.0, 0.0000001) + + test("pnlForecast"): + + val reportDate = LocalDate.of(2026, 1, 1) + val losses = Array[Double](0.0, 0.1, 0.0, 0.2) + val forecastPrice = PositionCalculations.pnlForecast1Year( + 102, PriceUnit.Pts, + reportDate, reportDate.plusYears(2), losses, 325.0, PriceUnit.Bps, 0, PriceUnit.Bps ) + + val forecast = Array[Double](0.0225, -0.0775, 0.0225, -0.1775) + assertVecEquals(forecastPrice, forecast ) + + val forecastPriceWSpread = PositionCalculations.pnlForecast1Year( + 102, PriceUnit.Pts, + reportDate, reportDate.plusYears(2), losses, 325.0, PriceUnit.Bps, 100.0, PriceUnit.Bps ) + assertVecEquals(forecastPriceWSpread, forecast + 0.01 ) + diff --git a/vecxt_re/test/src/priceUnit.test.scala b/vecxt_re/test/src/priceUnit.test.scala new file mode 100644 index 00000000..85864d35 --- /dev/null +++ b/vecxt_re/test/src/priceUnit.test.scala @@ -0,0 +1,16 @@ +package vecxt_re + + +class PriceUnitSuite extends munit.FunSuite: + + test("conversions"): + assertEqualsDouble(PriceUnit.Bps.convert(10000, PriceUnit.One), 1.0, 0.00000001) + assertEqualsDouble(PriceUnit.Pts.convert(100, PriceUnit.One), 1.0, 0.00000001) + assertEqualsDouble(PriceUnit.One.convert(1, PriceUnit.Bps), 1e4, 0.00000001) + + + + + + + From bd13e1b662dda239c3fa4db8192575b69978aa5d Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Wed, 23 Sep 2026 12:54:26 +0200 Subject: [PATCH 2/6] . --- build.mill | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.mill b/build.mill index 842a4489..d7a5bdd9 100644 --- a/build.mill +++ b/build.mill @@ -37,7 +37,7 @@ object V: val spire: Dep = mvn"org.typelevel::spire::0.18.0" val laminar: Dep = mvn"com.raquo::laminar::17.2.1" val scalaTags: Dep = mvn"com.lihaoyi::scalatags::0.13.1" - val scalaVersion = "3.9.0" + val scalaVersion = "3.8.4" val munitVersion = "1.3.6" val blas: Dep = mvn"dev.ludovic.netlib:blas:3.2.0" val lapack: Dep = mvn"dev.ludovic.netlib:lapack:3.2.0" From 13045edce80506092031acf327a297f3d23f3c93 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 10:56:06 +0000 Subject: [PATCH 3/6] [autofix.ci] apply automated fixes --- vecxt_re/src/PositionReturn.scala | 113 +++++++++++---------- vecxt_re/src/PriceUnit.scala | 5 +- vecxt_re/test/src/priceForecast.test.scala | 58 +++++++---- vecxt_re/test/src/priceUnit.test.scala | 11 +- 4 files changed, 106 insertions(+), 81 deletions(-) diff --git a/vecxt_re/src/PositionReturn.scala b/vecxt_re/src/PositionReturn.scala index 7bfd71cb..0e52d568 100644 --- a/vecxt_re/src/PositionReturn.scala +++ b/vecxt_re/src/PositionReturn.scala @@ -5,62 +5,71 @@ import java.time.temporal.ChronoUnit import vecxt.all.- object PositionCalculations: - /** - * Forecasts the price of a bond which is being pulled to parity assuming a one year time horizon. - * + /** Forecasts the price of a bond which is being pulled to parity assuming a one year time horizon. + * * This allows us to ignore seasonality, which is assumed to be annual. * - * @param price the current price, expressed in `priceUnit` - * @param priceUnit the unit `price` is quoted in (e.g. `Pts` for a price of 102 meaning 102% of par) - * @param priceDate the date on which `price` was observed; the forecast is for one year after this date - * @param maturity the maturity date of the bond - * @return the forecast price one year after `priceDate`, in `priceUnit`. If the bond matures within the year, - * this is par. + * @param price + * the current price, expressed in `priceUnit` + * @param priceUnit + * the unit `price` is quoted in (e.g. `Pts` for a price of 102 meaning 102% of par) + * @param priceDate + * the date on which `price` was observed; the forecast is for one year after this date + * @param maturity + * the maturity date of the bond + * @return + * the forecast price one year after `priceDate`, in `priceUnit`. If the bond matures within the year, this is par. */ def priceForecast1Year( - price: Double, - priceUnit: PriceUnit, - priceDate: LocalDate, - maturity: LocalDate - ): Double = + price: Double, + priceUnit: PriceUnit, + priceDate: LocalDate, + maturity: LocalDate + ): Double = price + pull2Parity1Year(price, priceUnit, priceDate, maturity) - /** - * The change in price over one year of a bond which is being pulled linearly (by calendar days) to parity at + /** The change in price over one year of a bond which is being pulled linearly (by calendar days) to parity at * maturity. - * + * * This allows us to ignore seasonality, which is assumed to be annual. - * + * * Uses calendar days, not yield accretion. * - * @param price the current price, expressed in `priceUnit` - * @param priceUnit the unit `price` is quoted in - * @param priceDate the date on which `price` was observed; the projection is for one year after this date - * @param maturity the maturity date of the bond - * @return the forecast price change (forecast price minus `price`) in `priceUnit`. Positive for a bond priced - * below par, negative above par. If the bond matures within the year, this is the full distance to par. + * @param price + * the current price, expressed in `priceUnit` + * @param priceUnit + * the unit `price` is quoted in + * @param priceDate + * the date on which `price` was observed; the projection is for one year after this date + * @param maturity + * the maturity date of the bond + * @return + * the forecast price change (forecast price minus `price`) in `priceUnit`. Positive for a bond priced below par, + * negative above par. If the bond matures within the year, this is the full distance to par. */ def pull2Parity1Year( - price: Double, - priceUnit: PriceUnit, - priceDate: LocalDate, - maturity: LocalDate - ): Double = + price: Double, + priceUnit: PriceUnit, + priceDate: LocalDate, + maturity: LocalDate + ): Double = import scala.math.Ordered.orderingToOrdered - assert(maturity >= priceDate, s"Maturity $maturity and pricing date $priceDate suggest this bond has already matured. It should not pull to parity" ) - + assert( + maturity >= priceDate, + s"Maturity $maturity and pricing date $priceDate suggest this bond has already matured. It should not pull to parity" + ) + val one = PriceUnit.One.convert(1.0, priceUnit) val projectionDate = priceDate.plusYears(1L) val days2Maturity = ChronoUnit.DAYS.between(priceDate, maturity) val days2Project = ChronoUnit.DAYS.between(priceDate, projectionDate) - if (maturity < projectionDate) - one - price - else - days2Project.toDouble / days2Maturity.toDouble * ( one - price) + if maturity < projectionDate then one - price + else days2Project.toDouble / days2Maturity.toDouble * (one - price) + end if + end pull2Parity1Year - /** - * Investment is purchased on the price date + /** Investment is purchased on the price date * Investment is sold in one years time, it is assumed the market is at a steady state over this period with no material change in risk premia. * Annual seasonality - i.e. seasonality can be ignored over a single annual time period. * A deep liquid market which is not affected by purchase / sale @@ -79,17 +88,19 @@ object PositionCalculations: * plus pull to parity minus that loss */ def pnlForecast1Year( - price: Double, - priceUnit: PriceUnit, - priceDate: LocalDate, - maturity: LocalDate, - lossVector: Array[Double], - riskFreeRate: Double, - riskFreeRateUnit: PriceUnit, - spread: Double, - spreadUnit: PriceUnit - ): Array[Double] = - val riskFreeInterest = riskFreeRateUnit.convert(1.0, PriceUnit.One) * riskFreeRate - val spreadInOne = spreadUnit.convert(spread, PriceUnit.One) - val priceInOne = priceUnit.convert(price, PriceUnit.One) - ( spreadInOne + riskFreeInterest + pull2Parity1Year(priceInOne, PriceUnit.One, priceDate, maturity)) - lossVector + price: Double, + priceUnit: PriceUnit, + priceDate: LocalDate, + maturity: LocalDate, + lossVector: Array[Double], + riskFreeRate: Double, + riskFreeRateUnit: PriceUnit, + spread: Double, + spreadUnit: PriceUnit + ): Array[Double] = + val riskFreeInterest = riskFreeRateUnit.convert(1.0, PriceUnit.One) * riskFreeRate + val spreadInOne = spreadUnit.convert(spread, PriceUnit.One) + val priceInOne = priceUnit.convert(price, PriceUnit.One) + (spreadInOne + riskFreeInterest + pull2Parity1Year(priceInOne, PriceUnit.One, priceDate, maturity)) - lossVector + end pnlForecast1Year +end PositionCalculations diff --git a/vecxt_re/src/PriceUnit.scala b/vecxt_re/src/PriceUnit.scala index 2d4e2d05..a939162d 100644 --- a/vecxt_re/src/PriceUnit.scala +++ b/vecxt_re/src/PriceUnit.scala @@ -1,7 +1,7 @@ package vecxt_re /** The unit a price or rate is quoted in. `unit` is the value that represents par (100%) in that unit. */ -enum PriceUnit(val unit: Int): +enum PriceUnit(val unit: Int): case Bps extends PriceUnit(10000) case Pts extends PriceUnit(100) case One extends PriceUnit(1) @@ -10,4 +10,5 @@ enum PriceUnit(val unit: Int): def convert[A: Fractional](value: A, to: PriceUnit): A = import scala.math.Fractional.Implicits.* value * Fractional[A].fromInt(to.unit) / Fractional[A].fromInt(unit) - \ No newline at end of file + end convert +end PriceUnit diff --git a/vecxt_re/test/src/priceForecast.test.scala b/vecxt_re/test/src/priceForecast.test.scala index 0c7d50ae..74c76de9 100644 --- a/vecxt_re/test/src/priceForecast.test.scala +++ b/vecxt_re/test/src/priceForecast.test.scala @@ -4,50 +4,70 @@ import java.time.LocalDate import vecxt.all.+ class PositionCalculationsSuite extends munit.FunSuite: - + test("one year maturity"): val reportDate = LocalDate.of(2026, 1, 1) - val forecastPrice = PositionCalculations.priceForecast1Year(102, PriceUnit.Pts, reportDate,reportDate.plusYears(1)) + val forecastPrice = PositionCalculations.priceForecast1Year(102, PriceUnit.Pts, reportDate, reportDate.plusYears(1)) assertEqualsDouble(forecastPrice, 100.0, 0.0000001) test("Maturity same date"): val reportDate = LocalDate.of(2026, 1, 1) - val forecastPrice = PositionCalculations.priceForecast1Year(102, PriceUnit.Pts, reportDate,reportDate) + val forecastPrice = PositionCalculations.priceForecast1Year(102, PriceUnit.Pts, reportDate, reportDate) assertEqualsDouble(forecastPrice, 100.0, 0.0000001) - + test("Early maturity"): val reportDate = LocalDate.of(2026, 1, 1) - val forecastPrice = PositionCalculations.priceForecast1Year(102, PriceUnit.Pts, reportDate,reportDate.plusMonths(6)) - val forecastPrice2 = PositionCalculations.priceForecast1Year(102, PriceUnit.Pts, reportDate,reportDate.plusDays(1)) + val forecastPrice = + PositionCalculations.priceForecast1Year(102, PriceUnit.Pts, reportDate, reportDate.plusMonths(6)) + val forecastPrice2 = PositionCalculations.priceForecast1Year(102, PriceUnit.Pts, reportDate, reportDate.plusDays(1)) assertEqualsDouble(forecastPrice, 100.0, 0.0000001) assertEqualsDouble(forecastPrice2, 100.0, 0.0000001) - - test("refuses already matured"): + + test("refuses already matured"): val reportDate = LocalDate.of(2026, 1, 1) - intercept[java.lang.AssertionError](PositionCalculations.priceForecast1Year(102, PriceUnit.Pts, reportDate,reportDate.plusYears(-1))) - + intercept[java.lang.AssertionError]( + PositionCalculations.priceForecast1Year(102, PriceUnit.Pts, reportDate, reportDate.plusYears(-1)) + ) + test("Interpolation to a point"): val reportDate = LocalDate.of(2026, 1, 1) - val forecastPrice = PositionCalculations.priceForecast1Year(10200, PriceUnit.Bps, reportDate, reportDate.plusYears(2)) + val forecastPrice = + PositionCalculations.priceForecast1Year(10200, PriceUnit.Bps, reportDate, reportDate.plusYears(2)) - assertEqualsDouble(forecastPrice, 10100.0, 0.0000001) + assertEqualsDouble(forecastPrice, 10100.0, 0.0000001) test("pnlForecast"): val reportDate = LocalDate.of(2026, 1, 1) val losses = Array[Double](0.0, 0.1, 0.0, 0.2) val forecastPrice = PositionCalculations.pnlForecast1Year( - 102, PriceUnit.Pts, - reportDate, reportDate.plusYears(2), losses, 325.0, PriceUnit.Bps, 0, PriceUnit.Bps ) + 102, + PriceUnit.Pts, + reportDate, + reportDate.plusYears(2), + losses, + 325.0, + PriceUnit.Bps, + 0, + PriceUnit.Bps + ) val forecast = Array[Double](0.0225, -0.0775, 0.0225, -0.1775) - assertVecEquals(forecastPrice, forecast ) + assertVecEquals(forecastPrice, forecast) val forecastPriceWSpread = PositionCalculations.pnlForecast1Year( - 102, PriceUnit.Pts, - reportDate, reportDate.plusYears(2), losses, 325.0, PriceUnit.Bps, 100.0, PriceUnit.Bps ) - assertVecEquals(forecastPriceWSpread, forecast + 0.01 ) - + 102, + PriceUnit.Pts, + reportDate, + reportDate.plusYears(2), + losses, + 325.0, + PriceUnit.Bps, + 100.0, + PriceUnit.Bps + ) + assertVecEquals(forecastPriceWSpread, forecast + 0.01) +end PositionCalculationsSuite diff --git a/vecxt_re/test/src/priceUnit.test.scala b/vecxt_re/test/src/priceUnit.test.scala index 85864d35..48de8842 100644 --- a/vecxt_re/test/src/priceUnit.test.scala +++ b/vecxt_re/test/src/priceUnit.test.scala @@ -1,16 +1,9 @@ package vecxt_re - class PriceUnitSuite extends munit.FunSuite: - + test("conversions"): assertEqualsDouble(PriceUnit.Bps.convert(10000, PriceUnit.One), 1.0, 0.00000001) assertEqualsDouble(PriceUnit.Pts.convert(100, PriceUnit.One), 1.0, 0.00000001) assertEqualsDouble(PriceUnit.One.convert(1, PriceUnit.Bps), 1e4, 0.00000001) - - - - - - - +end PriceUnitSuite From 937b9c85d5f8950f470d0edad19c8b7e5e497354 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Wed, 23 Sep 2026 17:37:25 +0200 Subject: [PATCH 4/6] . --- jitAudit/package.mill | 8 +- vecxt/src/BooleanMatrix.scala | 132 +++++++++++++++++++++ vecxt/src/all.scala | 1 + vecxt/src/doublematrix.scala | 5 + vecxt/test/src/MatrixBoolean.test.scala | 146 ++++++++++++++++++++++++ vecxt/test/src/matrix.test.scala | 39 +++++++ vecxt_re/src/LossMatrix.scala | 37 ++++++ vecxt_re/test/src/lossMatrix.test.scala | 59 ++++++++++ 8 files changed, 423 insertions(+), 4 deletions(-) create mode 100644 vecxt/src/BooleanMatrix.scala create mode 100644 vecxt_re/src/LossMatrix.scala create mode 100644 vecxt_re/test/src/lossMatrix.test.scala diff --git a/jitAudit/package.mill b/jitAudit/package.mill index d881a073..e9811081 100644 --- a/jitAudit/package.mill +++ b/jitAudit/package.mill @@ -28,8 +28,8 @@ import mill.*, scalalib.* * * ==What was deliberately not built: D2 and D5== * - * The plan specified two further checks in this tier, both of which work by running with - * {@code -XX:+LogCompilation} and parsing the XML: D2 confirms Vector API intrinsics were applied by looking for + * The plan specified two further checks in this tier, both of which work by running with {@code -XX:+LogCompilation} + * and parsing the XML: D2 confirms Vector API intrinsics were applied by looking for * {@code } entries, and D5 counts {@code } and recompilation events. Neither * is implemented, and the decision is not "not yet" — it is that the cost/benefit does not work. Recorded here rather * than left as an absence, because an unexplained gap in a checklist reads as an oversight. @@ -101,8 +101,8 @@ object `package` extends ScalaModule: * ==How this relates to the unbuilt D2== * * It covers part of the same ground, from an observable consequence and a product-grade {@code -XX:} flag rather - * than from a JDK-internal XML vocabulary. D1 with EA on cannot distinguish "the SIMD intrinsics were applied" - * from "they were not, and EA scalarised the software-path objects instead" — both read as zero. This scope can. + * than from a JDK-internal XML vocabulary. D1 with EA on cannot distinguish "the SIMD intrinsics were applied" from + * "they were not, and EA scalarised the software-path objects instead" — both read as zero. This scope can. * * Only part, though, and the earlier framing of it as "what replaces D2" was too strong. It relies on EA being what * rescues a software-path kernel, and that is not always so: D6's canary is a software-path kernel that allocates diff --git a/vecxt/src/BooleanMatrix.scala b/vecxt/src/BooleanMatrix.scala new file mode 100644 index 00000000..7fa60a2d --- /dev/null +++ b/vecxt/src/BooleanMatrix.scala @@ -0,0 +1,132 @@ +package vecxt + +import vecxt.matrix.* + +/** Logical operations on `Matrix[Boolean]`, mirroring [[BooleanArrays]]. + * + * Each op has the same two branches as the other matrix element-wise ops: when the backing array holds exactly the + * matrix's elements in order, it delegates to the (SIMD on the JVM) array op and reuses `m.layout`; otherwise it walks + * the view through `linearIndex`, and any new result is dense column-major. + */ +object BooleanMatrix: + + extension (m: Matrix[Boolean]) + + /** True if every element of the matrix is true. True for an empty matrix. */ + def allTrue: Boolean = + if m.hasSimpleContiguousMemoryLayout then BooleanArrays.allTrue(m.raw) + else + var out = true + var j = 0 + while out && j < m.cols do + var i = 0 + while out && i < m.rows do + if !m.raw(m.layout.linearIndex(i, j)) then out = false + end if + i += 1 + end while + j += 1 + end while + out + end allTrue + + /** True if at least one element of the matrix is true. */ + def any: Boolean = + if m.hasSimpleContiguousMemoryLayout then BooleanArrays.any(m.raw) + else + var out = false + var j = 0 + while !out && j < m.cols do + var i = 0 + while !out && i < m.rows do + if m.raw(m.layout.linearIndex(i, j)) then out = true + end if + i += 1 + end while + j += 1 + end while + out + end any + + /** The number of true elements in the matrix. */ + def trues: Int = + if m.hasSimpleContiguousMemoryLayout then BooleanArrays.trues(m.raw) + else + var sum = 0 + m.layout.foreach2D { (i, j) => + if m.raw(m.layout.linearIndex(i, j)) then sum += 1 + end if + } + sum + end trues + + /** Element-wise negation, returning a new matrix. */ + def not: Matrix[Boolean] = + if m.hasSimpleContiguousMemoryLayout then Matrix[Boolean](BooleanArrays.not(m.raw), m.layout) + else + val newArr = Array.ofDim[Boolean](m.numel) + m.layout.foreach2D { (i, j) => + newArr(i + j * m.rows) = !m.raw(m.layout.linearIndex(i, j)) + } + Matrix[Boolean](newArr, m.rows, m.cols) + end not + + /** Element-wise negation in place, writing through to the backing array (so any other view sharing it sees the + * change). + * + * Throws [[UnsupportedLayoutException]] for a view whose strides alias one backing element to several positions + * (e.g. a broadcast with a zero stride): each alias would flip the same element again, so the result would depend + * on how many times it is repeated. + */ + def `not!`: Unit = + if m.hasSimpleContiguousMemoryLayout then BooleanArrays.`not!`(m.raw) + else + if (m.rowStride == 0 && m.rows > 1) || (m.colStride == 0 && m.cols > 1) then + throw UnsupportedLayoutException( + s"not! cannot be applied in place to a view that aliases elements, got layout: ${m.layoutString}" + ) + end if + m.layout.foreach2D { (i, j) => + val idx = m.layout.linearIndex(i, j) + m.raw(idx) = !m.raw(idx) + } + end `not!` + + /** Element-wise logical and. Both matrices must have the same shape. */ + def &&(that: Matrix[Boolean]): Matrix[Boolean] = + sameDimMatCheck(m, that) + if sameDenseElementWiseMemoryLayoutCheck(m, that) then + Matrix[Boolean](BooleanArrayOps.and(m.raw, that.raw), m.layout) + else + val newArr = Array.ofDim[Boolean](m.numel) + m.layout.foreach2D { (i, j) => + newArr(i + j * m.rows) = m.raw(m.layout.linearIndex(i, j)) && that.raw(that.layout.linearIndex(i, j)) + } + Matrix[Boolean](newArr, m.rows, m.cols) + end if + end && + + /** Element-wise logical or. Both matrices must have the same shape. */ + def ||(that: Matrix[Boolean]): Matrix[Boolean] = + sameDimMatCheck(m, that) + if sameDenseElementWiseMemoryLayoutCheck(m, that) then + Matrix[Boolean](BooleanArrayOps.or(m.raw, that.raw), m.layout) + else + val newArr = Array.ofDim[Boolean](m.numel) + m.layout.foreach2D { (i, j) => + newArr(i + j * m.rows) = m.raw(m.layout.linearIndex(i, j)) || that.raw(that.layout.linearIndex(i, j)) + } + Matrix[Boolean](newArr, m.rows, m.cols) + end if + end || + end extension +end BooleanMatrix + +/** The array `&&` / `||` live in `BooleanArrays` on the JVM but in `arrayUtil` on JS and Native, so resolve them + * through `all` rather than naming either. Kept outside [[BooleanMatrix]], whose own `&&` / `||` would shadow them. + */ +private object BooleanArrayOps: + import vecxt.all.* + def and(a: Array[Boolean], b: Array[Boolean]): Array[Boolean] = a && b + def or(a: Array[Boolean], b: Array[Boolean]): Array[Boolean] = a || b +end BooleanArrayOps diff --git a/vecxt/src/all.scala b/vecxt/src/all.scala index 04db2bf4..b4c636fc 100644 --- a/vecxt/src/all.scala +++ b/vecxt/src/all.scala @@ -32,6 +32,7 @@ object all: export vecxt.JsFloatMatrix.* export vecxt.NativeFloatMatrix.* export vecxt.JvmIntMatrix.* + export vecxt.BooleanMatrix.* export vecxt.JsDoubleMatrix.* export vecxt.NativeDoubleMatrix.* export vecxt.DoubleMatrix.* diff --git a/vecxt/src/doublematrix.scala b/vecxt/src/doublematrix.scala index 4ddb6277..a513edd9 100644 --- a/vecxt/src/doublematrix.scala +++ b/vecxt/src/doublematrix.scala @@ -538,6 +538,11 @@ object DoubleMatrix: reduceAlongDimension(dim, _ * _, 1.0) end product + def mean(dim: DimensionExtender): Matrix[Double] = + val s = sum(dim) + s / (m.numel / s.numel).toDouble + end mean + // inline def - : Matrix[Double] = // Matrix(vecxt.doublearrays.*(m.raw)(-1), m.shape) diff --git a/vecxt/test/src/MatrixBoolean.test.scala b/vecxt/test/src/MatrixBoolean.test.scala index 9afb71df..50fcc07b 100644 --- a/vecxt/test/src/MatrixBoolean.test.scala +++ b/vecxt/test/src/MatrixBoolean.test.scala @@ -81,4 +81,150 @@ class MatrixBooleanSuite extends FunSuite: assertVecEquals[Double](calc.raw, result.raw) } + private def assertLogical(actual: Matrix[Boolean], expectedRows: Array[Boolean]*)(implicit + loc: munit.Location + ): Unit = + assertEquals(actual.rows, expectedRows.length) + for i <- 0 until actual.rows do + for j <- 0 until actual.cols do assertEquals(actual(i, j), expectedRows(i)(j), clue = s"at ($i, $j)") + end for + end for + end assertLogical + + // 2 x 2 view on the middle of a 3 x 3 col-major array (rows 1-2, cols 1-2). Non-contiguous: offset 4, colStride 3. + // col-major raw: col0 = (T, F, F), col1 = (T, T, F), col2 = (F, T, F) + // view: [ T, T ] + // [ F, F ] + private def strided: Matrix[Boolean] = + Matrix(Array[Boolean](true, false, false, true, true, false, false, true, false), 2, 2, 1, 3, 4) + + private def rowMajor: Matrix[Boolean] = + // [ T, F, T ] + // [ F, F, T ] + Matrix(Array[Boolean](true, false, true, false, false, true), 2, 3, 3, 1, 0) + + test("allTrue") { + assert(Matrix.fromRows[Boolean](Array(true, true), Array(true, true)).allTrue) + assert(!Matrix.fromRows[Boolean](Array(true, true), Array(false, true)).allTrue) + assert(!rowMajor.allTrue) + assert(!strided.allTrue) + + val big = Matrix(Array.fill(1025 * 3)(true), 1025, 3) + assert(big.allTrue) + big.raw(2000) = false + assert(!big.allTrue) + + val allTrueView = Matrix(Array(false, true, true, false, true, true), 2, 2, 1, 3, 1) + assert(allTrueView.allTrue) + } + + test("any") { + assert(!Matrix.zeros[Boolean]((3, 3)).any) + assert(Matrix.eye[Boolean](3).any) + assert(rowMajor.any) + assert(strided.any) + + val big = Matrix(Array.fill(1025 * 3)(false), 1025, 3) + assert(!big.any) + big.raw(2000) = true + assert(big.any) + + // Only elements outside the view are true. + val noneInView = Matrix(Array(true, false, false, true, false, false), 2, 2, 1, 3, 1) + assert(!noneInView.any) + } + + test("trues") { + assertEquals(Matrix.eye[Boolean](4).trues, 4) + assertEquals(Matrix.zeros[Boolean]((2, 5)).trues, 0) + assertEquals(rowMajor.trues, 3) + assertEquals(strided.trues, 2) + + val big = Matrix(Array.fill(1025 * 3)(true), 1025, 3) + assertEquals(big.trues, 3075) + } + + test("not - dense") { + val m = Matrix.fromRows[Boolean](Array(true, false, true), Array(false, true, false)) + val n = m.not + assertLogical(n, Array(false, true, false), Array(true, false, true)) + assertLogical(m, Array(true, false, true), Array(false, true, false)) // original untouched + + assertLogical(rowMajor.not, Array(false, true, false), Array(true, true, false)) + } + + test("not - strided view") { + val v = strided + val n = v.not + assertLogical(n, Array(false, false), Array(true, true)) + assert(n.isDenseColMajor) + assertLogical(v, Array(true, true), Array(false, false)) // view untouched + } + + test("not! - dense") { + val m = Matrix.fromRows[Boolean](Array(true, false), Array(false, false)) + m.`not!` + assertLogical(m, Array(false, true), Array(true, true)) + + val r = rowMajor + r.`not!` + assertLogical(r, Array(false, true, false), Array(true, true, false)) + } + + test("not! - strided view writes through and leaves the rest of the array alone") { + val v = strided + v.`not!` + assertLogical(v, Array(false, false), Array(true, true)) + assertVecEquals[Boolean](v.raw, Array(true, false, false, true, false, true, false, false, true)) + } + + test("not! - aliased (broadcast) view throws") { + val broadcastCol = Matrix(Array(true, false), 2, 3, 1, 0, 0) + intercept[UnsupportedLayoutException](broadcastCol.`not!`) + } + + test("&& and || - dense col-major") { + val a = Matrix.fromRows[Boolean](Array(true, true, false), Array(false, true, false)) + val b = Matrix.fromRows[Boolean](Array(true, false, false), Array(true, true, true)) + assertLogical(a && b, Array(true, false, false), Array(false, true, false)) + assertLogical(a || b, Array(true, true, false), Array(true, true, true)) + } + + test("&& and || - large dense exercises the SIMD path") { + val n = 1025 + val a = Matrix(Array.tabulate(n * 2)(_ % 2 == 0), n, 2) + val b = Matrix(Array.tabulate(n * 2)(_ % 3 == 0), n, 2) + val and = a && b + val or = a || b + for k <- 0 until n * 2 do + assertEquals(and.raw(k), k % 6 == 0, clue = s"and at $k") + assertEquals(or.raw(k), k % 2 == 0 || k % 3 == 0, clue = s"or at $k") + end for + } + + test("&& and || - dense row-major") { + val other = Matrix(Array(true, true, false, true, false, true), 2, 3, 3, 1, 0) + // other: [ T, T, F ] + // [ T, F, T ] + assertLogical(rowMajor && other, Array(true, false, false), Array(false, false, true)) + assertLogical(rowMajor || other, Array(true, true, true), Array(true, false, true)) + } + + test("&& and || - mixed layouts") { + val colMajor = Matrix.fromRows[Boolean](Array(true, true, false), Array(true, false, true)) + assertLogical(rowMajor && colMajor, Array(true, false, false), Array(false, false, true)) + assertLogical(colMajor || rowMajor, Array(true, true, true), Array(true, false, true)) + + val dense = Matrix.fromRows[Boolean](Array(false, true), Array(true, false)) + assertLogical(strided && dense, Array(false, true), Array(false, false)) + assertLogical(dense || strided, Array(true, true), Array(true, false)) + } + + test("&& and || - dimension mismatch throws") { + val a = Matrix.zeros[Boolean]((2, 2)) + val b = Matrix.zeros[Boolean]((2, 3)) + intercept[MatrixDimensionMismatch](a && b) + intercept[MatrixDimensionMismatch](a || b) + } + end MatrixBooleanSuite diff --git a/vecxt/test/src/matrix.test.scala b/vecxt/test/src/matrix.test.scala index eb5aa254..0fdb305a 100644 --- a/vecxt/test/src/matrix.test.scala +++ b/vecxt/test/src/matrix.test.scala @@ -145,6 +145,45 @@ class MatrixExtensionSuite extends FunSuite: assertMatrixEquals(prodC, Matrix[Double](Array[Double](8.0, 90.0), (1, 2))) } + // mean(dim) divides sum(dim) by the length of the collapsed axis. Non-square matrices throughout, so dividing by + // the kept axis instead (rows vs cols) would give a different answer. + + test("mean along dimension, column-major") { + // [[1,5],[4,3],[2,6]] + val mat1 = Matrix[Double](Array(1.0, 4.0, 2.0, 5.0, 3.0, 6.0), (3, 2)) + + assertMatrixEquals(mat1.mean(Rows), Matrix[Double](Array(3.0, 3.5, 4.0), (3, 1))) + assertMatrixEquals(mat1.mean(Cols), Matrix[Double](Array(7.0 / 3, 14.0 / 3), (1, 2))) + assertMatrixEquals(mat1.mean(0), mat1.mean(Rows)) + assertMatrixEquals(mat1.mean(1), mat1.mean(Cols)) + } + + test("mean along dimension, row-major") { + val mat1 = Matrix[Double](Array(1.0, 5.0, 4.0, 3.0, 2.0, 6.0), 3, 2, 2, 1, 0) + assert(mat1.isDenseRowMajor) + + assertMatrixEquals(mat1.mean(Rows), Matrix[Double](Array(3.0, 3.5, 4.0), (3, 1))) + assertMatrixEquals(mat1.mean(Cols), Matrix[Double](Array(7.0 / 3, 14.0 / 3), (1, 2))) + } + + test("mean along dimension, strided view") { + // [[1,2,3],[4,5,6]] sliced out of the 3x3 + val view = mat1to9(0 to 1, 0 to 2) + + assertMatrixEquals(view.mean(Rows), Matrix[Double](Array(2.0, 5.0), (2, 1))) + assertMatrixEquals(view.mean(Cols), Matrix[Double](Array(2.5, 3.5, 4.5), (1, 3))) + } + + test("mean along dimension agrees with overall mean") { + val mat1 = Matrix[Double](Array(1.0, 4.0, 2.0, 5.0, 3.0, 6.0), (3, 2)) + assertEqualsDouble(mat1.mean(Rows).mean, mat1.mean, 1e-12) + assertEqualsDouble(mat1.mean(Cols).mean, mat1.mean, 1e-12) + } + + test("mean along invalid dimension throws") { + intercept[InvalidDimensionException](mat1to9.mean(2)) + } + // reduceAlongDimension (which backs sum/min/max/product along a dimension) has no dedicated test at all for // Float or Int, row-major or column-major — this covers the same bug as the Double row-major tests above, for // both remaining types it was fixed in, using the same logical matrix ([[1,5],[4,3],[2,6]]) throughout. diff --git a/vecxt_re/src/LossMatrix.scala b/vecxt_re/src/LossMatrix.scala new file mode 100644 index 00000000..1addf125 --- /dev/null +++ b/vecxt_re/src/LossMatrix.scala @@ -0,0 +1,37 @@ +package vecxt_re + +import vecxt.all.Matrix +import vecxt.dimensionExtender.DimensionExtender.Dimension +import vecxt.DoubleMatrix.reduceAlongDimension +import vecxt.all.mean +import vecxt.all.sum +import vecxt.all.> + import vecxt.all.trues + +/** + * The assumption here. + * + * Potential future outcomes of a single transactions run down columns. + * + * Each row represents one potential future outcome, across a portfolio + * + */ +opaque type LossMatrix = Matrix[Double] + +object LossMatrix: + inline def apply(m: Matrix[Double]): LossMatrix = m +end LossMatrix + +extension (m: LossMatrix) + def transactionEls = m.mean(Dimension.Cols) + + def el = m.mean + + /** + * The probability of paying any loss + * + * @return + */ + def attachProb = (m.sum(Dimension.Rows) > 0).trues.toDouble / m.rows.toDouble + + diff --git a/vecxt_re/test/src/lossMatrix.test.scala b/vecxt_re/test/src/lossMatrix.test.scala new file mode 100644 index 00000000..b5c67106 --- /dev/null +++ b/vecxt_re/test/src/lossMatrix.test.scala @@ -0,0 +1,59 @@ +package vecxt_re + +import vecxt.all.* + +class LossMatrixSuite extends munit.FunSuite: + + test("attachProb is the fraction of outcomes (rows) with a positive portfolio loss") { + // 4 outcomes x 3 transactions. Rows 1 and 3 attach; row 3 only through a single transaction. + val losses = LossMatrix( + Matrix.fromRows[Double]( + Array(0.0, 0.0, 0.0), + Array(10.0, 0.0, 5.0), + Array(0.0, 0.0, 0.0), + Array(0.0, 0.0, 0.5) + ) + ) + assertEqualsDouble(losses.attachProb, 0.5, 1e-12) + } + + test("attachProb is 0 when nothing attaches and 1 when every outcome attaches") { + assertEqualsDouble(LossMatrix(Matrix.zeros[Double]((7, 3))).attachProb, 0.0, 1e-12) + assertEqualsDouble(LossMatrix(Matrix.fill(1.0, (7, 3))).attachProb, 1.0, 1e-12) + } + + test("attachProb of a single-transaction portfolio") { + val losses = LossMatrix(Matrix(Array(0.0, 1.0, 0.0, 2.0, 3.0), 5, 1)) + assertEqualsDouble(losses.attachProb, 3.0 / 5.0, 1e-12) + } + + test("attachProb does not depend on storage order") { + // Same logical matrix as the first test, stored row-major. + val rowMajor = Matrix( + Array(0.0, 0.0, 0.0, 10.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5), + 4, + 3, + 3, + 1, + 0 + ) + assertEqualsDouble(LossMatrix(rowMajor).attachProb, 0.5, 1e-12) + } + + test("attachProb matches a naive count on a large sparse portfolio") { + val rng = new scala.util.Random(42) + val rows = 10007 // not a multiple of any SIMD width + val cols = 5 + // ~90% of individual losses are zero, so a good share of outcomes don't attach at all. + val raw = Array.fill(rows * cols)(if rng.nextDouble() < 0.9 then 0.0 else rng.nextDouble() * 100) + val m = Matrix(raw, rows, cols) + + var attaching = 0 + for i <- 0 until rows do if (0 until cols).exists(j => m(i, j) > 0) then attaching += 1 + end for + + assert(attaching > 0 && attaching < rows) + assertEqualsDouble(LossMatrix(m).attachProb, attaching.toDouble / rows, 1e-12) + } + +end LossMatrixSuite From b6ffa38909123d3af6975c508a189a47a5b07d4f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:42:31 +0000 Subject: [PATCH 5/6] [autofix.ci] apply automated fixes --- vecxt_re/src/LossMatrix.scala | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/vecxt_re/src/LossMatrix.scala b/vecxt_re/src/LossMatrix.scala index 1addf125..99dc886d 100644 --- a/vecxt_re/src/LossMatrix.scala +++ b/vecxt_re/src/LossMatrix.scala @@ -6,15 +6,13 @@ import vecxt.DoubleMatrix.reduceAlongDimension import vecxt.all.mean import vecxt.all.sum import vecxt.all.> - import vecxt.all.trues +import vecxt.all.trues -/** - * The assumption here. - * +/** The assumption here. + * * Potential future outcomes of a single transactions run down columns. - * + * * Each row represents one potential future outcome, across a portfolio - * */ opaque type LossMatrix = Matrix[Double] @@ -22,16 +20,14 @@ object LossMatrix: inline def apply(m: Matrix[Double]): LossMatrix = m end LossMatrix -extension (m: LossMatrix) +extension (m: LossMatrix) def transactionEls = m.mean(Dimension.Cols) def el = m.mean - /** - * The probability of paying any loss + /** The probability of paying any loss * * @return */ def attachProb = (m.sum(Dimension.Rows) > 0).trues.toDouble / m.rows.toDouble - - +end extension From db49dbaec28f791a2f42d200441314c583fc73a1 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Wed, 23 Sep 2026 17:53:48 +0200 Subject: [PATCH 6/6] . --- build.mill | 2 +- ...eturn.scala => PositionCalculations.scala} | 0 vecxt_re/test/src/lossMatrix.test.scala | 59 ------------------- 3 files changed, 1 insertion(+), 60 deletions(-) rename vecxt_re/src/{PositionReturn.scala => PositionCalculations.scala} (100%) delete mode 100644 vecxt_re/test/src/lossMatrix.test.scala diff --git a/build.mill b/build.mill index d7a5bdd9..29da45a8 100644 --- a/build.mill +++ b/build.mill @@ -1,6 +1,6 @@ //| mill-version: 1.1.9 //| mill-jvm-version: 25 -//| mill-jvm-opts: [ "--add-modules", "jdk.incubator.vector"] +//| mill-jvm-opts: [ "--add-modules", "jdk.incubator.vector", -XX:TrimNativeHeapInterval=60000] //| mvnDeps: //| - io.github.quafadas:millSite_mill1_3.8:0.0.57 //| - com.goyeau:mill-scalafix_mill1_3:0.6.0 diff --git a/vecxt_re/src/PositionReturn.scala b/vecxt_re/src/PositionCalculations.scala similarity index 100% rename from vecxt_re/src/PositionReturn.scala rename to vecxt_re/src/PositionCalculations.scala diff --git a/vecxt_re/test/src/lossMatrix.test.scala b/vecxt_re/test/src/lossMatrix.test.scala deleted file mode 100644 index b5c67106..00000000 --- a/vecxt_re/test/src/lossMatrix.test.scala +++ /dev/null @@ -1,59 +0,0 @@ -package vecxt_re - -import vecxt.all.* - -class LossMatrixSuite extends munit.FunSuite: - - test("attachProb is the fraction of outcomes (rows) with a positive portfolio loss") { - // 4 outcomes x 3 transactions. Rows 1 and 3 attach; row 3 only through a single transaction. - val losses = LossMatrix( - Matrix.fromRows[Double]( - Array(0.0, 0.0, 0.0), - Array(10.0, 0.0, 5.0), - Array(0.0, 0.0, 0.0), - Array(0.0, 0.0, 0.5) - ) - ) - assertEqualsDouble(losses.attachProb, 0.5, 1e-12) - } - - test("attachProb is 0 when nothing attaches and 1 when every outcome attaches") { - assertEqualsDouble(LossMatrix(Matrix.zeros[Double]((7, 3))).attachProb, 0.0, 1e-12) - assertEqualsDouble(LossMatrix(Matrix.fill(1.0, (7, 3))).attachProb, 1.0, 1e-12) - } - - test("attachProb of a single-transaction portfolio") { - val losses = LossMatrix(Matrix(Array(0.0, 1.0, 0.0, 2.0, 3.0), 5, 1)) - assertEqualsDouble(losses.attachProb, 3.0 / 5.0, 1e-12) - } - - test("attachProb does not depend on storage order") { - // Same logical matrix as the first test, stored row-major. - val rowMajor = Matrix( - Array(0.0, 0.0, 0.0, 10.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5), - 4, - 3, - 3, - 1, - 0 - ) - assertEqualsDouble(LossMatrix(rowMajor).attachProb, 0.5, 1e-12) - } - - test("attachProb matches a naive count on a large sparse portfolio") { - val rng = new scala.util.Random(42) - val rows = 10007 // not a multiple of any SIMD width - val cols = 5 - // ~90% of individual losses are zero, so a good share of outcomes don't attach at all. - val raw = Array.fill(rows * cols)(if rng.nextDouble() < 0.9 then 0.0 else rng.nextDouble() * 100) - val m = Matrix(raw, rows, cols) - - var attaching = 0 - for i <- 0 until rows do if (0 until cols).exists(j => m(i, j) > 0) then attaching += 1 - end for - - assert(attaching > 0 && attaching < rows) - assertEqualsDouble(LossMatrix(m).attachProb, attaching.toDouble / rows, 1e-12) - } - -end LossMatrixSuite