There are a lot of things wrong in here:
- WSJ does not seem to display the quotes properly. According to the website, Tullett Prebon is the source. Tullett is a very reputable broker with high quality data. The quotes are most likely received following market standard, which is to quote in 1/32nd. When you asked the question, the quote was 98-25+ when I looked at it, which is equal to 98 + 25/32 + 1/64 ~98.7968.
- Your daycount is wrong because you need to use ACT/ACT and the period for the last coupon payment is only 181 days.
- The way you solve for YTM is also not how it usually works. Generally, you have NPV = cashflow / (1+ytm*dcf) where dcf stands for daycount fraction and NPV is net present value (the current bond price).
Below is a screenshot from Bloomberg
YAS, on the day you asked the question. I use bid price because my
YAS screen is set to default to bid. There is no difference computation wise.
Below, I will replicate the computation in Julia. There is no need to know the language. I used names similar to the Bloomberg screen and the code is mainly "mathematical" expressions. The relevant code is in 3 sections:
- Firstly, I compute all relevant dates and daycount fractions.
- Secondly, clean price, accrued interest and dirty price is computed
- Lastly, ytm is used to show the final value is indeed the result from dirty price * ytm (adjusted for proper daycount). I manually compute YTM and show that the logic the OP used to compute YTM is called Equiv 1/Yr in Bloomberg. This however is not the conventionally computed yield for treasuries.
In general, yield computations are more often than not depending on a lot of details and treasury bills are computed differently for example, as can be seen here.
Everything starting at #combine results in a table can be ignored as this simply prepares the data in a readable format.
# import relevant tables
using Dates, DataFrames, PrettyTables
# compute dates
today = Date(2023,02,08)
accrued_days = Day(86)
start = today - accrued_days
settle_date = Date(2023,05,15)
days_to_settle = Day(1)
coupon_date = settle_date-days_to_settle
days_to_next_coupon = coupon_date - today
accrued_days_between_coupon = coupon_date - start
dcf_next_coupon = (days_to_next_coupon/accrued_days_between_coupon)/2
dcf_accrued = accrued_days/accrued_days_between_coupon/2
# compute clean price, accrued interest and dirty price
price_clean = 98+25/32+1/64
accrued = dcf_accrued*(1/8)
price_dirty = price_clean + accrued
# ytm as quoted
ytm = 4.765459
# final cashflow (notional plus interest)
final_cashflow = 100 + (1/8)/2
# compute final cashflow based on ytm
final_cf_computed = round(price_dirty*(1+ytm/100*(dcf_next_coupon)), digits = 6)
# compute YTM
ytm_computed = (final_cash_flow/price_dirty - 1)/(yf)*200
op_logic = ((final_cash_flow/price_dirty)^(1/(yf/2))-1)*100
# combine results in a table
yield_comp = ["Price and Yield", "Price Clean", "Accrued Interest", "Price Dirty", "Final Cashflow", "Yield to Maturity (YTM)", "Final Cashflow according to YTM", "YTM computed", "Equiv 1/Yr (OP Logic)"]
yields = ["",price_clean, accrued, price_dirty, final_cashflow, ytm, final_cf_computed, ytm_computed, op_logic]
text = ["Dates", "Today", "Coupon Date", "Days To Next Coupon", "Coupon Start Date", "Accrued Days between coupons", "Daycount Fraction Accrued","Daycount Fraction next coupon" ]
date_vals = ["", today, coupon_date, days_to_next_coupon, start, accrued_days_between_coupon, dcf_accrued , dcf_next_coupon]
df_res = DataFrame(Fields = append!(text, yield_comp), Values = append!(date_vals, yields) )
# pretty print
hl_1 = Highlighter((data,i,j) -> data[i,1] == "Dates", crayon"bg:dark_gray white bold")
hl_2 = Highlighter((data,i,j) -> data[i,1] == "Price and Yield", crayon"bg:dark_gray white bold")
PrettyTables.pretty_table(df_res, border_crayon = Crayons.crayon"blue", header_crayon = Crayons.crayon"bold green", formatters = ft_printf("%.6f", ), highlighters = (hl_1, hl_2))