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,
PriorityQueue, equals, 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.
"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; }
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
2. Test for Direction amendment
3. Test for Security and Direction amendment
4. Test for quantity amendment;
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)
No comments:
Post a Comment