Lesson3 Python
Lesson3 Python
Lesson3 Python
class Asset:
pass
Constructor
A constructor is a special method that is used to initialize an object. It is useful for
creating objects with default values. Constructors are done using the __init__
method. The __init__ method has two arguments: self and args . The self
argument is used to refer to the object itself, and the args argument is used to pass
arguments to the constructor. The __init__ method is called when an object is
created.
class Asset:
def __init__(self, name, price):
self.name = name
self.price = price
Attributes
Attributes are variables that belong to an object. They are useful for storing information
about an object. Attributes can be accessed using the . operator. Attributes can also
be accessed using the getattr function. Attributes can be set using the = operator.
Attributes can also be set using the setattr function. Attributes can be deleted using
the del operator. Attributes can also be deleted using the delattr function.
Methods
Methods are functions that belong to an object. Methods can be called using the ()
operator. Since they are functions they are defined using the def keyword and always
contain the self argument first.
class Asset:
...
def double_price(self):
return self.price*2
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
v1 = Vector2D(1, 2)
v2 = Vector2D(3, 4)
v3 = v1 + v2
Juan F. Imbet Ph.D. 6
Python for Finance
__init__ # Constructor
__str__ # String representation
__add__ # Addition +
__sub__ # Subtraction -
__mul__ # Multiplication *
__truediv__ # Division /
__floordiv__ # Floor division //
__mod__ # Modulo %
__pow__ # Exponentiation **
__lt__ # Less than <
__le__ # Less than or equal to <=
__eq__ # Equal to ==
__ne__ # Not equal to !=
__gt__ # Greater than >
__ge__ # Greater than or equal to >=
A portfolio consists of a list of assets. Each asset has a name (identifier) as well as a
history of prices.
class Asset:
def compute_mu(self):
self.mu = self.price_history.pct_change().mean()
def compute_sigma(self):
self.sigma = self.price_history.pct_change().std()
Juan F. Imbet Ph.D. 8
Python for Finance
def compute_mu(self):
self.mu = np.sum([asset.mu * weight for asset, weight in zip(self.assets, self.weights)])
def compute_sigma(self):
# Covariance matrix
cov = np.cov([asset.price_history.pct_change().dropna() for asset in self.assets])
# Weighted covariance matrix
cov = np.diag(self.weights) @ cov @ np.diag(self.weights)
# Portfolio volatility
self.sigma = np.sqrt(np.diag(cov).sum())
Algorithm Analysis
What is an algorithm
An algorithm is a set of instructions that are used to solve a problem.
Example
Find the maximum value in a list of numbers.
Big O Notation
One way to compare algorithms is by understanding its behavior as the size of the
problem increases. Big O notation is used to describe the time complexity of an
algorithm.
They normally considered the amount of steps that the algorithm has to perform in the
worst case scenario. E.g. sorting a list that is in reverse order.
a = range(1000000)
%timeit a[0] # O(1)
%timeit a[500000] # O(1)
Differences are due to CPU caching, practically they are the same.
66.8 ns ± 0.124 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
86.2 ns ± 0.192 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
import random
random.seed(0)
a = [random.random() for _ in range(1000)]
%timeit max(a) # O(n)
a = [random.random() for _ in range(1000000)]
%timeit max(a) # O(n)
12.1 µs ± 4.11 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
12 ms ± 10.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
import random
random.seed(0)
a = [random.random() for _ in range(1000)]
%timeit bubble_sort(a) # O(n^2)
a = [random.random() for _ in range(10000)]
%timeit bubble_sort(a) # O(n^2)
Increasing the size ten times increases the time by 100 times.
Polynomial time
When an algorithm has a time complexity of for some constant , we say it has
polynomial time. Polynomial time algorithms are considered efficient.
import numpy as np
import random
random.seed(0)
a = np.random.rand(1000, 1000)
%timeit np.linalg.inv(a) # O(n^3)
a = np.random.rand(100000, 100000)
%timeit np.linalg.inv(a) # O(n^3)
Logarithmic time
When an algorithm has a time complexity of , we say it has logarithmic time.
Logarithmic time algorithms are considered efficient.
Example Binary search is a search algorithm that finds the position of a target value
within a sorted array. Increasing the size by 1000 barely changes the time.
a = range(1000000)
%timeit binary_search(a, 500000) # O(log n)
a = range(1000000000)
%timeit binary_search(a, 500000) # O(log n)
5.18 µs ± 9.36 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
7.79 µs ± 72 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Exponential time
When an algorithm has a time complexity of , we say it has exponential time.
Exponential time algorithms are considered inefficient.
Example The Power Set problem involves finding all possible subsets of a given set,
including the empty set and the set itself. For a set with n elements, the number of
subsets is , which grows exponentially with the size of the set. An extra element
almost doubles the time.
s = range(5)
%timeit generate_power_set(s) # O(2^n)
s = range(6)
%timeit generate_power_set(s) # O(2^n)
6.05 µs ± 18.8 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
10.7 µs ± 20.2 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Juan F. Imbet Ph.D. 21
Python for Finance
subsets = []
first_element = s[0]
remaining_elements = s[1:]
# Combine subsets without the first element with subsets including the first element
for subset in subsets_without_first:
subsets.append(subset) # Add subset without the first element
subsets.append([first_element] + subset) # Add subset including the first element
return subsets
Factorial time
When an algorithm has a time complexity of , we say it has factorial time.
Factorial time algorithms are considered inefficient.
s = list(range(5))
%timeit generate_permutations(s) # O(n!)
s = list(range(6))
%timeit generate_permutations(s) # O(n!)
137 µs ± 291 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
822 µs ± 587 ns per loop (mean ± std. dev. of 7 runs, 1,000 loops each)