## Introduction¶

The aim of this coursework is to code a Python 3 module stocktrader that allows the user to load historical financial data and to simulate the buying and selling of shares on the stock market. Shares represent a fraction of ownership in a company and the total of all shares form the stock of that company. Shares can be bought and sold on the stock market at a price that varies daily. A reasonable investor aims to buy cheap shares and sells them when the price is higher. The actions of buying and selling shares are referred to as transactions. Transactions usually incur transaction fees, but we will not consider these in this project.

### What can/cannot be used for this coursework?¶

The Python knowledge contained in the lecture notes is essentially sufficient to complete this project. While you are allowed to consult the web for getting ideas about how to solve a specific problem, the most straightforward solution may already be in the lecture notes. You are also allowed to use online sources, but you must clearly indicate copy-and-pasted code like so:

   # the following 3 lines follow a similar code on
# http://webaddress.org/somecode.htm as retrieved on 24/04/2018
Let’s be clear about what is *not* allowed: You are NOT allowed to send, give, or receive Python code to/from classmates and others.

This project is the equivalent of a standard exam and it counts 70% towards your final mark. Consequently, the standard examination rules apply to this project.

Once your Python module is submitted via the Blackboard system, it will undergo plagiarism tests:

1. The turn-it-in system automatically checks for similarities in the codes among all students (without syntactical comparisons).
2. Your lecturer compares the syntax of all submitted codes (interestingly, with the help of another Python program). Hence, refrain from communicating any code with others.

Note that even if you are the originator of the work (and not the one who copied), the University Guidelines Plagiarism and Academic Malpractice (link below) require that you will be equally responsible for this case of academic malpractice and may lose all marks on the coursework (or even be assigned 0 marks for the overall course).

### How is this coursework assessed?¶

There are several factors that enter the assessment:

• First, it will be checked whether you have followed the tasks and format specified below, using the prescribed function names, variable names, and data structures.
• Second, your module will be tested manually by performing a number of predefined transactions, loading/saving a couple of portfolios, and testing trading strategies.
• Further, all module functions will be tested automatically by another Python program (so-called unit testing). This is why it is important that your module "does not do anything" when it is imported into another program, and that you strictly follow the format of the functions specified in the tasks below (otherwise some of the tests may fail and you may lose marks).
• It is also checked whether each function/line of code is free of bugs and the program runs without crashing. Functionality will be the main factor in the assessment.
• It will be tested if your functions react to exceptional inputs in an appropriate manner (using Exceptions). It should be impossible to crash the code.
• Make sure that your module is properly documented, and that all functions have meaningful docstrings. In particular, each function must explain its own inputs and returned values so that there is no room for misinterpretation.
• Further marks will be given on the code efficiency and strategy.

### When and how to submit the coursework?¶

The coursework can be completed and submitted as a single Python module named stocktrader.py. The submission is via Blackboard and the strict deadline is Thursday, May 10th, at 1pm. You can resubmit your coursework as often as you like, but only the last submission counts. Submissions after the deadline will not be accepted.

**UPDATE: The strict deadline for submitting the resit coursework is Thursday, August 23rd, at 1pm.**

## Task 0: Prepare the module and understand the data structures¶

Download the coursework.zip file and unzip the folder to a convenient location on your computer (e.g., your Desktop). The folder already contains a template for your stocktrader.py module. Your whole coursework project can be completed using this module. You "only" need to replace the TODO comments with the actual code. Make sure that all code is contained in functions so your module "does not do anything" when it is imported into another Python program.

Module template:

"""
TODO: Add a description of the module...
Also fill out the personal fields below.

Full name: Peter Pan
StudentId: 123456
Email: [email protected]
"""

class TransactionError(Exception):
pass

class DateError(Exception):
pass

stocks = {}
portfolio = {}
transactions = []

def normaliseDate(s):
# TODO

# TODO: All other functions from the tasks go here

def main():

# the following allows your module to be run as a program
if __name__ == '__main__' or __name__ == 'builtins':
main()


CSV data:

• The coursework folder also contains the files portfolio0.csv and portfolio.csv in the same location as the stocktrader.py file.

• In the subfolder stockdata you will find ten CSV files containing historic stock prices of different companies.

The module stocktrader uses three essential data structures as explained below.

### The stocks dictionary¶

