FINC50
FINC50 is one half of your Finance 101. (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.
- 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" } }
}
}
]
}