Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

<!-- towncrier release notes start -->

## 0.52.4
### 2026-03-16
* NEW: add `ConstTrue` / `ConstFalse` for bool type formula



## 0.52.3
### 2026-03-05
* NEW: add `IrrOfBond` as a formula, which return the irr of the bond
* NEW: add `IsAnyOutstanding` as formula: return `True` if any of the bond is outstanding
* NEW: add `PoolAccruedInterest` as a formula: return the accural amount of the pool

* ENHANCEMENT: allow `negative amount` when calculating `AmountRequiredForIRR`

## 0.51.6
### 2025-09-05
* NEW: add new integer formula `activeBondNumber`
Expand Down
9 changes: 5 additions & 4 deletions Hastructure.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ cabal-version: 3.0
-- see: https://github.com/sol/hpack

name: Hastructure
version: 0.52.0
version: 0.52.4
synopsis: Cashflow modeling library for structured finance
description: Please see the README on GitHub at <https://github.com/absbox/Hastructure#readme>
category: StructuredFinance,Securitisation,Cashflow
homepage: https://github.com/absbox/Hastructure#readme
bug-reports: https://github.com/absbox/Hastructure/issues
author: Xiaoyu
maintainer: always.zhang@gmail.com
copyright: 2025 Xiaoyu, Zhang
copyright: 2026 Xiaoyu, Zhang
license: BSD-3-Clause
license-file: LICENSE
build-type: Simple
Expand Down Expand Up @@ -58,6 +58,7 @@ library
Expense
Hedge
InterestRate
Interface
Ledger
Liability
Lib
Expand Down Expand Up @@ -90,13 +91,13 @@ library
regex-base >= 0.94.0 && < 0.95,
aeson >= 2.2.3 && < 2.3,
aeson-gadt-th >= 0.2.5.4 && < 0.3,
hashable >= 1.4.7 && < 1.5.1,
hashable >= 1.4 && <= 1.5.1.0,
dlist >= 1.0 && < 1.1,
scientific >= 0.3.8 && < 0.4,
vector >= 0.13.2 && < 0.14,
aeson-pretty >= 0.8.10 && < 0.9,
base-compat >= 0.13.0 && < 0.15,
lens >= 5.2.3 && < 5.3.6,
lens >= 5.2.3 && < 5.4,
parallel >= 3.2.2 && < 3.3,
math-functions >= 0.3.4 && < 0.4,
monad-loops >= 0.4.3 && < 0.5,
Expand Down
43 changes: 2 additions & 41 deletions app/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ debug = flip Debug.Trace.trace


version1 :: Version
version1 = Version "0.52.0"
version1 = Version "0.52.4"


wrapRun :: [D.ExpectReturn] -> DealType -> Maybe AP.ApplyAssumptionType -> AP.NonPerfAssumption -> RunResp
Expand Down Expand Up @@ -268,15 +268,6 @@ modifyDealType dm f (UDeal d) = UDeal $ DM.modDeal dm f d
modifyDealType dm f (VDeal d) = VDeal $ DM.modDeal dm f d
modifyDealType dm f (PDeal d) = PDeal $ DM.modDeal dm f d

queryDealType :: DealType -> Date -> DealStats -> Either String Rational
queryDealType (MDeal _d) = Q.queryCompound _d
queryDealType (RDeal _d) = Q.queryCompound _d
queryDealType (IDeal _d) = Q.queryCompound _d
queryDealType (LDeal _d) = Q.queryCompound _d
queryDealType (FDeal _d) = Q.queryCompound _d
queryDealType (UDeal _d) = Q.queryCompound _d
queryDealType (VDeal _d) = Q.queryCompound _d
queryDealType (PDeal _d) = Q.queryCompound _d

queryClosingDate :: DealType -> Either String Date
queryClosingDate (MDeal _d) = DD.getClosingDate (DB.dates _d)
Expand All @@ -289,25 +280,7 @@ queryClosingDate (VDeal _d) = DD.getClosingDate (DB.dates _d)
queryClosingDate (PDeal _d) = DD.getClosingDate (DB.dates _d)


