Last active
June 10, 2016 03:03
-
-
Save tedmiston/760736be62920421a864 to your computer and use it in GitHub Desktop.
High-yield checking analysis
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"cells": [ | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"# High-yield checking calculator\n", | |
"\n", | |
"The goal of this program is to decide how much money to invest in a high-yield checking account with tiered interest rates.\n", | |
"\n", | |
"For this project, I'm sharing my Jupyter Notebook (née IPython Notebook). I think being able to view some sample results alongside the code in a browser is a lot more valuable than just the code itself.\n", | |
"\n", | |
"You can download a copy and upload it (for free) to the awesome [tmpnb.org](http://tmpnb.org), where you can tweak the amounts and make it your own." | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## 1. Setup\n", | |
"\n", | |
"But first some input params." | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 1, | |
"metadata": { | |
"collapsed": true | |
}, | |
"outputs": [], | |
"source": [ | |
"# 0.25% APY on balances < $2500, and 1.00% APY on any balance above (uncapped)\n", | |
"\n", | |
"APY = {\n", | |
" 'LOW': .0025,\n", | |
" 'HIGH': .01,\n", | |
"}\n", | |
"\n", | |
"THRESHOLD = 2499.99 # the last penny at the lower rate" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## 2. Computing interest\n", | |
"\n", | |
"No surprises here.\n", | |
"\n", | |
"Note that I choose not to switch to cents (as integers) here even though it would be more technically correct for two reasons:\n", | |
"\n", | |
"1. I'm only printing output to screen, not doing further math or storing it.\n", | |
"2. I'm rounding to the nearest dollar anyway." | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 2, | |
"metadata": { | |
"collapsed": true | |
}, | |
"outputs": [], | |
"source": [ | |
"import sys\n", | |
"\n", | |
"def interest(amount):\n", | |
" \"\"\"Compute interest for a high-yield checking account (USD).\"\"\"\n", | |
" invalid_type = not any([isinstance(amount, int),\n", | |
" isinstance(amount, float)])\n", | |
" invalid_amount = amount < 0 or amount > sys.maxint\n", | |
" if invalid_type or invalid_amount:\n", | |
" return None\n", | |
"\n", | |
" low_amount = THRESHOLD if amount > THRESHOLD else amount\n", | |
" low_interest = low_amount * APY['LOW']\n", | |
"\n", | |
" high_amount = amount - low_amount\n", | |
" high_interest = high_amount * APY['HIGH']\n", | |
"\n", | |
" return int(round(low_interest + high_interest, 0))" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"### Aside: Generating a list of halved numbers\n", | |
"\n", | |
"*This part can be skipped if you're only interested in the problem at hand.*\n", | |
"\n", | |
"I wrote a one-liner utility function to quickly find the point where change becomes small after halving an amount repeatedly. Kind of inspired by git-bisect.\n", | |
"\n", | |
"For example, divide the number `1000` in half, `5` times:\n", | |
"\n", | |
" bisect(1000, 5) == [1000, 500, 250, 166, 125]" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 3, | |
"metadata": { | |
"collapsed": true | |
}, | |
"outputs": [], | |
"source": [ | |
"def bisect(value, repetitions):\n", | |
" \"\"\"Divide value in half a number of repetitions.\"\"\"\n", | |
" return [value] + [value/(2*i) for i in xrange(1, repetitions)]" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"Add a snazzy print function:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 4, | |
"metadata": { | |
"collapsed": false | |
}, | |
"outputs": [], | |
"source": [ | |
"from IPython.display import Markdown, display\n", | |
"\n", | |
"def build_markdown_table(headers, body):\n", | |
" s = ' | '.join(headers) + '\\n'\n", | |
" s += ' | '.join(['---'] * len(headers)) + '\\n'\n", | |
"\n", | |
" for row in body:\n", | |
" s += ' | '.join(row) + '\\n'\n", | |
"\n", | |
" return s\n", | |
"\n", | |
"def print_table(amount, reverse=False):\n", | |
" headers = ['Deposit', 'Interest']\n", | |
" body = []\n", | |
" for amount in sorted(amounts, reverse=reverse):\n", | |
" balance_dollars = '\\${}'.format(amount)\n", | |
" interest_dollars = '\\${}'.format(interest(amount))\n", | |
" body.append([balance_dollars, interest_dollars])\n", | |
" \n", | |
" s = build_markdown_table(headers, body)\n", | |
" display(Markdown(s))" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## 3. Show me some numbers" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 5, | |
"metadata": { | |
"collapsed": false | |
}, | |
"outputs": [ | |
{ | |
"data": { | |
"text/markdown": [ | |
"Deposit | Interest\n", | |
"--- | ---\n", | |
"\\$100000 | \\$981\n", | |
"\\$50000 | \\$481\n", | |
"\\$25000 | \\$231\n", | |
"\\$16666 | \\$148\n", | |
"\\$12500 | \\$106\n", | |
"\\$10000 | \\$81\n", | |
"\\$8333 | \\$65\n", | |
"\\$7142 | \\$53\n", | |
"\\$6250 | \\$44\n", | |
"\\$5555 | \\$37\n", | |
"\\$5000 | \\$31\n", | |
"\\$4545 | \\$27\n", | |
"\\$4166 | \\$23\n", | |
"\\$3846 | \\$20\n", | |
"\\$3571 | \\$17\n", | |
"\\$3333 | \\$15\n", | |
"\\$3125 | \\$13\n", | |
"\\$2941 | \\$11\n", | |
"\\$2777 | \\$9\n", | |
"\\$2631 | \\$8\n", | |
"\\$2500 | \\$6\n", | |
"\\$2380 | \\$6\n", | |
"\\$2272 | \\$6\n", | |
"\\$2173 | \\$5\n", | |
"\\$2083 | \\$5\n" | |
], | |
"text/plain": [ | |
"<IPython.core.display.Markdown object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"amounts = bisect(100000, 25)\n", | |
"print_table(amounts, reverse=True)" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"What I think is interesting is how quickly returns drop off at tiered interest rates.\n", | |
"\n", | |
"For example, let's look at the difference between investing \\$100 and \\$3000:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 6, | |
"metadata": { | |
"collapsed": false | |
}, | |
"outputs": [ | |
{ | |
"data": { | |
"text/markdown": [ | |
"Deposit | Interest\n", | |
"--- | ---\n", | |
"\\$100 | \\$0\n", | |
"\\$200 | \\$1\n", | |
"\\$300 | \\$1\n", | |
"\\$400 | \\$1\n", | |
"\\$500 | \\$1\n", | |
"\\$600 | \\$2\n", | |
"\\$700 | \\$2\n", | |
"\\$800 | \\$2\n", | |
"\\$900 | \\$2\n", | |
"\\$1000 | \\$3\n", | |
"\\$1100 | \\$3\n", | |
"\\$1200 | \\$3\n", | |
"\\$1300 | \\$3\n", | |
"\\$1400 | \\$4\n", | |
"\\$1500 | \\$4\n", | |
"\\$1600 | \\$4\n", | |
"\\$1700 | \\$4\n", | |
"\\$1800 | \\$5\n", | |
"\\$1900 | \\$5\n", | |
"\\$2000 | \\$5\n", | |
"\\$2100 | \\$5\n", | |
"\\$2200 | \\$6\n", | |
"\\$2300 | \\$6\n", | |
"\\$2400 | \\$6\n", | |
"\\$2500 | \\$6\n", | |
"\\$2600 | \\$7\n", | |
"\\$2700 | \\$8\n", | |
"\\$2800 | \\$9\n", | |
"\\$2900 | \\$10\n", | |
"\\$3000 | \\$11\n" | |
], | |
"text/plain": [ | |
"<IPython.core.display.Markdown object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"amounts = range(100, 3000+1, 100)\n", | |
"print_table(amounts)" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"$10 is the price of a nice lunch these days. It's not even really worth your time.\n", | |
"\n", | |
"*Of course*, this is why banks bury them in fine print. Luckily you know better ;).\n", | |
"\n", | |
"Now, let's dig into that high-yield spectrum." | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 7, | |
"metadata": { | |
"collapsed": false | |
}, | |
"outputs": [ | |
{ | |
"data": { | |
"text/markdown": [ | |
"Deposit | Interest\n", | |
"--- | ---\n", | |
"\\$2500 | \\$6\n", | |
"\\$3000 | \\$11\n", | |
"\\$3500 | \\$16\n", | |
"\\$4000 | \\$21\n", | |
"\\$4500 | \\$26\n", | |
"\\$5000 | \\$31\n", | |
"\\$5500 | \\$36\n", | |
"\\$6000 | \\$41\n", | |
"\\$6500 | \\$46\n", | |
"\\$7000 | \\$51\n", | |
"\\$7500 | \\$56\n", | |
"\\$8000 | \\$61\n", | |
"\\$8500 | \\$66\n", | |
"\\$9000 | \\$71\n", | |
"\\$9500 | \\$76\n", | |
"\\$10000 | \\$81\n" | |
], | |
"text/plain": [ | |
"<IPython.core.display.Markdown object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"amounts = range(2500, 10000+1, 500)\n", | |
"print_table(amounts)" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 8, | |
"metadata": { | |
"collapsed": false | |
}, | |
"outputs": [ | |
{ | |
"data": { | |
"text/markdown": [ | |
"Deposit | Interest\n", | |
"--- | ---\n", | |
"\\$10000 | \\$81\n", | |
"\\$11000 | \\$91\n", | |
"\\$12000 | \\$101\n", | |
"\\$13000 | \\$111\n", | |
"\\$14000 | \\$121\n", | |
"\\$15000 | \\$131\n", | |
"\\$16000 | \\$141\n", | |
"\\$17000 | \\$151\n", | |
"\\$18000 | \\$161\n", | |
"\\$19000 | \\$171\n", | |
"\\$20000 | \\$181\n", | |
"\\$21000 | \\$191\n", | |
"\\$22000 | \\$201\n", | |
"\\$23000 | \\$211\n", | |
"\\$24000 | \\$221\n", | |
"\\$25000 | \\$231\n" | |
], | |
"text/plain": [ | |
"<IPython.core.display.Markdown object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"amounts = range(10000, 25000+1, 1000)\n", | |
"print_table(amounts)" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## 4. Test all the things" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 9, | |
"metadata": { | |
"collapsed": false | |
}, | |
"outputs": [], | |
"source": [ | |
"# Some \"micro unit tests\" pytest-style because unittest doesn't work elegantly\n", | |
"# in IPython Notebooks.\n", | |
"\n", | |
"# error cases\n", | |
"assert interest(-1) is None\n", | |
"assert interest('cat') is None\n", | |
"assert interest([]) is None\n", | |
"assert interest(['x', 'y']) is None\n", | |
"assert interest(None) is None\n", | |
"\n", | |
"# normal and edge cases\n", | |
"assert interest(100) == interest(100.0) == interest(100.00)\n", | |
"assert interest(0) == 0\n", | |
"assert interest(1) == 0\n", | |
"assert interest(10) == 0\n", | |
"assert interest(100) == 0\n", | |
"assert interest(200) == 1\n", | |
"assert interest(2499.99) == 6\n", | |
"assert interest(2500) == 6\n", | |
"assert interest(5000) == 31" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## 5. Future work\n", | |
"\n", | |
"In the future, I may add support for capped balance accounts, minimum balances, and the ability to give a true total minus account fees (the account I had in mind while creating this charges none)." | |
] | |
} | |
], | |
"metadata": { | |
"kernelspec": { | |
"display_name": "Python 2", | |
"language": "python", | |
"name": "python2" | |
}, | |
"language_info": { | |
"codemirror_mode": { | |
"name": "ipython", | |
"version": 2 | |
}, | |
"file_extension": ".py", | |
"mimetype": "text/x-python", | |
"name": "python", | |
"nbconvert_exporter": "python", | |
"pygments_lexer": "ipython2", | |
"version": "2.7.11" | |
} | |
}, | |
"nbformat": 4, | |
"nbformat_minor": 0 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment