5 minute read

The net asset value of an ETF is computed by dividing the total value of the ETF’s underlying assets by the number of outstanding shares:

\[\text{nav} = {\text{cash} + \sum \text{assets} \over \text{number of oustanding shares}}\]

The calculation is quite easy but getting the right data can be a hassle. In this post, we go over how to calculate the net asset value of

Data and parameters

We will calculate the net asset value of KODEX KOSPI200 ETF (KR7069500007) that tracks KOSPI200 index and KODEX KOSDAQ150 ETF (KR7229200001) that tracks KOSDAQ150 index on May 26, 2023.

In our database, we have the following tables (and interface id):

  • KrxETFNAV (BV03S): contains ETF net asset value messages that is sent by Koscom.
  • KrxETFPDF (F803S): contains the portfolio deposit file (or portfolio composition file)
  • KrxStockBatchData (IFMSBTD0001): contains stock batch data messages where each message contains information of a security traded at KRX
  • KrxStockMarketdepth (IFMSRPD0002): contains stock marketdepth data, we are only interested in best ask and best bid price/quantity.

ETF PDF data

ETF’s portfolio deposit file message get disseminated in the morning before the market opens (it is sent twice, one at 6:00 and the next at 7:00). We are interested in KODEX KOSDAQ150 ETF’s pdf which we can find sending the following query to our clickhouse database:

SELECT DISTINCT ON (component_isin)
   component_isin
   , securities_per_1_cu_or_contract_units_or_korean_won_cash
   , par_value_or_initial_cash_amount
   , valuation_amount
   , toTimeZone(system_datetime, 'Asia/Seoul') as system_datetime
FROM default.KrxETFPDF
WHERE 
    isin='KR7229200001' 
    AND toDate(system_datetime, 'Asia/Seoul')='2023-05-26'
ORDER BY component_isin

This query will return a table that looks like the following:

	component_isin	securities_per_1_cu_or_contract_units_or_korean_won_cash	par_value_or_initial_cash_amount	valuation_amount	system_datetime
0	KR7000250001	7300.0	0.0	4577100.0	2023-05-26 07:07:11.751923396+09:00
1	KR7003100005	1400.0	0.0	367500.0	2023-05-26 07:07:11.782741794+09:00
2	KR7003380003	20400.0	0.0	1750320.0	2023-05-26 06:27:11.556796744+09:00
3	KR7005290002	18100.0	0.0	6452650.0	2023-05-26 06:27:11.587258894+09:00
4	KR7006730006	14200.0	0.0	1079200.0	2023-05-26 06:27:11.617790990+09:00
5	KR7007390008	26500.0	0.0	2806350.0	2023-05-26 06:27:11.648251013+09:00
6	KR7015750003	24800.0	0.0	2480000.0	2023-05-26 06:27:11.678772217+09:00
7	KR7016790008	11500.0	0.0	3588000.0	2023-05-26 07:07:11.966518570+09:00
8	KR7022100002	26800.0	0.0	3130240.0	2023-05-26 07:07:11.997238082+09:00
9	KR7023410004	23100.0	0.0	820050.0	2023-05-26 07:07:12.027744952+09:00
10	KR7025900002	2300.0	0.0	1104000.0	2023-05-26 06:27:11.800727598+09:00
11	KR7025980004	30900.0	0.0	1918890.0	2023-05-26 07:07:12.088699514+09:00
12	KR7027360007	23900.0	0.0	579575.0	2023-05-26 06:27:11.862013942+09:00
13	KR7028300002	57700.0	0.0	20598900.0	2023-05-26 06:27:11.892536047+09:00
14	KR7029960002	11000.0	0.0	811800.0	2023-05-26 06:27:11.923032848+09:00
15	KR7030190003	17900.0	0.0	1909930.0	2023-05-26 06:27:11.953537584+09:00

...

On May 26 2023, there are 152 components in the KODEX KOSDAQ150 ETF. To compute the net asset value of the ETF, we sum the valuation amount column and divide it by the creation unit. For KODEX KOSDAQ150 ETF, the creation unit is 50000:

nav = np.sum(df["valuation_amount"]) / 50000  # 12793.0077

The resulting nav matches the official previous day nav disseminated by Koscom. We can check the correctness of the valuation amount by taking the previous closing price of each stock and multiplying it to the number securities per 1 cu column value:

etf_pdf = EtfPdf(etf_isin, date_str, etf_cu, tz)
etf_batchdata = StockBatchData([etf_isin], date_str, tz)
etf_intraday_nav = EtfIntradayNav(etf_isin, date_str, tz)
assert etf_pdf.nav - etf_intraday_nav.prev_day_nav < 1e-2, f"diff: {etf_pdf.nav - etf_intraday_nav.prev_day_nav}"

The class functions EtfPdf, StockBatchData, EtfIntradayNav are provided in the attached file. When running the above code, we see that all the valuation amounts match our calculated valuation amounts.

Marketdepth data for component stocks

Koscom disseminates nav data of every ETF every second, and more importantly, the first nav message is sent at ‘09:00:10’. For HFT firms, this is a potential arbitrage opportunity. We can compute intraday NAV of an ETF from individual stock quotes. In order to do that, we need to get the marketdepth data of all component stocks and update our nav:

WITH stock_marketdepth AS (
    SELECT 
        isin,
        l1askprice,
        l1asksize,
        l1bidprice,
        l1bidsize,
        (l1askprice + l1bidprice) / 2 AS midprice,
        (l1askprice * l1bidsize + l1bidprice * l1asksize) / (l1asksize + l1bidsize) 
           AS weighted_midprice,
        toDate(system_datetime, 'Asia/Seoul') as date,
        toTimeZone(system_datetime, 'Asia/Seoul') as system_datetime,
        'KR7229200001' as etf_isin
    FROM default.KrxStockMarketdepth
    WHERE
        toDate(system_datetime, 'Asia/Seoul')='2023-05-26' 
        AND
        toTime(system_datetime, 'Asia/Seoul') BETWEEN
            toTime(parseDateTimeBestEffort('09:00:01.0', 'Asia/Seoul'), 'Asia/Seoul') 
            AND
            toTime(parseDateTimeBestEffort('15:19:59.0', 'Asia/Seoul'), 'Asia/Seoul')
        AND isin in ({",".join([f"'{isin}'" for isin in isins])})
    ORDER BY system_datetime
)
, nav AS (
    SELECT
        isin
        , previous_day_nav / 100 as prev_day_nav
        , intraday_final_nav / 100 as intraday_final_nav
        , toTimeZone(system_datetime, 'Asia/Seoul') as system_datetime
    FROM default.KrxETFNAV
    WHERE isin='KR7229200001'
    AND toDate(system_datetime, 'Asia/Seoul')='2023-05-26'
    ORDER BY system_datetime ASC
)
SELECT 
    stock_marketdepth.*
    , nav.prev_day_nav
    , nav.intraday_final_nav
FROM stock_marketdepth
ASOF LEFT JOIN nav
ON stock_marketdepth.etf_isin = nav.isin
AND stock_marketdepth.system_datetime >= nav.system_datetime
ORDER BY system_datetime 

Note that we used python to run this query since it is cumbersome to write down all 152 isins that are in KODEX KOSDAQ150 ETF. The result of the query will look something like this:


