Friday, September 27, 2024

How to System Design Trade Position Aggregator? [Solution]

Hello guys, it's been a long since I shared object-oriented design and system design questions in this blog. It was almost 7 to 8 years when I last blogged about how to solve vending machine design problems in Java. Actually, I had ideas for lots of such design questions at that time, but because of the lengthy nature of these kinds of posts and the amount of time they take, they just sit on my list of drafts. I thought to take them out and publish then slowly improve on them instead of publishing the perfect post for the first time, and today, I am excited to share one of such problems, how to design a trade position aggregator in Java. This program is like portfolio management software that monitors your risk and exposure in real-time based on trades. 

You can also call it a Risk Engine because it monitor your position. It generate alert at the moment, but once you have data an live position you can always modifiy this program to generate alert when position breaches previously definite thresolds

This is an interesting problem to solve to improve your thinking, coding, and design skill as these kinds of software are quite common in the Investment banking space, and most of the big banks have their own position aggregator, portfolio management software, or risk analysis systems. 

By solving this problem, you will not only learn about essential Java libraries and classes like Map, ConcurrentHashMap, PriorityQueueequals, and hashcode but also how to structure your program and how to convert a solution into code. If you are new to the programming world, I highly encourage you to solve these kinds of questions on your own to develop your coding skills and grow your confidence. 

My solution is not perfect, and though it works, there is a lot of scope for improvement. Feel free to post your own solution or suggest an improvement, do a code review, and learn. I am happy to incorporate any solution or suggestion for this object-oriented design problem. You can just copy-paste the code and run it on your favorite IDE like Eclipse or IntelliJIDEA



 

What is Trade Position Aggregator or Risk Engine? What do you need to Design?

You need to build a Position Aggregator, which can report position in real-time. The position is maintained as a combination of account and security. You need to remember that positions are generated when trades are executed on an exchange and received at your system. 
 
Here are some rules you need to remember while building this software:
1. When Direction is Buy and operation is NEW or AMEND, you need to increase the position as your exposure increases. 
2.  When Direction is Sell and operation is CANCEL, you need to increase the position as your exposure is not reducing. 
3.  When Direction is Buy and operation is Cancel reduce position;
4. When Direction is Sell, and operation is NEW or AMEND, reduce positions.
 
When amend comes in, the previous quantity is amended. Trades with the same id, but different versions can come up, too, in a different order; this is the corner case that needs to be handled.

Verify Account, Security, Quantity, Trades
"ACC-123" "MSFT" 100 6759, 7891, 3421




Solution of Trade Position Aggregator and Risk Engine Design Problem in Java 

My solution is simple and does what it says, but I haven't tested it extensively, so it may or may not handle all the corner cases. You can also suggest more test cases or add them to my JUnit testing class to see how does this solution fair for high volume traffic. 


List of files
============
  • Account.java
  • Direction.java
  • Operation.java
  • Trade.java
  • PositionManager.java
  • PositionManagerTest.java


PositionManager.java
=====================
This is the main class; while it doesn't contain the main method because we run this class using the JUnit test, it is the one that manages position. It contains a cache of accounts mapped with their account number.

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class PositionManager {
    private Map<String, Account> positions = new ConcurrentHashMap<String, Account>();
   
    public long getRealTimePosition(String security){
        return 100;
    }
   
    public long getPositionForAccountAndSecurity(String accountNo, String security){
       return positions.get(accountNo).getPosition(security);
      
    }
   
    public void process(Trade trade){
        String accountNo = trade.getAccountNo();
        Account account = positions.get(accountNo);
        if( account == null){
           account = new Account(accountNo);
        }
        account.process(trade);
        positions.put(accountNo, account);
    } 
   
}




Account.java
=============
This class represents an Account. It has a unique identifier accountNumber it holds all the trades don't for this account and maintains a cache of positions by security. It also contains a queue of pending trades that needs to process and applied for this account. 

If there is a requirement to save this data into a database, you can create an Account table to store the position for this account and map it to this class. 

