Skip to content

FINC50

FINC50 is one half of your Finance 101. (1)

  1. 🙋‍♂️ 50 is a half of 101, rounded down.

The objectives are

Note

This is a proof-of-concept and always a work-in-progress.

It could take a relatively long time for me to "complete".

Course notes

Demo

Bond cashflows and price

An interactive chart and calculator of bond cashflows, present values and prices.

{ "$schema": "https://vega.github.io/schema/vega/v5.json", "description": "A chart of bond's cashflows, present value and price, made by Mingze Gao", "width": 700, "height": 300, "title": { "text": "Cashflows, PV and Price of a $10,000 Bond", "fontSize": 18, "anchor": "middle" }, "data": [ { "name": "table", "transform": [ { "type": "sequence", "as": "year", "start": 0, "step": 0.5, "stop": 31 }, { "type": "formula", "as": "i", "expr": "(0.5*(couponFrequency=='semiannual')+(couponFrequency=='annual'))" }, { "type": "formula", "as": "i2", "expr": "(2*(couponFrequency=='semiannual')+(couponFrequency=='annual'))" }, { "type": "formula", "as": "cashflow", "expr": "10000*couponRate*(datum.i)*(datum.year>0)+10000*(datum.year==maturityInYears)" }, { "type": "formula", "as": "r", "expr": "couponFrequency=='annual'? discountRate : pow(1+discountRate,0.5)-1" }, { "type": "formula", "as": "pv", "expr": "datum.cashflow / pow(1+datum.r, datum.year)" }, { "type": "formula", "as": "price", "expr": "datum.r>0 ? (10000*couponRate*(datum.i)*(1-pow(1+datum.r,-maturityInYears*datum.i2))/datum.r+10000*pow(1+datum.r,-maturityInYears*datum.i2)) : 10000*(1+couponRate*(datum.i)*maturityInYears*datum.i2)" }, { "type": "filter", "expr": "datum.year<=maturityInYears" }, { "type": "filter", "expr": "couponFrequency=='annual'? (datum.year==round(datum.year)) : 1 " } ] }, { "name": "scaledata", "source": "table", "transform": [ { "type": "aggregate", "fields": ["cashflow", "price"], "ops": ["max", "max"], "as": ["maxCashflow", "mP"] }, { "type": "formula", "as": "maxV", "expr": "max(datum.maxCashflow, datum.mP*1.1)" } ] } ], "signals": [ { "name": "maturityInYears", "value": 10, "bind": { "input": "range", "min": 1, "max": 30, "step": 1 } }, { "name": "discountRate", "value": 0.08, "bind": { "input": "range", "min": 0, "max": 0.2, "step": 0.0001 } }, { "name": "couponRate", "value": 0.05, "bind": { "input": "range", "min": 0, "max": 0.2, "step": 0.0001 } }, { "name": "couponFrequency", "value": "annual", "bind": { "input": "radio", "options": ["annual", "semiannual"] } } ], "scales": [ { "name": "x", "type": "band", "domain": { "data": "table", "field": "year", "sort": true }, "range": "width", "padding": 0.7 }, { "name": "y", "type": "linear", "domain": { "data": "scaledata", "field": "maxV" }, "range": "height" } ], "axes": [ { "orient": "bottom", "scale": "x", "title": "Year" }, { "orient": "left", "scale": "y", "title": "Cash Flows, PV and Bond Price" } ], "marks": [ { "type": "rect", "from": { "data": "table" }, "encode": { "update": { "fill": { "value": "steelblue" }, "x": { "scale": "x", "field": "year" }, "width": { "scale": "x", "band": 1 }, "y": { "scale": "y", "field": "cashflow" }, "y2": { "scale": "y", "value": 0 }, "tooltip": { "signal": "{ 'Cashflow': format(datum.cashflow, '$,.2f') }" } } } }, { "type": "rect", "from": { "data": "table" }, "encode": { "update": { "fill": { "value": "#d6001c" }, "x": { "scale": "x", "field": "year" }, "width": { "scale": "x", "band": 1 }, "y": { "scale": "y", "field": "pv" }, "y2": { "scale": "y", "value": 0 }, "tooltip": { "signal": "{ 'PV': format(datum.pv, '$,.2f') }" } } } }, { "type": "rect", "from": { "data": "table" }, "encode": { "update": { "fill": { "value": "darkgray" }, "x": { "scale": "x", "value": 0 }, "width": { "scale": "x", "band": 1 }, "y": { "scale": "y", "field": "price" }, "y2": { "scale": "y", "value": 0 }, "tooltip": { "signal": "{ 'Bond Price': format(datum.price, '$,.2f') }" } } } }, { "type": "text", "from": { "data": "table" }, "encode": { "update": { "x": { "scale": "x", "value": 0 }, "y": { "scale": "y", "field": "price", "offset": -5 }, "text": { "signal": "format(datum.price, '$,.2f')" }, "fontSize": { "value": 12 }, "align": { "value": "left" }, "baseline": { "value": "bottom" }, "fill": { "value": "black" } } } }, { "type": "text", "encode": { "enter": { "align": { "value": "right" }, "baseline": { "value": "bottom" }, "fill": { "value": "rgba(0, 0, 0, 0.2)" }, "fontSize": { "value": 14 }, "x": { "value": 0, "offset": "width*0.85" }, "y": { "value": 0, "offset": "height*1.2" }, "text": { "value": "Assume coupons paid in arrears and effective annual discount rate (conversion based on coupon frequency)." } } } } ] }