queryDealTypeBool :: DealType -> Date -> DealStats -> Either String Bool
queryDealTypeBool (MDeal _d) d s = Q.queryDealBool _d s d
queryDealTypeBool (RDeal _d) d s = Q.queryDealBool _d s d
queryDealTypeBool (IDeal _d) d s = Q.queryDealBool _d s d
queryDealTypeBool (LDeal _d) d s = Q.queryDealBool _d s d
queryDealTypeBool (FDeal _d) d s = Q.queryDealBool _d s d
queryDealTypeBool (UDeal _d) d s = Q.queryDealBool _d s d
queryDealTypeBool (VDeal _d) d s = Q.queryDealBool _d s d
queryDealTypeBool (PDeal _d) d s = Q.queryDealBool _d s d

testDealTypeBool :: DealType -> Date -> Pre -> Either String Bool
testDealTypeBool (MDeal _d) d p = Q.testPre d _d p
testDealTypeBool (RDeal _d) d p = Q.testPre d _d p
testDealTypeBool (IDeal _d) d p = Q.testPre d _d p
testDealTypeBool (LDeal _d) d p = Q.testPre d _d p
testDealTypeBool (FDeal _d) d p = Q.testPre d _d p
testDealTypeBool (UDeal _d) d p = Q.testPre d _d p
testDealTypeBool (VDeal _d) d p = Q.testPre d _d p
testDealTypeBool (PDeal _d) d p = Q.testPre d _d p


getDealBondMap :: DealType -> Map.Map BondName L.Bond
getDealBondMap (MDeal d) = DB.bonds d
Expand Down Expand Up @@ -395,18 +368,6 @@ evalRootFindStop (BondMetTargetIrr bn target) (dt,_,_,pResult,osPflow)
Nothing -> -1 -- `debug` ("No IRR found for bond:"++ show bn)
Just irr -> (fromRational . toRational) $ irr - target -- `debug` ("IRR for bond:"++ show target ++" is "++ show irr)

evalRootFindStop (BalanceFormula ds targetBal) (dt,collectedFlow,logs,_,osPflow)
= let
_date = case find (\(EndRun d msg) -> True) (reverse logs) of
Just (EndRun (Just d) _ ) -> d
Nothing -> case queryClosingDate dt of
Right d' -> d'
Left err -> error $ "Error in BalanceFormula: " ++ err
v = case queryDealType dt _date (Q.patchDateToStats _date ds) of
Right v' -> fromRational v'
Left err -> error $ "Error in BalanceFormula: " ++ err
in
(fromRational . toRational) $ v - targetBal -- `debug` ("querydate" ++ show _date++"iteration" ++ show v ++ " target:" ++ show targetBal ++ ">> " ++ show ( v- targetBal))



Expand Down
1 change: 1 addition & 0 deletions shell.nix
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pkgs.mkShell {
cabal2nix
haskell.compiler.ghc912
haskell-language-server
python313Packages.towncrier
ghciwatch
just
];
Expand Down
7 changes: 5 additions & 2 deletions src/Accounts.hs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Types
import Lib
import Util
import DateUtil
import Interface
import Data.Aeson hiding (json)
import Language.Haskell.TH
import Data.Aeson.TH
Expand Down Expand Up @@ -52,14 +53,14 @@ data ReserveAmount

data Account = Account {
accBalance :: Balance -- ^ account current balance
,accName :: String -- ^ account name
,accName :: AccountName -- ^ account name
,accInterest :: Maybe InterestInfo -- ^ account reinvestment interest
,accType :: Maybe ReserveAmount -- ^ target info if a reserve account
,accStmt :: Maybe Statement -- ^ transactional history
} deriving (Show, Generic, Eq, Ord)

