When calculating capital gains from equity transactions, the order in which shares are considered sold significantly impacts the reported gain or loss. Two fundamental methods dominate this space: First-In-First-Out (FIFO) and Last-In-First-Out (LIFO). While FIFO assumes the oldest shares are sold first, LIFO takes the opposite approach-the most recently purchased shares are the first to be sold. Understanding both methods is essential for investors seeking to optimize their tax strategies or comply with specific accounting requirements.

This post presents a unified Python implementation that supports both valuation methods, demonstrating how a single codebase can handle either approach with minimal modifications.

The Core Concept

Both FIFO and LIFO operate on the same principle: matching buy transactions with sell transactions to calculate realized gains. The key difference lies in which buy transaction is matched against a given sale:

Consider an investor who makes two purchases:

When selling shares, FIFO would match against the January 1st purchase first, while LIFO would match against the January 2nd purchase first.

Implementation Foundation

The implementation leverages Python's deque class, which provides efficient append and pop operations from both ends of the container. This data structure is ideal for our purpose because:

  1. FIFO uses popleft() to remove the oldest element (left side) and append() to add new elements (right side)
  2. LIFO uses pop() to remove the newest element (right side) and append() to add new elements (right side)

The balanceQueue function processes a list of transactions stored in Trans objects, each containing the date, amount, and price of shares.

Transaction Processing: Three Cases

Both methods handle sell transactions through three distinct scenarios, determined by the relationship between buy and sell amounts.

Case 1: Equal Amounts

When the number of shares sold exactly matches the buy transaction being matched:

Sell: Trans("2022-01-03", -5, 12)

The buy and sell transactions balance perfectly. Neither returns to the queue:

if abs(tq.amount) == abs(t.amount):
    insertTransaction(tq.datetime, t.datetime,
            math.copysign(t.amount, tq.amount), tq.price, t.price)
    t.amount = 0
    continue

FIFO: The oldest buy transaction is matched first.

LIFO: The most recent buy transaction is matched first.

FIFO queue - case 1
FIFO queue - case 1
LIFO queue - case 1
LIFO queue - case 1

Case 2: Buy Exceeds Sell

When the buy transaction has more shares than the sell transaction:

Sell: Trans("2022-01-03", -2, 12)

The buy transaction is partially consumed and returns to the queue with its remaining shares:

if abs(tq.amount) > abs(t.amount):
    insertTransaction(tq.datetime, t.datetime,
            math.copysign(t.amount, tq.amount), tq.price, t.price)
    tq.amount = tq.amount + t.amount
    qTransactions.appendleft(tq)
    break
FIFO queue - case 2
FIFO queue - case 2
LIFO queue - case 2
LIFO queue - case 2

Case 3: Sell Exceeds Buy

When the sell transaction has more shares than the matching buy transaction:

Sell: Trans("2022-01-03", -8, 12)

The buy transaction is fully consumed, and the remaining sell amount is matched against the next buy transaction:

if abs(tq.amount) < abs(t.amount):
    t.amount = t.amount + tq.amount
    insertTransaction(tq.datetime, t.datetime, tq.amount, tq.price, t.price)
    continue
FIFO queue - case 3
FIFO queue - case 3
LIFO queue - case 3
LIFO queue - case 3

The Key Difference: Queue Construction

The remarkable insight is that both methods use the same logic for removing elements from the queue. The difference lies entirely in how elements are added:

def balanceQueue(all_trans, session, comp_method):
    ...
    if tq.amount * t.amount > 0:
        qTransactions.appendleft(tq)
        if comp_method == 'fifo':
            qTransactions.append(t)      # Add to right (becomes oldest)
        else:
            qTransactions.appendleft(t)  # Add to left (becomes newest)
    ...

Similarly, when handling unbalanced transactions:

if (t.amount != 0 and len(qTransactions) == 0):
    if comp_method == 'fifo':
        qTransactions.append(t)
    else:
        qTransactions.appendleft(t)

This elegant design allows a single parameter (comp_method) to switch between FIFO and LIFO behavior.

Control and Debug Structures

The code contains two simple data validation structures. The first fragment handles the situation where the number of sold shares outnumbers the bought ones. In this case, the sell transaction is appended to the queue and waits for buy transactions to balance:

if (t.amount != 0 and len(qTransactions) == 0):
    # Add unbalanced transaction to the queue
    # The queue changes polarisation
    if comp_method == 'fifo':
        qTransactions.append(t)
    else:
        qTransactions.appendleft(t)

The last fragment removes unbalanced transactions from the queue, which is mainly helpful for debugging:

while (len(qTransactions) > 0):
    tq = qTransactions.popleft()
    logging.debug('Remained on list transaction: %s', tq.getInfo())

Summary: Choosing the Right Method

Aspect FIFO LIFO
Match Order Oldest shares first Newest shares first
Rising Markets Higher reported gains (older, cheaper shares) Lower reported gains (newer, expensive shares)
Falling Markets Lower reported gains Higher reported gains
Tax Implications May result in higher short-term tax burden May defer gains to later periods
Regulatory Acceptance Widely accepted globally Restricted in some jurisdictions

Both implementations share the same validation structures for handling edge cases where sold shares outnumber bought shares, and both include debugging capabilities to trace transaction flow.

The described code is written for educational purposes to make it comprehensible. You can enable the debugging option which will make the code more verbose and allow for better understanding. For now, the code doesn't handle date and time indexing (dates are included only for labelling purpose), short selling (the shares are first sold then bought back) or multiple equities.

Full Source Code

Complete source code for both methods is available: