A library to manage a stock. The way quantities are sold can follow these rules :
- FIFO accounting (First In First Out)
- LIFO accounting (Last In Last Out)
- PRMP (Prix de Revient Moyen Pondéré) (Average Cost ?)
This library uses the JavaMoney API (JSR 354). You have to provide and implementation such as Moneta.
It's also possible to modify the value of the stock.
The trades must be ordered in time ascending order.
Rouding is taken into account. For example, you can buy 3 items at 100.00 euros and sell them one by one. The first item is sold at 33.33 euros. The second at 33.34 euros and the last one at 33.33 euros.
- Maven Dependency
- TradeWrapper
- Example
- FIFO
- LIFO
- PRMP
- Modification
- Reimbursement
- Force values
- Force sell
- Force modification
<dependency>
<groupId>io.github.ritonglue</groupId>
<artifactId>go-stock</artifactId>
<version>2.2.1</version>
</dependency>
This class stores an amount and a quantity. It's possible to provide the source of the information.
- buy 100 units at 50 each
- buy 125 units at 55 each
- buy 75 units at 59 each
- sell 210 units
Using this class as source :
public class Transaction {
private final int id;
private final BigDecimal quantity;
private final MonetaryAmount amount;
Transaction(int id, long quantity, long unitAmount) {
this.id = id;
this.quantity = new BigDecimal(quantity);
this.amount = Monetary.getDefaultAmountFactory().
setCurrency("EUR").setNumber(new BigDecimal(unitAmount)).create()
.multiply(quantity);
}
Transaction(int id, long quantity) {
this.id = id;
this.quantity = new BigDecimal(quantity);
this.amount = null;
}
public BigDecimal getQuantity() {
return quantity;
}
public MonetaryAmount getAmount() {
return amount;
}
@Override
public boolean equals(Object obj) {
if(obj == this) return true;
if(!(obj instanceof Transaction)) return false;
Transaction a = (Transaction) obj;
return a.id == this.id;
}
@Override
public int hashCode() {
return id;
}
}Use the StockManager in FIFO mode to solve this problem :
StockManager manager = new StockManager(Mode.FIFO); int id = 1;
Transaction a = new Transaction(id++, 100, 50);
Transaction b = new Transaction(id++, 125, 55);
Transaction c = new Transaction(id++, 75, 59);
Transaction d = new Transaction(id++, -210);
manager.add(Trade.buy(a.getQuantity(), a.getAmount(), a));
manager.add(Trade.buy(b.getQuantity(), b.getAmount(), b));
manager.add(Trade.buy(c.getQuantity(), c.getAmount(), c));
manager.add(Trade.sell(d.getQuantity(), d));Check the stock :
Trade stock = manager.getStock();
Assert.assertEquals(new BigDecimal(90), stock.getQuantity());
Assert.assertEquals(createMoney(5250), stock.getAmount());Check the closed positions :
List<Position> closedPositions = manager.getClosedPositions();
Assert.assertEquals(2, closedPositions.size());
Position position = closedPositions.get(0);
Assert.assertEquals(new BigDecimal(100), position.getQuantity());
Assert.assertEquals(createMoney(5000), position.getAmount());
Assert.assertEquals(a, position.getBuy());
Assert.assertEquals(d, position.getSell());
position = closedPositions.get(1);
Assert.assertEquals(createQuantity(110), position.getQuantity());
Assert.assertEquals(createMoney(110*55), position.getAmount());
Assert.assertEquals(b, position.getBuy());
Assert.assertEquals(d, position.getSell());Check the opened positions :
List<Position> openedPositions = manager.getOpenedPositions();
Assert.assertEquals(2, openedPositions.size());
Assert.assertEquals(new BigDecimal(15), position.getQuantity());
Assert.assertEquals(createMoney(15*55), position.getAmount());
Assert.assertEquals(b, position.getBuy());
position = openedPositions.get(1);
Assert.assertEquals(createQuantity(75), position.getQuantity());
Assert.assertEquals(createMoney(59*75), position.getAmount());
Assert.assertEquals(c, position.getBuy());Use the StockManager in LIFO mode to solve this problem :
StockManager manager = new StockManager(Mode.LIFO);Check the stock :
Trade stock = manager.getStock();
Assert.assertEquals(new BigDecimal(90), stock.getQuantity());
Assert.assertEquals(createMoney(90*50), stock.getAmount());Check the closed positions :
List<Position> closedPositions = manager.getClosedPositions();
Assert.assertEquals(3, closedPositions.size());
Position position = closedPositions.get(0);
Assert.assertEquals(new BigDecimal(75), position.getQuantity());
Assert.assertEquals(createMoney(75*59), position.getAmount());
Assert.assertEquals(c, position.getBuy());
Assert.assertEquals(d, position.getSell());
position = closedPositions.get(1);
Assert.assertEquals(createQuantity(125), position.getQuantity());
Assert.assertEquals(createMoney(125*55), position.getAmount());
Assert.assertEquals(b, position.getBuy());
Assert.assertEquals(d, position.getSell());
position = closedPositions.get(2);
Assert.assertEquals(createQuantity(10), position.getQuantity());
Assert.assertEquals(createMoney(10*50), position.getAmount());
Assert.assertEquals(a, position.getBuy());
Assert.assertEquals(d, position.getSell());Check the opened positions :
List<Position> openedPositions = manager.getOpenedPositions();
Assert.assertEquals(1, openedPositions.size());
position = openedPositions.get(0);
Assert.assertEquals(new BigDecimal(90), position.getQuantity());
Assert.assertEquals(createMoney(90*50), position.getAmount());
Assert.assertEquals(a, position.getBuy());Use the StockManager in PRMP mode to solve this problem :
StockManager manager = new StockManager(Mode.PRMP);In PRMP mode, all buy values are mixed together and sold at the same price.
List<Position> openedPositions = manager.getOpenedPositions();
List<Position> closedPositions = manager.getClosedPositions();
Assert.assertEquals(1, closedPositions.size());
Assert.assertEquals(1, openedPositions.size());
TradeWrapper stock = manager.getStock();
Assert.assertEquals(new BigDecimal(90), stock.getQuantity());
Assert.assertEquals(createMoney(4890), stock.getAmount()); Position position = closedPositions.get(0);
Assert.assertEquals(new BigDecimal(210), position.getQuantity());
Assert.assertEquals(createMoney(11410), position.getAmount()); position = openedPositions.get(0);
Assert.assertEquals(new BigDecimal(90), position.getQuantity());
Assert.assertEquals(createMoney(4890), position.getAmount());Just add a positive or negative amount to modify the amount of the stock. The quantity is unchanged. It's possible to pass a source of modification. In case of multiple opened position you can use different modes :
- by quantity
- by amount of money
- mixed mode (default mode):
- a reduction amount is spread prorata the amounts and a StockAmountReductionException is thrown if the reduction exceeds the amount in stock.
- an increased amount is spread prorata the quantities.
By default mixed mode is applied. You can change the modification mode at StockManager level or override it at TradeWrapper level.
Object source;
stock = manager.getStock();
BigDecimal oldQuantity = stock.getQuantity();
MonetaryAmount oldAmount = stock.getAmount();
MonetaryAmount delta = oldAmount.multiply(2);
manager.add(TradeWrapper.modification(delta, source));
stock = manager.getStock();
Assert.assertEquals(oldQuantity, stock.getQuantity());
Assert.assertEquals(oldAmount.add(delta), stock.getAmount());
List<Modification> modifications = manager.getModifications();
Assert.assertEquals(1, modifications.size());
Modification modification = modifications.get(0);
Assert.assertEquals(source, modification.getModification().getSource());
Assert.assertEquals(delta, modification.getModification().getAmount());
Assert.assertTrue(oldQuantity.compareTo(modification.getQuantity()) == 0);
Assert.assertEquals(oldAmount, modification.getAmountBefore());
Assert.assertEquals(oldAmount.add(delta), modification.getAmountAfter());A reimbursement will close all opened positions. After that the stock is empty (quantity zero).
Transaction rbt;
manager.add(Trade.reimbursement(rbt));
openedPositions = manager.getOpenedPositions();
Assert.assertTrue(openedPositions.isEmpty());
stock = manager.getStock();
Assert.assertEquals(BigDecimal.ZERO, stock.getQuantity());
Assert.assertNull(stock.getAmount());It's possible to force some operations.
For example : you buy 3 units at 100.00 and you sell them one by one. By default the reduction amount sequence is 33.33, 33.34, 33.33. It's possible to force the first values
int id = 1;
List<TradeWrapper> list = new ArrayList<>();
MonetaryAmount amount = createMoney("100.00");
SourceTest a = new SourceTest(id++);
SourceTest b = new SourceTest(id++);
SourceTest c = new SourceTest(id++);
SourceTest d = new SourceTest(id++);
TradeWrapper buy = TradeWrapper.buy(createQuantity(3), amount, a);
TradeWrapper sell1 = TradeWrapper.sell(createQuantity(1), b);
TradeWrapper sell2 = TradeWrapper.sell(createQuantity(1), c);
TradeWrapper sell3 = TradeWrapper.sell(createQuantity(1), d);
list.add(buy);
list.add(sell1);
list.add(sell2);
list.add(sell3);
StockManager manager = newStockManager();
//force modification for the couple (buy, sell1)
manager.addBuySellMoney(buy, sell1, createMoney("33.34"));
manager.process(list);For example : you want to apply an even distribution of you modification
int id = 1;
SourceTest a = new SourceTest(id++);
SourceTest b = new SourceTest(id++);
TradeWrapper buy1 = TradeWrapper.buy(createQuantity(3), createMoney("123.11"), a);
TradeWrapper buy2 = TradeWrapper.buy(createQuantity(4), createMoney("12.99"), b);
TradeWrapper modification = TradeWrapper.modification(createMoney("-10.00"));
List<TradeWrapper> list = List.of(buy1, buy2, modification);
StockManager manager = newStockManager();
manager.addBuyModificationMoney(buy1, modification, createMoney("-5.00"));
//not necessary
manager.addBuyModificationMoney(buy2, modification, createMoney("-5.00"));
manager.process(list);