-- | build interest earn actions
buildEarnIntAction :: [Account] -> Date -> [(String,Dates)] -> [(String,Dates)]
buildEarnIntAction :: [Account] -> Date -> [(AccountName,Dates)] -> [(AccountName,Dates)]
buildEarnIntAction [] ed r = r
buildEarnIntAction (acc:accs) ed r =
case accInterest acc of
Expand All @@ -69,6 +70,8 @@ buildEarnIntAction (acc:accs) ed r =
Just (InvestmentAccount _ _ dp _ lastAccDate _)
-> buildEarnIntAction accs ed [(accName acc, genSerialDatesTill2 NO_IE lastAccDate dp ed)]++r


-- | accrue interest from last reset date to today
accrueInt :: Date -> Account -> Balance
accrueInt _ (Account _ _ Nothing _ _) = 0
-- ^ bank account type interest
Expand Down
37 changes: 20 additions & 17 deletions src/Analytics.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import Language.Haskell.TH
import Data.Aeson.TH
import Data.Aeson.Types
import Data.Ord (comparing)
import Data.List (sortBy)
import Data.List (sortBy, uncons)
import GHC.Generics
import Data.Ratio
import Numeric.RootFinding
Expand Down Expand Up @@ -61,8 +61,8 @@ initialSlopes points@((t0, y0):rest) =
let deltas = secantSlopes points
n = length points
slopes = [head deltas] ++ -- First slope: use first secant slope
[ (d1 + d2) / 2 | (d1, d2) <- zip deltas (tail deltas) ] ++ -- Interior slopes
[last deltas] -- Last slope: use last secant slope
[ (d1 + d2) / 2 | (d1, d2) <- zip deltas (tail deltas) ] ++ -- Interior slopes
[last deltas] -- Last slope: use last secant slope
in slopes

fritschCarlsonSlopes :: [TimeYield] -> [Double]
Expand Down Expand Up @@ -239,9 +239,9 @@ fv2 discount_rate today futureDay amt

