Skip to the content.

Looking for a YTM function, I found this article the most promising: https://www.insidesql.org/blogs/frankkalis/2006/01/25/yield-to-maturity. However, even with a nice explanation, the code was only rudimentary (on purpose by the author), so I decided to publish a usable version for bonds with act/act pricing convention (most European bonds, except Bulgaria, Croatia, Malta, UK and Italy).

The function returns -999.9 in case of obvious input errors, no checking is done for arithmetic overflows in the used newton method for finding the root. For the denominator of the act/act daycount fraction I used an approximate average to not slow down the function further by calculating the actual average.

CREATE FUNCTION yield_to_maturity(@price_date DATETIME, @maturity DATETIME, @coupon decimal(9,6), @price decimal(9,6))
RETURNS float
AS
BEGIN
 DECLARE @ytm_prev float
 DECLARE @ytm float
 DECLARE @pv_prev float
 DECLARE @pv float
 DECLARE @accrued float
 DECLARE @cash_flows TABLE (cashflow decimal(9,6), value_date datetime)
 DECLARE @cf_date datetime
 
 -- sanity checks, return "impossible value"
 IF @maturity < @price_date
  RETURN -999.9
 IF @price IS NULL OR @maturity IS NULL OR @price_date IS NULL OR @coupon IS NULL
  RETURN -999.9

 -- set up cashflows, first add the bond coupons, backdated from maturity
 SET @cf_date = @maturity
 WHILE @cf_date > @price_date
 BEGIN
  INSERT INTO @cash_flows (cashflow, value_date) VALUES (@coupon, @cf_date)
  SET @cf_date = DATEADD(year,-1,@cf_date)
 END

 -- @cf_date is now the last coupon date, used to calculate accrued from coupon to determine dirty price
 SET @accrued = @coupon * cast(DATEDIFF(day, @cf_date, @price_date) as float)/DATEDIFF(day,@cf_date,DATEADD(year,1,@cf_date))
 -- bond redemption: final cashflow
 INSERT INTO @cash_flows (cashflow, value_date) VALUES (100.0, @maturity)
 -- bond purchase: first cashflow
 INSERT INTO @cash_flows (cashflow, value_date) VALUES (-(@price+@accrued), @price_date)

 -- initialize newton method, initial ytm set to 1%
 SET @ytm_prev = 0
 SET @ytm = 0.01
 SELECT @pv_prev = SUM(cashflow) FROM @cash_flows
 -- calculate pv using initial ytm as total discounted sum of cashflows using act/act daycount
 SET @pv = (SELECT SUM(cashflow/POWER(1+@ytm,cast(DATEDIFF(day,@price_date, value_date) as float)/365.25)) FROM @cash_flows)

 -- iterate with newton method until either accuracy goal reached or x- differential zero (would lead to div/0 otherwise)
 WHILE ABS(@pv) >= 0.000001 AND (@pv - @pv_prev) <> 0
 BEGIN
  DECLARE @t float
  SET @t = @ytm_prev
  SET @ytm_prev = @ytm
  SET @ytm = @ytm + (@t-@ytm)*@pv/(@pv - @pv_prev)
  SET @pv_prev = @pv
  SET @pv = (SELECT SUM(cashflow/POWER(1+@ytm,cast(DATEDIFF(day,@price_date, value_date) as float)/365.25) FROM @cash_flows)
 END

 RETURN @ytm
END
GO