It also has methods to update positions and handle different kinds of trades, which can increase or decrease positions like NEW, MODIFY, and CANCEL. 


import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.concurrent.ConcurrentHashMap;

/**
 *
 * @author https://javarevisited.blogspot.com
 */
public class Account {
    private String accountNo;
    private Map<Long, Trade> repository = new ConcurrentHashMap<Long, Trade>();
    private Map<String, Long> positionBySecurity = new ConcurrentHashMap<String, Long>();
    private Map<Long, PriorityQueue<Trade>> pendingTrades 
             = new ConcurrentHashMap<Long, PriorityQueue<Trade>>();
   
   
    public Account(String accNo){
        this.accountNo = accNo;
    }
   
    public long getPosition(String security) {
        Long qty = positionBySecurity.get(security);
        return qty == null? 0: qty;       
    }
   
   public void process(Trade trade) {
        if(isOutOfSequence(trade)){
            addOutOfSequenceToPendingQueue(trade);
           
        }else {
            List<Trade> sequencedTrades = getSequencedTrades(trade);
            for(Trade sequenced : sequencedTrades){
                updatePositions(trade);
            }
           
        }
    }
  
   
   private void updatePositions(Trade trade) {       
        String security = trade.getSecurity();
        long quantity = trade.getQuantity();
        Operation operation = trade.getOperation();
       
        switch(operation){
            case NEW:
                handleNew(trade, security, quantity);
                break;
               
            case AMEND:
                handleAmend(trade);
                break;
               
            case CANCEL:
                handleCancel(trade, security, quantity);
                break;
        }
        insertOrUpdateTrade(trade);
    }

    private void handleCancel(Trade trade, String security, long quantity) {
        if(trade.isBuy()){
             decrementPosition(security, quantity);
            
        }else if(trade.isSell()){
            incrementPosition(security, quantity);
        }       
               
    }

    private void handleNew(Trade trade, String security, long quantity) {
        if(trade.isBuy()){
             incrementPosition(security, quantity);
        }else if (trade.isSell()) {
            decrementPosition(security, quantity);
        }
        return;
    }

   
   
    private void incrementPosition(String security, long qty) {
        if(!positionBySecurity.keySet().contains(security)){
            positionBySecurity.put(security, qty);
           
        }else {
            Long totalQty = positionBySecurity.get(security);
            positionBySecurity.put(security, totalQty + qty);
        }
       
    }

    private void decrementPosition(String security, long qty ) {
       if(!positionBySecurity.keySet().contains(security)){
            positionBySecurity.put(security, -qty);
           
        }else {
            Long totalQty = positionBySecurity.get(security);
            positionBySecurity.put(security, totalQty -qty);
        }
    }
   
    private void handleAmend(Trade trade) {
        Trade previous = repository.get(trade.getTradeId());
       
        if(isSecurityAndDirectionAmended(trade, previous)){
            handleSecurityAndDirectionAmendment(trade, previous);
           
        } else if(isSecurityAmended(trade, previous)){
            handleSecurityAmendment(trade, previous);
           
        } else if(isDirectionAmended(trade, previous)){
            handleDirectionAmendment(trade, previous);
           
        } else {
            handleQuantityAmendment(trade, previous);
        }
    }

    private boolean isSecurityAndDirectionAmended(Trade current, Trade previous) {
       return isSecurityAmended(current, previous)
                && isDirectionAmended(current, previous);
    }

    private boolean isSecurityAmended(Trade current, Trade previous) {
        String pSecurity = previous.getSecurity();
        String nSecurity = current.getSecurity();
       
        return !pSecurity.equals(nSecurity);
    }

    private boolean isDirectionAmended(Trade current, Trade previous) {
        Direction pDir = previous.getDirection();
        Direction nDir = current.getDirection();
       
        return  pDir != nDir;
    }

    private void handleSecurityAndDirectionAmendment(Trade current, Trade previous) {
        handleDirectionAmendment(current, previous);
    }