calcPvFromIRR :: Double -> [Date] -> [Amount] -> Date -> Double -> Double
calcPvFromIRR irr [] _ d amt = 0
calcPvFromIRR irr ds vs d amt =
calcPvFromIRR irr ds@(sd:_) vs d amt =
let
begDate = head ds
begDate = sd
vs' = fromRational . toRational <$> vs
pv = pv22 irr begDate (ds++[d]) (vs'++[amt])
in
Expand All @@ -255,27 +255,30 @@ calcRequiredAmtForIrrAtDate irr ds vs d =
itertimes = 500
def = RiddersParam { riddersMaxIter = itertimes, riddersTol = RelTol 0.00000001}
in
case ridders def (0.0001,100000000000000) (calcPvFromIRR irr ds vs d) of
case ridders def (-100000000000.0,100000000000000) (calcPvFromIRR irr ds vs d) of
Root finalAmt -> Just (fromRational (toRational finalAmt))
_ -> Nothing
error -> Nothing -- `debug` ("calcRequiredAmtForIrrAtDate: error"++ show error)

-- ^ calc IRR from a cashflow
calcIRR :: [Date] -> [Amount] -> Either String Rate
calcIRR :: [Date] -> [Amount] -> Either ErrorRep Rate
calcIRR _ [] = Left "No cashflow amount"
calcIRR [] _ = Left "No cashflow date"
calcIRR ds vs
| all (>= 0) vs = Left $ "All cashflow can't be all positive:"++ show vs
| all (<= 0) vs = Left $ "All cashflow can't be all negative:"++ show vs
| all (<= 0) vs = return $ -1.0
| all (== 0) vs = Left "All cashflow can't be all zeros"
| otherwise =
let
itertimes = 1000
def = RiddersParam { riddersMaxIter = itertimes, riddersTol = RelTol 0.000001}
beginDate = head ds
vs' = fromRational . toRational <$> vs
sumOfPv irr = pv22 irr beginDate ds vs'
in
case ridders def (-1,1000) sumOfPv of
Root irrRate -> Right $ toRational irrRate
NotBracketed -> Left $ "IRR: not bracketed" ++ show vs' ++ " and dates"++ show ds
SearchFailed -> Left $ "IRR: search failed: can't be calculated with input "++ show vs++" and dates"++ show ds
in
case uncons ds of
Nothing -> Left "calcIRR: empty dates"
Just (beginDate, _) ->
let
sumOfPv irr = pv22 irr beginDate ds vs'
in
case ridders def (-1,1000) sumOfPv of
Root irrRate -> return $ toRational irrRate
NotBracketed -> Left $ "IRR: not bracketed" ++ show vs' ++ " and dates"++ show ds
SearchFailed -> Left $ "IRR: search failed: can't be calculated with input "++ show vs++" and dates"++ show ds
32 changes: 18 additions & 14 deletions src/Asset.hs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

module Asset ( Asset(..),
buildAssumptionPpyDefRecRate,buildAssumptionPpyDelinqDefRecRate
,calcRecoveriesFromDefault,getCurBalance
,calcRecoveriesFromDefault
,priceAsset,applyHaircut,buildPrepayRates,buildDefaultRates,getObligorFields
,getObligorTags,getObligorId,getRecoveryLagAndRate,getDefaultDelinqAssump,getOriginInfo
) where
Expand Down Expand Up @@ -157,11 +157,15 @@ applyExtraStress Nothing _ ppy def = (ppy,def)
applyExtraStress (Just ExtraStress{A.defaultFactors= mDefFactor
,A.prepaymentFactors = mPrepayFactor}) ds ppy def =
case (mPrepayFactor,mDefFactor) of
(Nothing,Nothing) -> (ppy,def)
(Nothing,Just defFactor) -> (ppy ,getTsVals $ multiplyTs Exc (zipTs ds def) defFactor)
(Just ppyFactor,Nothing) -> (getTsVals $ multiplyTs Exc (zipTs ds ppy) ppyFactor, def)
(Just ppyFactor,Just defFactor) -> (getTsVals $ multiplyTs Exc (zipTs ds ppy) ppyFactor
,getTsVals $ multiplyTs Exc (zipTs ds def) defFactor)
(Nothing,Nothing)
-> (ppy,def)
(Nothing,Just defFactor)
-> (ppy ,getTsVals $ multiplyTs Exc (zipTs ds def) defFactor)
(Just ppyFactor,Nothing)
-> (getTsVals $ multiplyTs Exc (zipTs ds ppy) ppyFactor, def)
(Just ppyFactor,Just defFactor)
-> (getTsVals $ multiplyTs Exc (zipTs ds ppy) ppyFactor
,getTsVals $ multiplyTs Exc (zipTs ds def) defFactor)

-- ^ convert annual CPR to single month mortality
cpr2smm :: Rate -> Rate
Expand All @@ -183,13 +187,13 @@ buildPrepayRates a ds (Just (A.PrepaymentCPR r))
buildPrepayRates a ds (Just (A.PrepaymentVec vs))
| any (> 1.0) vs || any (< 0.0) vs = Left $ "buildPrepayRates: prepayment vector should be between 0 and 1, got " ++ show vs
| otherwise = return $ zipWith Util.toPeriodRateByInterval
(paddingDefault 0.0 vs (pred (length ds)))
(getIntervalDays ds)
(paddingDefault 0.0 vs (pred (length ds)))
(getIntervalDays ds)
buildPrepayRates a ds (Just (A.PrepaymentVecPadding vs))
| any (> 1.0) vs || any (< 0.0) vs = Left $ "buildPrepayRates: prepayment vector should be between 0 and 1, got " ++ show vs
| otherwise = return $ zipWith Util.toPeriodRateByInterval
(paddingDefault (last vs) vs (pred (length ds)))
(getIntervalDays ds)
(paddingDefault (last vs) vs (pred (length ds)))
(getIntervalDays ds)
buildPrepayRates a ds (Just (A.PrepayStressByTs ts x))
| any (< 0.0) (getTsVals ts) = Left $ "buildPrepayRates: prepayment vector by ts should be non-negative, got " ++ show (getTsVals ts)
| otherwise = do
Expand Down Expand Up @@ -229,13 +233,13 @@ buildDefaultRates a ds (Just (A.DefaultCDR r))
buildDefaultRates a ds (Just (A.DefaultVec vs))
| any (> 1.0) vs || any (< 0.0) vs = Left $ "buildDefaultRates: default vector should be between 0 and 1, got " ++ show vs
| otherwise = return $ zipWith Util.toPeriodRateByInterval
(paddingDefault 0.0 vs (pred (length ds)))
(getIntervalDays ds)
(paddingDefault 0.0 vs (pred (length ds)))
(getIntervalDays ds)
buildDefaultRates a ds (Just (A.DefaultVecPadding vs))
| any (> 1.0) vs || any (< 0.0) vs = Left $ "buildDefaultRates: default vector should be between 0 and 1, got " ++ show vs
| otherwise = return $ zipWith Util.toPeriodRateByInterval
(paddingDefault (last vs) vs (pred (length ds)))
(getIntervalDays ds)
(paddingDefault (last vs) vs (pred (length ds)))
(getIntervalDays ds)
buildDefaultRates a ds (Just (A.DefaultAtEndByRate r rAtEnd))
| r > 1.0 || r < 0.0 = Left $ "buildDefaultRates: default at end rate should be between 0 and 1, got " ++ show r
| rAtEnd > 1.0 || rAtEnd < 0.0 = Left $ "buildDefaultRates: default at end rate should be between 0 and 1, got " ++ show rAtEnd
Expand Down
23 changes: 14 additions & 9 deletions src/AssetClass/AssetBase.hs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import Util
import qualified Data.Map as Map
import qualified InterestRate as IR
import qualified Cashflow as CF
-- import Assumptions (RevolvingAssumption(Dummy4))
import Control.Lens hiding (element,Index)
import Control.Lens.TH

Expand All @@ -54,14 +53,15 @@ data AmortPlan = Level -- ^ for mortgage / french system ->

-- | calculate period payment (Annuity/Level mortgage)
calcPmt :: Balance -> IRate -> Int -> Amount
calcPmt bal rate periods | rate == 0.0 = divideBI bal periods
| otherwise =
let rate' = realToFrac rate :: Double
logBase = log (1 + rate')
num = exp (logBase * fromIntegral periods)
den = num - 1
r1 = num / den
in mulBR (realToFrac bal) (toRational (rate' * r1))
calcPmt bal rate periods
| rate == 0.0 = divideBI bal periods
| otherwise =
let rate' = realToFrac rate :: Double
logBase = log (1 + rate')
num = exp (logBase * fromIntegral periods)
den = num - 1
r1 = num / den
in mulBR (realToFrac bal) (toRational (rate' * r1))

type InterestAmount = Balance
type PrincipalAmount = Balance
Expand Down Expand Up @@ -200,6 +200,11 @@ data Mortgage = Mortgage OriginalInfo Balance IRate RemainTerms (Maybe BorrowerN
| ScheduleMortgageFlow Date [CF.TsRow] DatePattern
deriving (Show,Generic,Eq,Ord)

data StudentLoan = StudentLoan OriginalInfo Balance IRate RemainTerms Status
| DUMMY3
deriving (Show,Generic,Eq,Ord)


type FixRatePortion = (Rate, IRate)
type FloatRatePortion = (Rate, IRate, Spread, Index)
type ScheduleBalance = (Date, Balance)
Expand Down
2 changes: 1 addition & 1 deletion src/AssetClass/MixedAsset.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import qualified Cashflow as CF -- (Cashflow,Amount,Interests,Principals)
import qualified Assumptions as A
import qualified AssetClass.AssetBase as ACM
import InterestRate
import Interface
import qualified Asset as P
import Lib
import Util
Expand All @@ -31,7 +32,6 @@ import AssetClass.Mortgage
import AssetClass.Lease
import AssetClass.Loan
import AssetClass.Installment

import AssetClass.Receivable
import AssetClass.AssetCashflow
import AssetClass.FixedAsset
Expand Down
Loading
Loading