Robo-Advising

As a capstone to chapter 1, let us build a function that serves as a robo-adivsor for a user’s investment portfolio.

There are two new ingredients to add to make a simplified robo-advisor work: type-conversion and input.

We won’t build a complete robo-advisor just yet, but we can make some progress towards building one using the material from this chapter.

Type Conversions

Two basic data types within Python are integers and strings.

my_int = 1
my_str = '1'

The type of a variable can be checked with the type() function.

print( type(my_int) )
print( type(my_str) )
<class 'int'>
<class 'str'>

Variable typing can be important to keep track of. Note that \(1\) and '1' do not count as the same thing in Python.

'1' == 1
False

This matters in the event that you have a string number that you want to use as an integer (or floating point) number. Suppose you need to add the number 8 to your string number.

8+'1'
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-4-bc89c6d67d5b> in <module>
----> 1 8+'1'

TypeError: unsupported operand type(s) for +: 'int' and 'str'

Strings containing integers can be converted into integers with the int() function.

int('1') + 8
9

Likewise strings containing decimal numbers can be converted into floating point numbers with the float() function.

float('1.23') + 8
9.23

Moreover, integers or floating point numbers can be converted to strings with the str() function.

type(str(1.23))
str

Translating variables from one type to another will only be successul when the data in the variable can be reasonably converted. One could not do int('banana') because there is no way to convert the string 'banana' into integer data.

Input

The other new ingredient needed to get a basic form of robo-adivising to work is the input() function. Our goal is to build a tool that asks a user a series of questions such that the tool can then recommend an investment strategy based on those questions. The input() function is the ingredient we’ll need to ask for user input into the tool.

Consider the example:

color = input('What is your favorite color?\n')
print('User entered the response:', color)
What is your favorite color?
purple
User entered the response: purple

The input() function prints out a string, given as the argument to the function. The input() function waits for a response and then returns that response. We store the returned value in a variable named color. The second line of the code above simply prints out the response to verify that the input() function worked correctly.

Responses to the input() function are always strings.

print(type(color))
<class 'str'>

This means that if we want to use the response as something else, like an integer, we need to convert the data.

Let’s move to another example to really see the input() function in action. This time, we’ll ask for the user’s age and print a response that is conditional on the user’s age.

age = input('What is your age?\n')
print('Equity allocation percent:', 100-int(age))
What is your age?
31
Equity allocation percent: 69

The output above represents the rule of 100. One frequently proposed piece of investment advice is for younger people to invest more in stocks (which generally grow more in value than bonds, but are riskier) and for older people to invest more in bonds (which are less risky and thus potentially safer to live off of in retirement). The rule of 100 is a trick for thinking about how much of a person’s portfolio should be in stocks: 100 minus your age is approximately the percentage of your wealth that should be in stocks. Or so the rule goes, at least. These notes are certainly not intended to serve as investment advice; the rule of 100 is just a convenient way to show up the utility of the input() function.

As another example of a conditional response, let’s ask the user how much risk they’re willing to take on.

def check_attitude():
    attitude = input('How much risk are you willing to take on?\n')

    if attitude == 'not much':
        print("Okay, we'll invest mostly in bonds.")
    elif attitude == 'a lot':
        print("Okey, we'll invest mostly in equities.")
    else:
        print("I'm just a stupid robot, I can't understand you.")

The goal, of course, is to get a sense of how much risk the user is willing to take on. The robo-adivsor can allocate the user’s money into riskier investments (potentially earning higher returns) if the user is willing to bear the risk of those investment.

check_attitude()
How much risk are you willing to take on?
a lot
Okey, we'll invest mostly in equities.

Of course, the problem here is that the input question, as stated above, is very open-ended. A user could in all likelihood enter a response that is not expected.

check_attitude()
How much risk are you willing to take on?
maybe a medium amount
I'm just a stupid robot, I can't understand you.

Computers are getting better at open-ended questions. The rise of artificial intelligence, and in particular a topic called natural language understanding, is helping with that.

For our use case, we will give the user a bit more guidance with the response we want.

def get_risk_preference():
    response = input("On a scale of 1-5, how much do you dislike risk?  1=not bothered by risk, 5=extremely worried about risk.\n")
    
    if response not in ['1', '2', '3', '4', '5']:
        print('Sorry, response must be 1, 2, 3, 4, or 5')
        print('I will guess that you meant to put 3')
        return 3
    
    return int(response)
get_risk_preference()
On a scale of 1-5, how much do you dislike risk?  1=not bothered by risk, 5=extremely worried about risk.
4
4

With these ideas, we can begin to form the start of a robo-advisor.

