5. Functions, classes and error handling
Level 2: Python from Scratch for Finance
A function groups code you want to reuse under a name, so you do not repeat the same logic in twenty different places in the program. It is defined with the keyword def, followed by the name, the arguments in parentheses, and a colon.
def calculate_return(initial_price, final_price):
"""
Returns the percentage return between two prices.
"""
return (final_price - initial_price) / initial_price
calculate_return(180, 183.6) # 0.02, a 2% returnThe text between triple quotes right after the definition is the docstring: a description of what the function does, which you can also look up at any time with help(calculate_return). It is not mandatory, but in any code that someone else will maintain — including you, six months from now — it is close to essential.
Functions can have arguments with a default value, which is used automatically if you do not supply it when calling:
def calculate_position_size(capital, risk_per_trade=0.02):
"""
Returns how much capital to risk on a trade,
based on a risk percentage (2% by default).
"""
return capital * risk_per_trade
calculate_position_size(10000) # uses the 2% default: 200.0
calculate_position_size(10000, 0.05) # explicit 5% risk: 500.0
calculate_position_size(capital=10000, risk_per_trade=0.01) # named arguments, in any orderFor very small, single-use functions, Python lets you define them without a name using lambda — typically to pass them as an argument to another function, for example to sort a list of trades by their return:
trades = [("AAPL", 0.02), ("TSLA", -0.01), ("MSFT", 0.05)]
sorted_trades = sorted(trades, key=lambda trade: trade[1], reverse=True)When you need to group data together with the behavior that acts on it in a single unit, you enter object-oriented programming, and the tool is the class. A class defines the structure of an object — its attributes, that is, its data — and its methods, the functions it can run.
class Position:
"""
Represents an open position in an asset.
"""
def __init__(self, ticker, entry_price, quantity):
self.ticker = ticker
self.entry_price = entry_price
self.quantity = quantity
def market_value(self, current_price):
"""
Returns the current market value of the position.
"""
return current_price * self.quantity
def pnl(self, current_price):
"""
Returns the gain or loss relative to the entry price.
"""
return (current_price - self.entry_price) * self.quantity
def __str__(self):
return f"Position of {self.quantity} shares of {self.ticker} at {self.entry_price}"The __init__ method runs automatically when a new object of that class is created, and it is where its attributes are initialized. The first argument of any method is always self, a reference to the object itself — that is what allows market_value and pnl to access the ticker, the entry_price and the quantity of that particular position.
my_position = Position("AAPL", 180.00, 50)
print(my_position) # uses __str__: "Position of 50 shares of AAPL at 180.0"
print(my_position.pnl(183.6)) # 180.0 of profitEach object created from the class (my_position, for example) has its own independent copy of the attributes: creating a second position does not affect the first. That independence is exactly what makes object-oriented programming useful when you are managing a portfolio with many open positions at once.
Finally, no function working with real data is safe from receiving something unexpected: a ticker that does not exist, a negative price, a division by zero. Python handles these cases with exceptions. When something goes wrong, an exception can be 'raised' with raise, and caught elsewhere in the program with try/except so that the program does not stop dead.
def calculate_return(initial_price, final_price):
if initial_price <= 0:
raise ValueError("The initial price cannot be zero or negative")
return (final_price - initial_price) / initial_price
try:
r = calculate_return(0, 100)
except ValueError as error:
print(f"Invalid operation: {error}")Without the try/except, that raise would have halted the entire execution of the program. With it, you catch the error, handle it — in this case by printing a clear message — and the rest of the program can keep running. In a live trading system, this pattern is the difference between a single corrupt data point bringing down the whole algorithm, or that error being logged while the system carries on trading normally.