According to the first-in-first-out (FIFO) valuation method of capital gain, it’s assumed that equities are sold in the order in which they’re bought. In other words, the oldest shares are sold first. A detailed description of this method can be found in many places, e.g. here. In this post, I will focus on the implementation of the FIFO method in Python.
For the implementation, I chose the minimal possible set of packages that allows me to avoid external dependencies and better shows the details of the FIFO mechanism. The implementation heavily exploits deque class, which is a list with quicker append and pop operations from the left side of the container and more intuitive addition (appendLeft) and removal (popLeft) of elements. However, the implementation is also possible with a standard list mechanism.
The balanceFifo function takes a list of transactions. The information about every transaction: date, amount and price of shares is stored in the Trans class. In our example, an investor bought two times 5 shares of stocks, then sold them in two transactions. Thus, the first transaction is Trans(“2022-01-01”,5,10) – an investor bought 5 shares of specified equity and paid 10$ for every share. In the second transaction Trans(“2022-01-02”,5,12), on the next day the investor bought the same amount of shares and paid 12$ for a share. Let stop here and see how these transactions are processed in the code. The addition of the first element to the FIFO queue represented by qTransactions deque is handled separately.
if len(qTransactions)==0:
logging.debug('Added the first element: %s',t.getInfo())
qTransactions.append(t)
continue
Note, that the first element may be added to the qTransactions many times during the loop. Every time the buy and sell transactions level off qTransactions becomes empty and the algorithm needs to initialise the qTransactions with the first element again.
When the FIFO list has at least one element, we take the next earlier mentioned buy transaction Trans(“2022-01-02”,5,12) and add it to our queue
while (t.amount!=0 and len(qTransactions)>0):
#investigate the first element from the queue
tq=qTransactions.popleft()
#the same type of transaction: both sell or both buy
if tq.amount*t.amount>0:
#return the first element back to the same place
qTransactions.appendleft(tq)
#add the new element to the list
qTransactions.append(t)
logging.debug('Added: %s',t.getInfo())
break
Next, come sell transactions. Depending on the amount of sold shares, we have 3 cases:
- Case 1: The investor sold 5 shares for 12$ each and, on the next day, sold the same amount for 14$ each. In the code, the transactions are represented as Trans(“2022-01-03”,-5,12) and Trans(“2022-01-04”,-5,14)
- Case 2: The investor sold 2 shares for 12$ each and next day sold 8 shares for 14$ each. In the code, the transactions are represented as Trans(“2022-01-03”,-2,12) and Trans(“2022-01-04”,-8,14)
- Case 3: The investor sold 8 shares for 12$ each and next day sold 2 shares for 14$ each. In the code, the transactions are represented as Trans(“2022-01-03”,-8,12) and Trans(“2022-01-04”,-2,14)
As we will see, each of these cases is handled by a separate fragment of code.
Case 1
We take the next item Trans(“2022-01-03”,-5,12). The numbers of shares in buy and sell transactions are equal. In this case, the contrary pair of transactions are balanced. The previously removed buy transaction from the queue and the current sell transaction are not added to the queues. The next sell transaction Trans(“2022-01-04”,-5,14) repeats this pattern.
if abs(tq.amount)==abs(t.amount):
insertTransaction(tq.datetime,t.datetime,\
math.copysign(t.amount,tq.amount), tq.price,t.price)
#update the amount in the transaction
t.amount=0
logging.debug('Balanced, removed transaction: %s',t.getInfo())
logging.debug('Balanced, removed from the queue: %s',tq.getInfo())
#the transaction has been balanced, take a new transaction
continue
Case 2
We take the next item Trans(“2022-01-03”,-2,12). The number of shares in the buy transaction Trans(“2022-01-01”,5,10) which matches the sell transaction is 5. We deduct shares between the buy and sell transactions and the updated buy transaction returns to the list. The next sell transaction Trans(“2022-01-04”,-8,12) is balanced by the remains of the 1st buy transaction and the whole 2nd buy transaction.
if abs(tq.amount)>abs(t.amount):
insertTransaction(tq.datetime,t.datetime,\
math.copysign(t.amount,tq.amount), tq.price,t.price)
#update the amount of the element in the queue
tq.amount=tq.amount+t.amount
#return the element back to the same place
qTransactions.appendleft(tq)
logging.debug('Removed transaction: %s',t.getInfo())
#the transaction has been balanced, take a new transaction
break
Case 3
We take the next item Trans(“2022-01-03”,-8,10). The number of shares in the buy transaction Trans(“2022-01-01”,5,10) is not sufficient to matches the sell transaction. Therefore the buy transaction doesn’t return to the queue. Instead, the unbalanced part of the sell transaction and the next sell transaction Trans(“2022-01-04”,-2,10) are balanced with the next buy transaction Trans(“2022-01-02”,5,10).
if abs(tq.amount)<abs(t.amount):
#update the units in transaction, (remove element from the queue)
t.amount=t.amount+tq.amount
insertTransaction(tq.datetime,t.datetime,tq.amount,tq.price,t.price)
logging.debug('Removed from queue: %s',tq.getInfo())
continue
Control and debug structures
The code contains two simple data validation structures. The first fragment of code is responsible for handling the situation, where the number of sold shares outnumber the bought ones. In this case, the sell transaction should be appended to the queue and wait for buy transactions to balance.
if (t.amount!=0 and len(qTransactions)==0):
#Add unbalanced transaction to the queue
#The queue changes polarisation
qTransactions.append(t)
The last fragment of the balanceFifo function removes from the queue transactions which were not balanced. It’s mainly helpful for debugging.
while (len(qTransactions)>0):
tq=qTransactions.popleft()
logging.debug('Remained on list transaction: %s',tq.getInfo())
Summary
The described code is far from being optimal. Many changes can be introduced to make it faster. However, I tried to write for educational purposes and to make it comprehensible. Furthermore, You can switch the debugging option which will make the code more chatty and allows for its better understanding.
For now, the code doesn’t handle date and time indexing (dates are included only for labeling purposes), short selling (the shares are first sold the bought back) or multiple equities.
In the next post, I extended the code to handle the LIFO case.
Full source code
import logging
from collections import deque
import math
class Trans:
datetime=None
amount=None
price=None
def __init__(self, datetime, amount, price):
self.datetime=datetime
self.amount=amount
self.price=price
def getInfo(self):
return(str(self.datetime)+"; "+
str(self.amount)+"; "+
str(self.price))+"; "
def balanceFifo(all_trans):
qTransactions = deque()
for t in all_trans:
#Add first element to the queue
if len(qTransactions)==0:
logging.debug('Added the first element: %s',t.getInfo())
qTransactions.append(t)
continue
while (t.amount!=0 and len(qTransactions)>0):
#investigate the first element from the queue
tq=qTransactions.popleft()
#the same type of transaction: both sell or both buy
if tq.amount*t.amount>0:
#return the first element back to the same place
qTransactions.appendleft(tq)
#add the new element to the list
qTransactions.append(t)
logging.debug('Added: %s',t.getInfo())
break
#contrary transactions: (sell and buy) or (buy and sell)
if tq.amount*t.amount<0:
logging.debug('Transaction : %s',t.getInfo())
logging.debug('... try to balance with: %s',tq.getInfo())
#The element in the queue have more units and takes in the current transaction
if abs(tq.amount)>abs(t.amount):
insertTransaction(tq.datetime,t.datetime,\
math.copysign(t.amount,tq.amount), tq.price,t.price)
#update the amount of the element in the queue
tq.amount=tq.amount+t.amount
#return the element back to the same place
qTransactions.appendleft(tq)
logging.debug('Removed transaction: %s',t.getInfo())
#the transaction has been balanced, take a new transaction
break
#The element from the queue and transaction have the same amount of units
if abs(tq.amount)==abs(t.amount):
insertTransaction(tq.datetime,t.datetime,\
math.copysign(t.amount,tq.amount), tq.price,t.price)
#update the amount in the transaction
t.amount=0
logging.debug('Balanced, removed transaction: %s',t.getInfo())
logging.debug('Balanced, removed from the queue: %s',tq.getInfo())
#the transaction has been balanced, take a new transaction
continue
#The transaction has more units
if abs(tq.amount)<abs(t.amount):
#update the units in transaction, (remove element from the queue)
t.amount=t.amount+tq.amount
insertTransaction(tq.datetime,t.datetime,tq.amount,tq.price,t.price)
logging.debug('Removed from queue: %s',tq.getInfo())
#the transaction has not been balanced,
#take a new element from the queue (t.amount>0)
continue
#We have unbalanced transaction but the queue is empty
if (t.amount!=0 and len(qTransactions)==0):
#Add unbalanced transaction to the queue
#The queue changes polarisation
qTransactions.append(t)
logging.debug('Left element: %s',t.getInfo())
#If something remained in the queue, treat it as open or part-open transactions
while (len(qTransactions)>0):
tq=qTransactions.popleft()
logging.debug('Remained on list transaction: %s',tq.getInfo())
def insertTransaction(dateStart,dateEnd,amount,priceStart,priceEnd):
print("Bought={}, sold={}, amount={}, buy price={}, sell_price={}, gain={}".\
format(dateStart,dateEnd,amount,priceStart,priceEnd, amount*(priceEnd-priceStart)))
#Uncomment if you want to see more information
#logging.basicConfig(level=logging.DEBUG)
trans_list=list()
#Case 1
print("Case 1")
trans=Trans("2022-01-01",5,10)
trans_list.append(trans)
trans=Trans("2022-01-02",5,12)
trans_list.append(trans)
trans=Trans("2022-01-03",-5,12)
trans_list.append(trans)
trans=Trans("2022-01-04",-5,14)
trans_list.append(trans)
balanceFifo(trans_list)
trans_list.clear()
#Case 2
print("Case 2")
trans=Trans("2022-01-01",5,10)
trans_list.append(trans)
trans=Trans("2022-01-02",5,12)
trans_list.append(trans)
trans=Trans("2022-01-03",-2,12)
trans_list.append(trans)
trans=Trans("2022-01-04",-8,14)
trans_list.append(trans)
balanceFifo(trans_list)
trans_list.clear()
#Case 3
print("Case 3")
trans=Trans("2022-01-01",5,10)
trans_list.append(trans)
trans=Trans("2022-01-02",5,12)
trans_list.append(trans)
trans=Trans("2022-01-03",-8,12)
trans_list.append(trans)
trans=Trans("2022-01-04",-2,14)
trans_list.append(trans)
balanceFifo(trans_list)