First, apply the rule of 100 to get a baseline for how the debt-equity mix should be allocated. Second, shift the allocation more towards stocks if the user is more okay with risk and shift the allocation more towards bonds if the user hates risk.

Note that we are allocating the task of asking for a user’s age to a separate function called get_age(). We will return to it and the get_risk_preference() shortly and the reason for dedicating this to a separate function will be made clear.

# get user's age
def get_age()
    age = input('What is your age?  Enter it as a numberic argument (e.g. "50", not "fifty")\n')
    return age
def get_allocation():
    
    age = get_age()
    
    # apply rule of 100
    equity_weight = 100-int(age)
    
    # get user's risk preference
    preference = get_risk_preference()
    
    # shift the allocation according to the user's preference
    if preference == 1:
        equity_weight += 10
    elif preference == 2:
        equity_weight += 5
    elif preference == 4:
        equity_weight -= 5
    elif preference == 5:
        equity_weight -= 10
        
    # correct weights to stick within 0-100
    equity_weight = min(equity_weight, 100)
    equity_weight = max(equity_weight, 0)
    
    debt_weight = 100-equity_weight
    
    print('I would recommend', equity_weight, 'percent stocks and ', debt_weight, 'percent bonds')
    
    return [equity_weight, debt_weight]
get_allocation()
What is your age?  Enter it as a numberic argument (e.g. "50", not "fifty")
31
On a scale of 1-5, how much do you dislike risk?  1=not bothered by risk, 5=extremely worried about risk.
5
I would recommend 59 percent stocks and  41 percent bonds
[59, 41]

While Loops

while loops are the slightly trickier cousin of for loops. A for loop will iterate over a defined list/set of items. This may be a range, a Python list, or something else. But we know the items over which a for loop will iterate, which means we know exactly how many times a for loop will iterate. For instance, if we say

for i in ['a', 'b', 'c']:

then we know that the loop will execute three times.

A while loop in contrast will continue executing so long as a given condition or set of conditions is True. The general syntax for a while loop is while <either a condition or set of conditions>. For instance, if we do:

i = 0
while i < 3:
    print(i)
    i = i +1
0
1
2

then we can increment over an index variable i. Let’s look at a second example that makes use of the len() function. The len() function will tell us the number of items in a list. So, len(['a','b']) would be \(2\).

my_list = [] # start by creating an empty list
while len(my_list) < 4:
    print(my_list, len(my_list))
    my_list.append('a')
[] 0
['a'] 1
['a', 'a'] 2
['a', 'a', 'a'] 3

The risky bit about while loops is that we could end up with code that never stops running! The trivial example is:

while True:
    print('this loop will never stop!')

since the True condition is given explicitly, it’s fairly obvious that the while condition will always be True and thus the loop will never stop. The trickier problem is when a condition turns out to be always True and we don’t anticipate it in advance. For example:

i = 1
while i > 0:
    i = i + 1

this code will never end because i will grow each iteration and consequently will always be greater than \(0\) (given the starting value is \(1\).

So, use while loops with caution. However, they can be useful little tools. We’ll see an example applied to the get_age() function shortly.

Try, Except

One of the best ways to write robust Python code is to employ the try and except pair of commands. This gives us the ability to catch small problems and deal with them, rather than just “giving up” if an issue arises.

The organization for a try/except block of code looks something like:

try:
    <stuff to try here>
except Exception as e:
    <things to do if there is a problem>
try:
    1 + 'nachos'
except Exception as e:
    print(e)
    print("it's okay, not all code works")
unsupported operand type(s) for +: 'int' and 'str'
it's okay, not all code works

When asking for user input, it can be helpful to pair while loops and try/except blocks. Note that in the get_age() function above, we just assumed the user would behave and enter a numeric age (e.g. 22) rather than an age expressed by alphabetic characters (e.g. 'twenty two'). That was naughty of us! It’s much better to head off potential issues if they can be anticipated.

Consider the following block:

def get_age():
    need_age = True
    while need_age:
        age = input('What is your age?  Enter it as a numberic argument (e.g. "50", not "fifty")\n')
        try:
            age = int(age) # this will break (raise an exception) if age is not numeric
            need_age = False
        except Exception as e:
            print('Please try entering your age again')
    return age
get_age()
What is your age?  Enter it as a numberic argument (e.g. "50", not "fifty")
fifty
Please try entering your age again
What is your age?  Enter it as a numberic argument (e.g. "50", not "fifty")
30t
Please try entering your age again
What is your age?  Enter it as a numberic argument (e.g. "50", not "fifty")
30
30

Concept check: The get_age() function is now robust to users not entering in a numeric value for their age. Copy and paste the get_risk_preference() function and modify it so that, rather than addressing errors by assuming that the response is '3', it re-asks the user for a risk preference.