Bond price and yield

An interactive chart of bond price and yield.

{ "$schema": "https://vega.github.io/schema/vega/v5.json", "description": "A chart of bond's price and yield, made by Mingze Gao", "width": 700, "height": 300, "title": { "text": "Bond Price and Yield", "fontSize": 18, "anchor": "middle" }, "data": [ { "name": "table", "transform": [ { "type": "sequence", "as": "yield", "start": 0.0, "step": 0.5, "stop": 20.5 }, { "type": "formula", "as": "price", "expr": "datum.yield>0 ? (10000*couponRate*(1-pow(1+datum.yield/100,-maturityInYears))/(datum.yield/100)+10000*pow(1+datum.yield/100,-maturityInYears)) : 10000*(1+couponRate*maturityInYears)" }, { "type": "formula", "as": "price5", "expr": "datum.yield>0 ? (10000*0.05*(1-pow(1+datum.yield/100,-maturityInYears))/(datum.yield/100)+10000*pow(1+datum.yield/100,-maturityInYears)) : 10000*(1+0.05*maturityInYears)" } ] }, { "name": "scaledata", "source": "table", "transform": [ { "type": "formula", "as": "maxV", "expr": "max(datum.price, datum.price5*1.2)" } ] } ], "signals": [ { "name": "maturityInYears", "value": 10, "bind": { "input": "range", "min": 1, "max": 30, "step": 1 } }, { "name": "couponRate", "value": 0.05, "bind": { "input": "range", "min": 0, "max": 0.1, "step": 0.0001 } } ], "scales": [ { "name": "x", "type": "linear", "domain": { "data": "table", "field": "yield", "sort": true }, "range": "width" }, { "name": "y", "type": "linear", "domain": { "data": "scaledata", "field": "maxV" }, "range": "height" } ], "axes": [ { "orient": "bottom", "scale": "x", "title": "Yield (%)", "ticks": false }, { "orient": "left", "scale": "y", "title": "Bond Price" } ], "marks": [ { "type": "rule", "encode": { "update": { "x": { "scale": "x", "value": 0 }, "y": { "scale": "y", "value": 10000 }, "x2": { "scale": "x", "value": 5 }, "y2": { "scale": "y", "value": 10000 }, "strokeWidth": { "value": 1 }, "strokeDash": { "value": [8, 3] }, "strokeCap": { "value": "round" }, "opacity": { "value": 1 } } } }, { "type": "rule", "encode": { "update": { "x": { "scale": "x", "value": 5 }, "y": { "scale": "y", "value": 0 }, "x2": { "scale": "x", "value": 5 }, "y2": { "scale": "y", "value": 10000 }, "strokeWidth": { "value": 1 }, "strokeDash": { "value": [8, 3] }, "strokeCap": { "value": "round" }, "opacity": { "value": 1 } } } }, { "type": "line", "from": { "data": "table" }, "encode": { "update": { "x": { "scale": "x", "field": "yield" }, "width": { "scale": "x", "band": 1 }, "y": { "scale": "y", "field": "price" }, "tooltip": { "signal": "{ 'Bond Price': format(datum.price, '$,.2f') }" } } } }, { "type": "line", "from": { "data": "table" }, "encode": { "update": { "x": { "scale": "x", "field": "yield" }, "y": { "scale": "y", "field": "price5" }, "stroke": { "value": "#d6001c" }, "tooltip": { "signal": "{ 'Bond Price': format(datum.price5, '$,.2f') }" } } } }, { "type": "text", "from": { "data": "table" }, "encode": { "update": { "x": { "scale": "x", "value": 20 }, "y": { "scale": "y", "field": "price", "offset": -5 }, "text": { "signal": "format(datum.price, '$,.0f')+'@'+format(datum.yield,'.1f')+'%'" }, "fontSize": { "value": 12 }, "align": { "value": "left" }, "baseline": { "value": "bottom" }, "fill": { "value": "black" } } } }, { "type": "text", "encode": { "enter": { "align": { "value": "right" }, "baseline": { "value": "bottom" }, "fill": { "value": "rgba(0, 0, 0, 0.2)" }, "fontSize": { "value": 14 }, "x": { "value": 0, "offset": "width" }, "y": { "value": 0, "offset": "height*1.2" }, "text": { "value": "Assume $10,000 bond, annual coupons paid in arrears and effective annual discount rate." } } } } ] }

Risk and return

A graph showing volatility and return of S&P500 constituents in 2022.(1) Try to pan, zoom, select and click.

  1. The data is retrieved using the following Python code.
    import yfinance as yf
    import pandas as pd
    import numpy as np
    
    link = "https://en.wikipedia.org/wiki/List_of_S%26P_500_companies#S&P_500_component_stocks"
    
    df = pd.read_html(link, header=0)[0]
    df = yf.download(tickers=df['Symbol'].tolist(), start="2022-01-01", end="2022-12-31", progress=False, rounding=True)
    df = df[['Adj Close']]
    df.columns = df.columns.droplevel(0)
    ret = ((df.pct_change()+1).cumprod()-1).iloc[-1]
    std = df.pct_change().std() * np.sqrt(252)
    df = pd.DataFrame({'return': ret.values, "std": std.values, "ticker": ret.index}).round(3).dropna()
    df.to_json("./spy_risk_return.json", orient="records")  
    

{ "$schema": "https://vega.github.io/schema/vega/v5.json", "title": { "text": "Return and Volatility of S&P500 Stocks in 2022", "fontSize": 18, "anchor": "middle" }, "description": "An interactive scatter plot example supporting pan and zoom.", "width": 700, "height": 300, "padding": { "top": 30, "left": 40, "bottom": 20, "right": 10 }, "autosize": "none", "config": { "axis": { "domain": false, "tickSize": 1, "tickColor": "#888", "labelFont": "Monaco, Courier New" } }, "signals": [ { "name": "margin", "value": 20 }, { "name": "hover", "on": [ { "events": "*:mouseover", "encode": "hover" }, { "events": "*:mouseout", "encode": "leave" }, { "events": "*:mousedown", "encode": "select" }, { "events": "*:mouseup", "encode": "release" } ] }, { "name": "xoffset", "update": "-(height + padding.bottom)" }, { "name": "yoffset", "update": "-(width + padding.left)" }, { "name": "xrange", "update": "[0, width]" }, { "name": "yrange", "update": "[height, 0]" }, { "name": "down", "value": null, "on": [ { "events": "touchend", "update": "null" }, { "events": "mousedown, touchstart", "update": "xy()" } ] }, { "name": "xcur", "value": null, "on": [ { "events": "mousedown, touchstart, touchend", "update": "slice(xdom)" } ] }, { "name": "ycur", "value": null, "on": [ { "events": "mousedown, touchstart, touchend", "update": "slice(ydom)" } ] }, { "name": "delta", "value": [0, 0], "on": [ { "events": [ { "source": "window", "type": "mousemove", "consume": true, "between": [ { "type": "mousedown" }, { "source": "window", "type": "mouseup" } ] }, { "type": "touchmove", "consume": true, "filter": "event.touches.length === 1" } ], "update": "down ? [down[0]-x(), y()-down[1]] : [0,0]" } ] }, { "name": "anchor", "value": [0, 0], "on": [ { "events": "wheel", "update": "[invert('xscale', x()), invert('yscale', y())]" }, { "events": { "type": "touchstart", "filter": "event.touches.length===2" }, "update": "[(xdom[0] + xdom[1]) / 2, (ydom[0] + ydom[1]) / 2]" } ] }, { "name": "zoom", "value": 1, "on": [ { "events": "wheel!", "force": true, "update": "pow(1.001, event.deltaY * pow(16, event.deltaMode))" }, { "events": { "signal": "dist2" }, "force": true, "update": "dist1 / dist2" } ] }, { "name": "dist1", "value": 0, "on": [ { "events": { "type": "touchstart", "filter": "event.touches.length===2" }, "update": "pinchDistance(event)" }, { "events": { "signal": "dist2" }, "update": "dist2" } ] }, { "name": "dist2", "value": 0, "on": [ { "events": { "type": "touchmove", "consume": true, "filter": "event.touches.length===2" }, "update": "pinchDistance(event)" } ] }, { "name": "xdom", "update": "slice(xext)", "on": [ { "events": { "signal": "delta" }, "update": "[xcur[0] + span(xcur) * delta[0] / width, xcur[1] + span(xcur) * delta[0] / width]" }, { "events": { "signal": "zoom" }, "update": "[anchor[0] + (xdom[0] - anchor[0]) * zoom, anchor[0] + (xdom[1] - anchor[0]) * zoom]" } ] }, { "name": "ydom", "update": "slice(yext)", "on": [ { "events": { "signal": "delta" }, "update": "[ycur[0] + span(ycur) * delta[1] / height, ycur[1] + span(ycur) * delta[1] / height]" }, { "events": { "signal": "zoom" }, "update": "[anchor[1] + (ydom[0] - anchor[1]) * zoom, anchor[1] + (ydom[1] - anchor[1]) * zoom]" } ] }, { "name": "size", "update": "clamp(20 / span(xdom), 1, 1000)" } ], "data": [ { "name": "points", "url": "./demo/spy_risk_return.json", "transform": [ { "type": "extent", "field": "std", "signal": "xext" }, { "type": "extent", "field": "return", "signal": "yext" }, { "type": "formula", "as": "url", "expr": "'https://www.google.com/search?q=ticker:'+datum.ticker", "initonly": true }, { "type": "formula", "as": "tip", "expr": "'Ticker:'+datum.ticker", "initonly": true } ] } ], "scales": [ { "name": "xscale", "zero": false, "domain": { "signal": "xdom" }, "range": { "signal": "xrange" } }, { "name": "yscale", "zero": false, "domain": { "signal": "ydom" }, "range": { "signal": "yrange" } } ], "axes": [ { "scale": "xscale", "orient": "top", "offset": { "signal": "xoffset" }, "title": "Volatility", "titlePadding": 15 }, { "scale": "yscale", "orient": "right", "offset": { "signal": "yoffset" }, "title": "Return", "titleAngle": -90, "titlePadding": 20 } ], "marks": [ { "type": "symbol", "from": { "data": "points" }, "clip": true, "encode": { "enter": { "fillOpacity": { "value": 0.6 }, "fill": { "value": "#a6192e" } }, "update": { "x": { "scale": "xscale", "field": "std" }, "y": { "scale": "yscale", "field": "return" }, "size": { "signal": "size" } }, "hover": { "fill": { "value": "firebrick" }, "tooltip": { "field": "tip", "type": "nominal" }, "size": { "signal": "size", "mult": 5 } }, "leave": { "fill": { "value": "#a6192e" } }, "select": { "size": { "signal": "size", "mult": 5 }, "href": { "field": "url", "type": "nominal" } }, "release": { "size": { "signal": "size" } } } } ] }

Comments