    private void handleSecurityAmendment(Trade current, Trade previous) {
        if(current.isBuy()){
            decrementPosition(previous.getSecurity(), previous.getQuantity());
            incrementPosition(current.getSecurity(), current.getQuantity());
           
        }else if(current.isSell()){
            incrementPosition(previous.getSecurity(), previous.getQuantity());
            decrementPosition(current.getSecurity(), current.getQuantity());
        }
       
    }

    private void handleDirectionAmendment(Trade current, Trade previous) {
        if(previous.isBuy()){
           
            // reverse previous incrment
            decrementPosition(previous.getSecurity(), previous.getQuantity());
           
            // since current is sell - decremtn qty
            decrementPosition(current.getSecurity(), current.getQuantity());
           
           
        }else if(previous.isSell()) {
            incrementPosition(previous.getSecurity(), previous.getQuantity());
            incrementPosition(current.getSecurity(), current.getQuantity());
        }
    }
   
    private void handleQuantityAmendment(Trade current, Trade previous) {
        decrementPosition(previous.getSecurity(), previous.getQuantity());
        incrementPosition(current.getSecurity(), current.getQuantity());
    }
   
     private void addOutOfSequenceToPendingQueue(Trade trade){
            PriorityQueue<Trade> queue = pendingTrades.get(trade.getTradeId());
            if(queue == null){
                queue = new PriorityQueue<Trade>();               
            }
            queue.add(trade);
            pendingTrades.put(trade.getTradeId(), queue);
    }
   
   
    private boolean isOutOfSequence(Trade trade){
        long id = trade.getTradeId();
        int lastVersion = repository.get(id) == null ? 0 : repository.get(id).getVersion();
        int versionDifference = trade.getVersion() - lastVersion;
        if(versionDifference == 1){
            return false;
        }
        return true;
    }
   
   

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Account other = (Account) obj;
        if ((this.accountNo == null) ? (other.accountNo != null) 
             : !this.accountNo.equals(other.accountNo)) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 83  hash + (this.accountNo != null ? this.accountNo.hashCode() : 0);
        return hash;
    }

    @Override
    public String toString() {
        return "Account{" + "accountNo=" + accountNo + '}';
    }

    private void insertOrUpdateTrade(Trade trade) {
        repository.put(trade.getTradeId(), trade);
    }

    private List<Trade> getSequencedTrades(Trade trade) {
        PriorityQueue<Trade> pendingQueue = pendingTrades.get(trade.getTradeId());
        List<Trade> sequencedTrades = new ArrayList<Trade>();
       
        if(pendingQueue == null){
            sequencedTrades.add(trade);
            return sequencedTrades;
           
        }else{
            //logic to get all trades in sequence and remove PQ
        }
       
        return Collections.EMPTY_LIST;
    }

 
   
}





Direction.java
===============
This is a simple enum in Java, representing the direction of trade that can either be a buy-side trade or sell-side trade. 

public enum Direction {
    BUY, SELL;
}

Operation.java
===============
This is another simple enum in Java representing different trade operations like NEW, AMEND, and CANCEL. A NEW trade has a new order id, and AMEND and CANCEL generally operate on that order id. 

public enum Operation {
    NEW, AMEND, CANCEL;
}

How to design Trade Position Calculator in Java




Trade.java
============
This is another important Java class that represents a Trade in Java. It implements Comparable so that each trade can be compared with others based upon their natural order. 
 
public class Trade implements Comparable<Trade>{
    private long tradeId;
    private String accountNo;
    private int version;
    private String security;
    private long quantity;
    private Direction direction;
    private Operation operation;

    public Trade(long tradeId, String accountNo, int version, 
                 String security, long quantity, Direction direction,
                    Operation operation) {

        this.tradeId = tradeId;
        this.accountNo = accountNo;
        this.version = version;
        this.security = security;
        this.quantity = quantity;
        this.direction = direction;
        this.operation = operation;
    }
   
   
    public boolean isBuy(){
        return direction == Direction.BUY;
    }
   