The dictionary stocks stores historic financial data that your module can work with. The data for stocks is located in the stockdata subfolder, with each file of the form SYMBOL.csv corresponding to a particular company. Every entry in the stocks dictionary is a key-value pair. Each key is a string corresponding to a symbol and the value is again a dictionary.

The dictionaries in stocks contain key-value pairs where the key (a string) corresponds to a date in the form YYYY-MM-DDand the value is a list of floating point numbers [ Open, High, Low, Close ] corresponding to the prices of a stock at that particular date.

Here is an excerpt of a valid stocks dictionary containing data for the symbol EZJ (easyJet plc) and SKY (Sky plc):

stocks = {
'EZJ' : {
'2012-01-03' : [435.273010, 435.273010, 425.050995, 434.835999],
'2012-01-04' : [434.618011, 434.618011, 423.273010, 428.072998],
'2012-01-05' : [430.472992, 430.472992, 417.273010, 418.364014],
...
},
'SKY' : {
'2012-01-03' : [751.000000, 755.500000, 731.500000, 742.000000],
'2012-01-04' : [740.000000, 741.125000, 718.000000, 730.000000],
'2012-01-05' : [733.500000, 735.500000, 719.500000, 721.000000],
...
},
}

The interpretation of this data at an example is as follows: on the 4rd of January 2012 the price of a Sky share ranged between £718.00 (the "low") and £741.125 (the "high").

### The portfolio dictionary¶

portfolio is a dictionary that represents our capital at a given date. Our capital is the combination of cash and the shares that you hold. The keys in portfolio are strings date, cash, and arbitrarily many symbols. The respective values are the date of the last transaction performed on the portfolio in the form YYYY-MM-DD, the cash amount as a floating point number, and the integer number of shares held for each symbol.

Here's an example of a valid portfolio dictionary:

portfolio = {
'date' : '2013-11-27',
'cash' : 12400.45,
'EZJ' : 10
}

The interpretation of this is as follows: on the 27th of November 2013 we have £12,400.45 in cash and we own 10 shares of easyJet. We could now look up in the stocks dictionary that the low price of easyJet on that day is £1426.00. Hence, if we sold all 10 easyJet shares on this day, we'd have £12,400.45 + 10 x £1426.00 = £26,660.45 of cash and no more EZJ shares. In this case the portfolio dictionary would only have two keys, date and cash.

### The transactions list¶

transactions is a list of dictionaries, with each dictionary corresponding to a buy/sell transaction on our portfolio. Here is an example of a valid transactions list:

transactions = [
{ 'date' : '2013-08-11', 'symbol' : 'SKY', 'volume' : -5 },
{ 'date' : '2013-08-21', 'symbol' : 'EZJ', 'volume' : 10 }
]

The interpretation of this is as follows: on 11th of August 2013 we sold 5 shares of Sky (because volume is negative), and on the 21st of August 2013 we bought 10 shares of easyJet (because volume is positive). The value of volume is always an integer, and the date values are chronological: while there can be two or more neighboring list entries in transactions having the same date, the following ones can never have an earlier date. This makes sense as the time order of transactions is important.

## Task 1: function normaliseDate(s)¶

Write a function normaliseDate(s) which takes as input a string s and returns a date string of the form YYYY-MM-DD. The function should accept the following input formats: YYYY-MM-DD, YYYY/MM/DD and DD.MM.YYYY, where DD and MM are integers with one or two digits (the day and/or month can be given with or without a leading 0), and YYYY is a four-digit integer. The function converts all of these formats to YYYY-MM-DD.

If the conversion of the format fails (i.e., it is not exactly in any of the formats specified above), the function raises a DateError exception.

Note that this function is only about conversion of formats, and there is no need to check whether the date YYYY-MM-DD actually exists.

Example: Both normaliseDate('08.5.2012') and normaliseDate('2012/05/8') should return the string 2012-05-08, while normaliseDate('8.5.212') should raise a DateError exception.

## Task 2: function loadStock(symbol)¶

Write a function loadStock(symbol) which takes as input a string symbol and loads the historic stock data from the corresponding CSV file (in the stockdata subdirectory) into the dictionary stocks. The function does not need to return anything as the dictionary stocks is in the outer namespace and therefore accessible to the function.

The CSV files in the stockdata subdirectory are of the following format:

• the first line is the header and can be ignored
• every following line is of the comma-separated form Date,Open,High,Low,Close,AdjClose,Volume, where Date is in any of the formats accepted by the function normaliseDate(), and all other entries are floating point numbers corresponding to prices and trading volumes. Note that only the first values are relevant for filling the stocks dictionary and AdjClose,Volume can be ignored.

If the file given by symbol cannot be opened (as it is not found), a FileNotFoundError exception should be raised.

If a line in the CSV file is of an invalid format, a ValueError exception should be raised.

Example: loadStock('EZJ') should load the easyJet data from the file stockdata/EZJ.csv into the dictionary stocks, whereas loadStock('XYZ') should raise a FileNotFoundError exception.

## Task 3: function loadPortfolio(fname)¶

Write a function loadPortfolio(fname) which takes a input a string fname corresponding to the name of a CSV file in the same directory as stocktrader.py. The function loads the data from the file and assigns them to the portfolio dictionary, with all entries of the form described above (including the date!).

Make sure that portfolio is emptied before new data is loaded into it, and that the list transactions is emptied as well.

The function does not need to return anything as the dictionary portfolio is in the outer namespace and therefore accessible to the function. If no filename is provided, the name portfolio.csv should be assumed.

As the loadPortfolio(fname) function goes through the list of shares in the CSV file, it should use the function loadStock(symbol) from Task 2 to load the historic stock data for each symbol it encounters.

A valid portfolio CSV file is of the following form:

• the first line contains the date of the portfolio in any of the forms accepted by the function normaliseDate()
• the second line contains the cash in the portfolio, a nonnegative floating point number
• the following lines (if present) are of the form symbol,volume. Here, symbol is the symbol of a stock and volume is an integer corresponding to the number of shares.

Here is an example of a portfolio.csv file:

2012/1/16
20000
SKY,5
EZJ,8

If the file specified by fname cannot be opened (as it is not found), a FileNotFoundError exception should be raised.

If a line in the file is of an invalid format, a ValueError exception should be raised.

Example: loadPortfolio() should empty the dictionary portfolio and the list transactions, and then load the data from portfolio.csv into the dictionary portfolio, as well as the corresponding stock data into the dictionary stocks.

## Task 4: function valuatePortfolio(date, verbose)¶

Write a function valuatePortfolio(date, verbose) with two named parameters date and verbose. The function valuates the portfolio at a given date and returns a floating point number corresponding to its total value. The parameter date is any string accepted by the normaliseDate() function and when it is not provided, the date of the portfolio is used. The parameter verbose is a Boolean value which is False by default. When the function is called with verbose=True it should still return the total value of the portfolio but also print to the console a table of all capital with the current low prices of all shares, as well as the total value.

Example: With the portfolio.csv example given in Task 3, a call to valuatePortfolio('2012-2-6') should return the floating point number 27465.372072.... When valuatePortfolio('2012-2-6', True) is called, it should also print a table like this:

 Your portfolio on 2012-02-06:
[* share values based on the lowest price on 2012-02-06]

Capital type          | Volume | Val/Unit* | Value in £*
-----------------------+--------+-----------+-------------
Cash                  |      1 |  20000.00 |    20000.00
Shares of SKY         |      5 |    686.50 |     3432.50
Shares of EZJ         |      8 |    504.11 |     4032.87
-----------------------+--------+-----------+-------------
TOTAL VALUE                                     27465.37

Note 1: For the valuation we use the low prices of Sky and easyJet on date, in this case the 6th of February 2012. This is to be on the safe side: if we were selling the shares on that day, we would at least get those prices.

Note 2: A call to valuatePortfolio(date) should raise DateError exceptions in two cases:

• When date is earlier than the date of the portfolio, there might have been transactions afterwards and we no longer know what was the value back then. For example, valuatePortfolio('2012-1-3') should fail if the portfolio is already dated 2012-02-06.

• When date is not a trading day (e.g., a bank holiday or weekend) the CSV files will not contain any price for it and hence we cannot look up the values of shares. For example, valuatePortfolio('2012-2-12') should fail for that reason.

## Task 5: function addTransaction(trans, verbose)¶

Write a function addTransaction(trans, verbose) which takes as input a dictionary trans corresponding to a buy/sell transaction on our portfolio and an optional Boolean variable verbose (which is False by default). The dictionary trans has three items as follows:

• the key date whose value is any string accepted by the function normaliseDate()
• the key symbol whose value is a string corresponding to the symbol of a stock
• the key volume whose value is an integer corresponding to the number of shares to buy or sell.

Example: Here are two valid transaction dictionaries, the first one for selling 5 shares of Sky on 12th of August 2013, and the second for buying 10 shares of easyJet on the 21st of August 2013.

{ 'date' : '2013-08-12', 'symbol' : 'SKY', 'volume' : -5 },
{ 'date' : '21.08.2013', 'symbol' : 'EZJ', 'volume' : 10 }


A call to the addTransaction(trans) function should

• update the portfolio value for cash
• insert, update, or delete the number of shares
• update the date of portfolio to the date of the transaction
• append trans to the list transactions.

To be on the safe side, we always assume to sell at the daily low price and buy at the daily high price.

The addTransaction(trans) function does not need to return any values as both portfolio and transactions are available in the outer namespace and therefore accessible to the function.

If the optional Boolean parameter verbose=True the function should print to the console an informative statement about the performed transaction.

Example: The call

addTransaction({ 'date':'2013-08-12', 'symbol':'SKY', 'volume':-5 }, True)


should print something like

> 2013-08-12: Sold 5 shares of SKY for a total of £4182.50
Available cash: £24182.50

Exceptions: The function addTransaction(trans) may fail for several reasons, in which case both portfolio and transactions should remain unchanged and the appropriate exception should be raised:

• if the date of the transaction is earlier than the date of the portfolio, a DateError exception should be raised (i.e., one cannot insert any transactions prior to the last one)
• if the symbol value of the transaction is not listed in the stocks dictionary, a ValueError exception should be raised
• if the volume is such that we either do not have enough cash to perform a buying transaction or we do not have enough (or none at all) shares to perform a selling transaction, a TransactionError exception should be raised.

## Task 5.5: Take a break and enjoy¶

When you arrive here, it's time to take a break and test your code extensively. You should now be able to use your module to load portfolio and stock data files into your computers memory, print the value of your portfolio, and perform buying and selling transactions. For example, if you create a test_stocktrader.py file (or use the one in the coursework.zip folder) the following code should now work:

import stocktrader as s
val1 = s.valuatePortfolio(verbose=True)
trans = { 'date':'2013-08-12', 'symbol':'SKY', 'volume':-5 }
val2 = s.valuatePortfolio(verbose=True)
print("Hurray, we have increased our portfolio value by £{:.2f}!".format(val2-val1))


The console output should be something like this:

 Your portfolio on 2012-01-16:
[* share values based on the lowest price on 2012-01-16]

Capital type          | Volume | Val/Unit* | Value in £*
-----------------------+--------+-----------+-------------
Cash                  |      1 |  20000.00 |    20000.00
Shares of SKY         |      5 |    677.50 |     3387.50
Shares of EZJ         |      8 |    429.93 |     3439.42
-----------------------+--------+-----------+-------------
TOTAL VALUE                                     26826.92

> 2013-08-12: Sold 5 shares of SKY for a total of £4182.50
Available cash: £24182.50

[* share values based on the lowest price on 2013-08-12]

Capital type          | Volume | Val/Unit* | Value in £*
-----------------------+--------+-----------+-------------
Cash                  |      1 |  24182.50 |    24182.50
Shares of EZJ         |      8 |   1327.35 |    10618.80
-----------------------+--------+-----------+-------------
TOTAL VALUE                                     34801.30

Hurray, we have increased our portfolio value by £7974.38!

Before moving on to the final tasks, make sure that all the functions of Tasks 1-5 work as expected, that all calculations are correct, and that the appropriate exceptions are raised whenever a problem occurs. The following tasks will rely on these core functions.

## Task 6: function savePortfolio(fname)¶

Write a function savePortfolio(fname) that saves the current dictionary portfolio to a CSV file with name fname (a string). The file should be saved in the same directory as the stocktrader.py module. If no filename is provided, the name portfolio.csv should be assumed.

The function does not need to return anything.

Example: savePortfolio('portfolio1.csv') should store the values of the portfolio in the file portfolio1.csv.

## Task 7: function sellAll(date, verbose)¶

Write a function sellAll(date, verbose) that sells all shares in the portfolio on a particular date. Here, date is an optional string of any format accepted by the function normaliseDate() and verbose is an optional Boolean variable which is False by default. If verbose=True all selling transactions are printed to the console. If date is not provided, the date of the portfolio is assumed for the sell out.

Note: You should be able to use a simple loop with the function addTransaction(trans, verbose) for this task.

## Task 8: function loadAllStocks()¶

Write a function loadAllStocks() which loads all historic stock data from the stockdata subdirectory into the dictionary stocks. The function does not need to return anything as the dictionary stocks is in the outer namespace and therefore accessible to the function.

If the loading of one of the files in the stockdata subdirectory fails, this file should simply be ignored. The coursework.zip folder contains an invalid file invalidcsv/PPB.csv which you can use for testing this.

Note: You should be able to use a simple loop with the function loadStock(symbol) for this task. You may want to use the os module for getting a list of all files in a directory.

## Task 9: function tradeStrategy1(verbose)¶

Write a function tradeStrategy1(verbose) that goes through all trading days in the dictionary stocks and buys and sells shares automatically. The strategy is as follows:

• The earliest buying decision is either on the date of the portfolio or the tenth available trading day in stock (whichever is later)
• At any time, we buy the highest possible volume of a stock given the available cash.
• When shares have been bought, no other shares will be bought until all shares from the previous buying transaction are sold again.
• All shares from a previous buying transaction are sold at once, so it's a simple "buy as much as possible & sell all" procedure.
• If shares a being sold on trading day j we will only consider buying new shares on the following trading day, j+1.

Assume that j is the index of the current trading day, then we will find the stock to buy as follows:

• For each stock s available in stocks evaluate the quotient

  Q_buy(s,j) = 10*H(s,j) / (H(s,j) + H(s,j-1) + H(s,j-2) + ... + H(s,j-9))

where H(s,j) is the high price of stock s at the j-th trading day. Note that Q_buy(s,j) is large when the high price of stock s on trading day j is large compared the average of all previous ten high prices (including the current). This means we might enter a phase of price recovery.

• Find the maximal quotient Q_buy(s,j) among all stocks s and buy a largest possible volume v of the corresponding stock on trading day j. (It might not be possible to buy any as there might not be enough cash left; in this case do nothing on trading day j and move to the next. If two or more stocks have exactly the same quotient, take the one whose symbol comes first in lexicographical order.)

• Note that, as usual, our buying decision is based on the high price.

If we have automatically bought v shares of a stock s on trading day j, then from trading day k = j+1 onwards we will consider selling all of it as follows:

• On trading day k = j+1, j+2, ... calculate the quotient

  Q_sell(k) = L(s,k) / H(s,j),

where L(s,k) corresponds to the low price of stock s on trading day k. This quotient is high if the current low value of the stock is large compared to the high value to which we bought it.

• Sell all v shares of s on day k if Q_sell(k) < 0.7 (we already lost at least 30%, let's get rid of these shares!) or if Q_sell(k) > 1.3 (we made a profit of at least 30%, time to cash in!).

Notes:

• For solving this task it might be useful to first extract a list of all trading days from the stocks dictionary:
  lst = [ '2012-01-03', '2012-01-04', ..., '2018-03-13' ]
You can assume that all loaded stocks in the stocks dictionary can be traded on exactly the same days, and that there is at least one stock in that dictionary.
• All buying and selling transactions should be performed using the addTransaction(trans, verbose) function from Task 5. The verbose parameter of tradeStrategy1(verbose) can just be handed over to addTransaction(trans, verbose).

Example: The following code loads a portfolio of £20,000 cash (and no shares) on the 1st of January 2012, runs the tradeStrategy1(verbose=True) until the end of available data, and valuates the portfolio on the 13th of March 2018.

s.loadPortfolio('portfolio0.csv')
s.valuatePortfolio(verbose=True)
s.valuatePortfolio('2018-03-13', verbose=True)


The console output is as follows:

 Your portfolio on 2012-01-01:
[* share values based on the lowest price on 2012-01-01]

Capital type          | Volume | Val/Unit* | Value in £*
-----------------------+--------+-----------+-------------
Cash                  |      1 |  20000.00 |    20000.00
-----------------------+--------+-----------+-------------
TOTAL VALUE                                     20000.00

> 2012-01-16: Bought 29 shares of PRU for a total of £19517.00
Remaining cash: £483.00
> 2012-11-21: Sold 29 shares of PRU for a total of £25520.00
Available cash: £26003.00
> 2012-11-22: Bought 37 shares of EZJ for a total of £25696.50
Remaining cash: £306.50
> 2013-01-25: Sold 37 shares of EZJ for a total of £33633.00
Available cash: £33939.50
> 2013-01-28: Bought 35 shares of EZJ for a total of £33103.00
Remaining cash: £836.50
> 2013-05-21: Sold 35 shares of EZJ for a total of £43120.00
Available cash: £43956.50
> 2013-05-22: Bought 34 shares of EZJ for a total of £43905.22
Remaining cash: £51.28
> 2014-01-22: Sold 34 shares of EZJ for a total of £58208.00
Available cash: £58259.28
> 2014-01-23: Bought 18 shares of BATS for a total of £57456.00
Remaining cash: £803.28
> 2016-04-08: Sold 18 shares of BATS for a total of £74853.00
Available cash: £75656.28
> 2016-04-11: Bought 68 shares of SMIN for a total of £75140.00
Remaining cash: £516.28
> 2016-09-29: Sold 68 shares of SMIN for a total of £98532.00
Available cash: £99048.28
> 2016-09-30: Bought 108 shares of SKY for a total of £98594.49
Remaining cash: £453.79
> 2018-02-27: Sold 108 shares of SKY for a total of £140400.00
Available cash: £140853.79
> 2018-02-28: Bought 104 shares of SKY for a total of £140192.00
Remaining cash: £661.79

[* share values based on the lowest price on 2018-03-13]

Capital type          | Volume | Val/Unit* | Value in £*
-----------------------+--------+-----------+-------------
Cash                  |      1 |    661.79 |      661.79
Shares of SKY         |    104 |   1316.00 |   136864.00
-----------------------+--------+-----------+-------------
TOTAL VALUE                                    137525.79

Not bad! In a bit more than six years we have multiplied our initial investment of £20,000 by a factor of almost seven. I am now quitting my job as a lecturer, but promise to still mark your coursework in my new house on the Cayman Islands...

## Task 10 (optional): function tradeStrategy2(verbose)¶

When you modify the start date of your portfolio, or remove some of the stocks from the available data, you will see that tradeStrategy1() is not very robust and we've just been lucky to make so much profit. Also, it is kind of strange that we repeatedly sell and then immediately buy the same stock. This doesn't seem to make much sense. In some cases, this strategy results in big loses.

Can you write your own tradeStrategy2(verbose) function that performs better?

The conditions are as follows:

• tradeStrategy2(verbose) should only perform transactions via the function addTransaction(trans,verbose)
• tradeStrategy2(verbose) itself should not modify the dictionaries portfolio, stocks and neither the list transactions
• tradeStrategy2(verbose) should work on all valid stock and portfolio dictionaries, with different companies than the provided ones and over different time ranges
• on any trading day the buy/sell decision is only based on the price data [ Open, High, Low, Close ] available up to that day (i.e., no information from the future is used for making decisions); no other (external) data should be used
• tradeStrategy2(verbose) can (and probably should) "diversify" to reduce the risk, which means that any time it can decide to have shares of more than one stock in the portfolio, or no shares at all
• tradeStrategy2(verbose) is not restricted to a single buying or selling transaction per day, and even allowed to buy and sell shares of a stock on the same day (which would however incur a loss because buying is at the daily high price, and selling at the low price)
• tradeStrategy2(verbose) should not use any "randomness", i.e., two calls to the function with exactly the same data should result in an indentical list of transactions
• tradeStrategy2(verbose) should run reasonably fast, not longer than a couple of seconds on the provided data.

This task is optional and will not be part of the formal assessment. However, if your module contains a working tradeStrategy2(verbose) function, it will enter a *trading competition.* In this competition I will test your strategy on different stock data and over shorter and longer time intervals. The winners with the three best-performing strategies will be announced on the course website and will receive Amazon vouchers sponsored by Sabisu.

Industry sponsor: Sabisu is a Manchester-based software company that provide an operational and project intelligence platform for oil & gas and petrochemicals customers (including global leaders like Shell and Sabic). Much of Sabisu's work is related to time series and they use Python for the analytics. Sabisu have a long-standing collaboration with our School of Mathematics and the University in general.

End of coursework.