isin	l1askprice	l1asksize	l1bidprice	l1bidsize	midprice	weighted_midprice	date	system_datetime	etf_isin	prev_day_nav	intraday_final_nav
10000	KR7041510009	104100.0	58	104000.0	28	104050.0	104032.558140	2023-05-26	2023-05-26 09:01:01.829710847+09:00	KR7229200001	12793.01	12844.34
10001	KR7060720000	20550.0	935	20500.0	100	20525.0	20504.830918	2023-05-26	2023-05-26 09:01:01.841332664+09:00	KR7229200001	12793.01	12844.34
10002	KR7052020005	19270.0	193	19260.0	78	19265.0	19262.878229	2023-05-26	2023-05-26 09:01:01.855721867+09:00	KR7229200001	12793.01	12844.34
10003	KR7067310003	17310.0	530	17300.0	10406	17305.0	17309.515362	2023-05-26	2023-05-26 09:01:01.862351680+09:00	KR7229200001	12793.01	12844.34
10004	KR7067630004	10920.0	30	10890.0	95	10905.0	10912.800000	2023-05-26	2023-05-26 09:01:01.864752235+09:00	KR7229200001	12793.01	12844.34
10005	KR7058470006	126700.0	573	126600.0	321	126650.0	126635.906040	2023-05-26	2023-05-26 09:01:01.874548800+09:00	KR7229200001	12793.01	12844.34
10006	KR7074600008	26600.0	662	26500.0	925	26550.0	26558.286074	2023-05-26	2023-05-26 09:01:01.876554644+09:00	KR7229200001	12793.01	12844.34
10007	KR7078340007	67900.0	200	67800.0	20	67850.0	67809.090909	2023-05-26	2023-05-26 09:01:01.881207917+09:00	KR7229200001	12793.01	12844.34
10008	KR7061970000	8030.0	902	8020.0	30	8025.0	8020.321888	2023-05-26	2023-05-26 09:01:01.884060829+09:00	KR7229200001	12793.01	12844.34
10009	KR7064260003	4285.0	161	4275.0	252	4280.0	4281.101695	2023-05-26	2023-05-26 09:01:01.886363998+09:00	KR7229200001	12793.01	12844.34

Now we are ready to calculate intraday nav.

Calculating intraday nav

To compute inav, we update the price of the stock whenever a message that contains an orderbook data of ETF’s component stock comes in. Using the query result from above, we can write the following python code:

tz = "Asia/Seoul"
beg_date_pd = pd.to_datetime("2023-05-26").tz_localize(tz).date()
end_date_pd = pd.to_datetime("2023-05-26").tz_localize(tz).date()
beg_time = "09:00:01.0"
end_time = "15:19:59.0"

etf_isin = 'KR7229200001'  # kosdaq150, 'KR7069500007' for kospi200
etf_cu = 50000

for date in pd.date_range(beg_date_pd, end_date_pd):
    date_str = date.strftime("%Y-%m-%d")
    print(date_str)
    
    # check if the date is a trading date, if not continue
    if not is_trading_date(date_str):
        continue

    # get pdf data
    etf_pdf = EtfPdf(etf_isin, date_str, etf_cu, tz)
    etf_batchdata = StockBatchData([etf_isin], date_str, tz)
    etf_intraday_nav = EtfIntradayNav(etf_isin, date_str, tz)
    assert etf_pdf.nav - etf_intraday_nav.prev_day_nav < 1e-2, f"diff: {etf_pdf.nav - etf_intraday_nav.prev_day_nav}"
    
    component_marketdepth = MarketdepthWithNavData(
        etf_pdf.component_isins, 
        etf_isin,
        date_str, 
        beg_time, end_time, tz)

    inav = []
    for k, row in enumerate(component_marketdepth):
        koscom_nav = row["intraday_final_nav"] if row["intraday_final_nav"] > 0 else etf_intraday_nav.prev_day_nav
        etf_pdf.update_state(
            row["isin"],
            row["weighted_midprice"],
            koscom_nav,
            row["system_datetime"]
        )
        inav.append((
                etf_pdf.updated_datetime, 
                etf_pdf.nav, 
                etf_pdf.koscom_nav,
                etf_pdf.nav - etf_pdf.koscom_nav))


df = pd.DataFrame(inav, columns=["updated_datetime", "nav", "koscom_nav", "diff"])
df.plot(
    x="updated_datetime",
    y=["nav", "koscom_nav"],
    figsize=(30, 10),
    grid=True
)

df.plot(
    x="updated_datetime",
    y=["diff"],
    figsize=(30, 10),
    grid=True
)

The resulting plots are shown below:

The plot indicates that divergence in nav values occur mostly in the morning. Throughout the day, the nav values converge pretty well. Let’s look at iNav and Koscom NAV more closely in the morning:

We see that in the first 5 minutes of trading hours, the nav value from Koscom has a hard time keeping track of our inav. However, after 09:05:00.0, things start to settle and we see that inav and koscom nav values converge nicely.

Above are inav and koscom nav before market close.

Leave a comment