    public boolean isSell(){
        return direction == Direction.SELL;
    }
   
   
    public String getAccountNo() { return accountNo; }
    public Direction getDirection() { return direction; }
    public Operation getOperation() { return operation; }
    public long getQuantity() { return quantity; }
    public String getSecurity() { return security; }
    public long getTradeId() { return tradeId; }
    public int getVersion() { return version; }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Trade other = (Trade) obj;
        if (this.tradeId != other.tradeId) {
            return false;
        }
        if ((this.accountNo == null) ? (other.accountNo != null) 
            : !this.accountNo.equals(other.accountNo)) {
            return false;
        }
        if ((this.security == null) ? (other.security != null) 
            : !this.security.equals(other.security)) {
            return false;
        }
        if (this.quantity != other.quantity) {
            return false;
        }
        if (this.direction != other.direction) {
            return false;
        }
        if (this.operation != other.operation) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 37  hash + (int) (this.tradeId ^ (this.tradeId >>> 32));
        hash = 37  hash + (this.accountNo != null ? this.accountNo.hashCode() : 0);
        hash = 37  hash + (this.security != null ? this.security.hashCode() : 0);
        hash = 37  hash + (int) (this.quantity ^ (this.quantity >>> 32));
        hash = 37  hash + (this.operation != null ? this.operation.hashCode() : 0);
        return hash;
    }

    @Override
    public String toString() {
        return "Trade{" + "tradeId=" + tradeId 
             + ", accountNo=" + accountNo + ", version=" + version 
             + ", security=" + security + ", quantity=" + quantity 
             + ", direction=" + direction + ", operation=" + operation + '}';

    }

    @Override
    public int compareTo(Trade o) {
       if(this.tradeId == o.tradeId){
           return this.version > o.version ? 1: (this.version < o.version? -1 : 0);
          
       }else{
           return this.tradeId > o.tradeId ? 1 : -1;
       }
    }
   
   
}





PositionManagerTest.java
====================
This is our main test class which executes the whole program for a list of test data and then verifies whether our program is calculating positions correct or not.
1. Test for Security amendment
2. Test for Direction amendment
3. Test for Security and Direction amendment
4. Test for quantity amendment;
 
Test Data
TradeId Version Security Direction Quantity Account Operation
5001 1 MSFT BUY 100 ACC-101 NEW
5001 1 MSFT BUY 100 ACC-101 NEW

5001 1 INFY BUY 100 ACC-102 NEW
5001 1 INFY BUY 100 ACC-102 NEW

5001 1 MSFT BUY 100 ACC-103 NEW
5001 1 MSFT BUY 100 ACC-103 NEW
5001 1 MSFT BUY 100 ACC-103 NEW

5001 1 BMW BUY 100 ACC-104 NEW
5001 1 AMZ BUY 100 ACC-104 NEW
5001 1 BMW BUY 100 ACC-104 NEW

5001 1 MSFT BUY 100 ACC-101 NEW
5001 1 MSFT BUY 100 ACC-101 NEW
5001 1 MSFT BUY 100 ACC-101 NEW

5001 1 OLA BUY 100 ACC-101 NEW
5001 1 META BUY 100 ACC-101 NEW
5001 1 OLA BUY 100 ACC-101 NEW
5001 1 META BUY 100 ACC-101 NEW
5001 1 META BUY 100 ACC-101 NEW
5001 1 META BUY 100 ACC-101 NEW


import org.junit.Ignore;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import org.junit.Test;
import static org.junit.Assert.;

public class PositionManagerTest {
   
    public PositionManagerTest() {
    }
   
    @Ignore
    public void testGetRealTimePosition() {
        assertEquals(1, 1);
    }

    @Test
    public void testGetPositionForAccountAndSecurity() {
        PositionManager pMgr = new PositionManager();
        List<Trade> tradeStream = createTrades();
       
        Iterator<Trade> itr = tradeStream.iterator();
       
        while(itr.hasNext()) {
            pMgr.process(itr.next());
            itr.remove();
        }
       
        assertEquals(200, pMgr.getPositionForAccountAndSecurity("ACC-101", "MSFT"));
        assertEquals(150, pMgr.getPositionForAccountAndSecurity("ACC-102", "INFY"));
        assertEquals(250, pMgr.getPositionForAccountAndSecurity("ACC-103", "REP"));
        assertEquals(150, pMgr.getPositionForAccountAndSecurity("ACC-104", "HKJ"));
        assertEquals(200, pMgr.getPositionForAccountAndSecurity("ACC-105", "FVE"));
        assertEquals(50,  pMgr.getPositionForAccountAndSecurity("ACC-105", "AMZ"));
        assertEquals(150, pMgr.getPositionForAccountAndSecurity("ACC-106", "ABC"));
        assertEquals(-50, pMgr.getPositionForAccountAndSecurity("ACC-107", "META"));
        assertEquals(200, pMgr.getPositionForAccountAndSecurity("ACC-108", "OLA"));
        assertEquals(200, pMgr.getPositionForAccountAndSecurity("ACC-109", "BCD"));
        assertEquals(-200,pMgr.getPositionForAccountAndSecurity("ACC-110", "BCD"));
        assertEquals(100, pMgr.getPositionForAccountAndSecurity("ACC-111", "EFG"));
        assertEquals(0, pMgr.getPositionForAccountAndSecurity("ACC-112", "BES"));
        assertEquals(0, pMgr.getPositionForAccountAndSecurity("ACC-113", "KBC"));
       
        //validate the total number of trades
       
        //validate correct account contain correct trades
       
    }
   
 
    private List<Trade> createTrades(){
        List<Trade> trades = new ArrayList<Trade>();
       
        //simple increment
        trades.add(new Trade(5001,"ACC-101", 1, "MSFT", 100,
                Direction.BUY, Operation.NEW));
        trades.add(new Trade(5002,"ACC-101", 1, "MSFT", 100, 
                Direction.BUY, Operation.NEW));
               
        //simple increment for another security
        trades.add(new Trade(5003,"ACC-102", 1, "INFY", 50,  
                Direction.BUY, Operation.NEW));
        trades.add(new Trade(5004,"ACC-102", 1, "INFY", 100, 
                Direction.BUY, Operation.NEW));
       
        //simple  NEW - AMEND - NEW (quantity)
        trades.add(new Trade(5005,"ACC-103", 1, "REP", 100,  
            Direction.BUY, Operation.NEW));
        trades.add(new Trade(5005,"ACC-103", 2, "REP", 50, 
            Direction.BUY, Operation.AMEND));
        trades.add(new Trade(5006,"ACC-103", 1, "REP", 200, 
            Direction.BUY, Operation.NEW));
       
        //simpel BUY SELL BUY Scenario
        trades.add(new Trade(5007, "ACC-104", 1, "HKJ", 100,  
            Direction.BUY, Operation.NEW));
        trades.add(new Trade(5008, "ACC-104", 1, "HKJ", 50, 
            Direction.SELL, Operation.NEW));
        trades.add(new Trade(5009, "ACC-104", 1, "HKJ", 100, 
            Direction.BUY, Operation.NEW));
       
        //security amendment scenario
        trades.add(new Trade(5010, "ACC-105", 1, "FVE", 100,  
            Direction.BUY, Operation.NEW));
        trades.add(new Trade(5010, "ACC-105", 2, "AMZ", 50, 
            Direction.BUY, Operation.AMEND));
        trades.add(new Trade(5011, "ACC-105", 1, "FVE", 200, 
            Direction.BUY, Operation.NEW));
       
        //direction amendment scenario
        trades.add(new Trade(5012, "ACC-106", 1, "ABC", 100, 
            Direction.BUY, Operation.NEW));
        trades.add(new Trade(5012, "ACC-106", 2, "ABC", 50, 
            Direction.SELL, Operation.AMEND));
        trades.add(new Trade(5014, "ACC-106", 1, "ABC", 200, 
            Direction.BUY, Operation.NEW));
       
        //security + direction amendment scenario
        trades.add(new Trade(5015, "ACC-107", 1, "OLA", 100,  
            Direction.BUY, Operation.NEW));
        trades.add(new Trade(5015, "ACC-107", 2, "META", 50, 
            Direction.SELL, Operation.AMEND));
        trades.add(new Trade(5016, "ACC-108", 1, "OLA", 200, 
            Direction.BUY, Operation.NEW));
        trades.add(new Trade(5016, "ACC-108", 2, "META", 50, 
            Direction.BUY, Operation.AMEND));
        trades.add(new Trade(5016, "ACC-108", 3, "OLA", 200, 
            Direction.BUY, Operation.AMEND));
      
       
        //same security - one accoutn sale other buy
        trades.add(new Trade(5017, "ACC-109", 1, "BCD", 200, 
            Direction.BUY, Operation.NEW));
        trades.add(new Trade(5018, "ACC-110", 2, "BCD", 200, 
            Direction.SELL, Operation.NEW));
       
        //same security - multiple amendments on quantity
        trades.add(new Trade(5019, "ACC-111", 1, "EFG", 50, 
            Direction.BUY, Operation.NEW));
        trades.add(new Trade(5019, "ACC-111", 2, "EFG", 200, 
            Direction.BUY, Operation.AMEND));
        trades.add(new Trade(5019, "ACC-111", 3, "EFG", 250, 
            Direction.BUY, Operation.AMEND));
        trades.add(new Trade(5019, "ACC-111", 4, "EFG", 100, 
            Direction.BUY, Operation.AMEND));
       
       
        //Same Security - NEW MOD CANCEL test
        trades.add(new Trade(5020, "ACC-112", 1, "BES", 50, 
            Direction.BUY, Operation.NEW));
        trades.add(new Trade(5020, "ACC-112", 2, "BES", 200, 
            Direction.BUY, Operation.AMEND));
        trades.add(new Trade(5020, "ACC-112", 3, "BES", 200, 
            Direction.BUY, Operation.CANCEL));
       
       
        //outofsequence trade events
        trades.add(new Trade(5019, "ACC-113", 1, "KBC", 100, 
            Direction.BUY, Operation.NEW));
        trades.add(new Trade(5019, "ACC-113", 2, "KBC", 150, 
            Direction.BUY, Operation.AMEND));
        trades.add(new Trade(5019, "ACC-113", 4, "KBC", 150, 
            Direction.BUY, Operation.CANCEL));
        trades.add(new Trade(5019, "ACC-113", 3, "KBC", 250, 
            Direction.BUY, Operation.AMEND));
       
        return trades;
    }
}

That's all about this Software design interview question about implementing a position aggregator in Java. It's a very popular question on the Java coding tests. There is a good chance you will come across such questions in your interview, particularly when interviewing with investment banks or financial institutions. 

Other Object-Oriented Design Pattern Tutorials from Javarevisited
  • 18 OOP Design Pattern Interview Questions for experienced Programmers (list)
  • How to design Vending Machine in Java part 2 (tutorial)
  • How to prepare for System Design Interview (system design prep guide)
  • 7 Best Java Design Pattern courses for Beginners (courses)
  • 20 Software design Questions from Programming Interviews (list)
  • How to implement the Builder design pattern in Java? (tutorial)
  • Top 5 Places to learn System Design (system design websites)
  • How to implement a Decorator design pattern in Java? (tutorial)
  • Top 5 System Design Interview Courses for Beginners and Experienced (Courses)
  • Top 5 object-oriented Pattern courses (design pattern courses)
  • How to use the Factory method design pattern in Java? (tutorial)
  • What is the difference between Factory pattern and Dependency Injection in Java? (answer)
  • 5 Books to learn Object-Oriented Design Patterns in Java? (books)
  • 10 System Design Courses for Programmers (system design courses)
Thanks for reading this tutorial; please share it with your friends and colleagues if you like this content. If you have any feedback or suggestion, then please drop a comment. I would be happy to offer any advise. 


No comments:

Post a Comment