/** * Copyright 2010 Archfirst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.archfirst.bfoms.domain.account.brokerage; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.persistence.AttributeOverride; import javax.persistence.AttributeOverrides; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Embedded; import javax.persistence.Entity; import javax.persistence.OneToMany; import javax.persistence.Transient; import org.archfirst.bfoms.domain.account.AccountStatus; import org.archfirst.bfoms.domain.account.BaseAccount; import org.archfirst.bfoms.domain.account.CashTransfer; import org.archfirst.bfoms.domain.account.OwnershipType; import org.archfirst.bfoms.domain.account.SecuritiesTransfer; import org.archfirst.bfoms.domain.account.brokerage.order.ExecutionReport; import org.archfirst.bfoms.domain.account.brokerage.order.Order; import org.archfirst.bfoms.domain.account.brokerage.order.OrderCompliance; import org.archfirst.bfoms.domain.account.brokerage.order.OrderCreated; import org.archfirst.bfoms.domain.account.brokerage.order.OrderEstimate; import org.archfirst.bfoms.domain.account.brokerage.order.OrderEventPublisher; import org.archfirst.bfoms.domain.account.brokerage.order.OrderParams; import org.archfirst.bfoms.domain.account.brokerage.order.OrderSide; import org.archfirst.bfoms.domain.marketdata.MarketDataService; import org.archfirst.bfoms.domain.referencedata.ReferenceDataService; import org.archfirst.bfoms.domain.security.User; import org.archfirst.bfoms.domain.util.Constants; import org.archfirst.common.money.Money; import org.archfirst.common.quantity.DecimalQuantity; import org.hibernate.annotations.OptimisticLock; import org.hibernate.annotations.Parameter; import org.hibernate.annotations.Type; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * BrokerageAccount * * @author Naresh Bhatia */ @Entity public class BrokerageAccount extends BaseAccount { private static final long serialVersionUID = 1L; private static final Logger logger = LoggerFactory.getLogger(BrokerageAccountFactory.class); // ----- Constructors ----- private BrokerageAccount() { } // Allow access only from AccountService BrokerageAccount(String name, AccountStatus status, OwnershipType ownershipType, Money cashPosition) { super(name, status); this.ownershipType = ownershipType; this.cashPosition = cashPosition; } // ----- Commands ----- @Override public void transferCash(CashTransfer transfer) { cashPosition = cashPosition.plus(transfer.getAmount()); this.addTransaction(transfer); } @Override public void transferSecurities(SecuritiesTransfer transfer) { if (transfer.getQuantity().isPlus()) { depositSecurities( transfer.getSymbol(), transfer.getQuantity(), transfer.getPricePaidPerShare(), new SecuritiesTransferAllocationFactory(brokerageAccountRepository, transfer)); } else { withdrawSecurities( transfer.getSymbol(), transfer.getQuantity().negate(), new SecuritiesTransferAllocationFactory(brokerageAccountRepository, transfer)); } this.addTransaction(transfer); } private void depositSecurities( String symbol, DecimalQuantity quantity, Money pricePaidPerShare, AllocationFactory factory) { // Find lots for the specified instrument List<Lot> lots = brokerageAccountRepository.findActiveLots(this, symbol); // First deposit to lots that have negative quantity (unusual case) DecimalQuantity quantityLeftToDeposit = quantity; for (Lot lot : lots) { DecimalQuantity lotQuantity = lot.getQuantity(); if (lotQuantity.isMinus()) { DecimalQuantity depositQuantity = lotQuantity.negate(); lot.buy(depositQuantity, pricePaidPerShare); lot.addAllocation(factory.createAllocation(depositQuantity)); quantityLeftToDeposit = quantityLeftToDeposit.minus(depositQuantity); if (quantityLeftToDeposit.isZero()) { break; } } } // Put the remaining quantity in a new lot if (quantityLeftToDeposit.isPlus()) { Lot lot = new Lot( new DateTime(), symbol, quantityLeftToDeposit, pricePaidPerShare); brokerageAccountRepository.persistAndFlush(lot); this.addLot(lot); lot.addAllocation(factory.createAllocation(quantityLeftToDeposit)); } } private void withdrawSecurities( String symbol, DecimalQuantity quantity, AllocationFactory factory) { // Find lots for the specified instrument List<Lot> lots = brokerageAccountRepository.findActiveLots(this, symbol); // Withdraw specified quantity from available lots DecimalQuantity quantityLeftToWithdraw = quantity; for (Lot lot : lots) { DecimalQuantity lotQuantity = lot.getQuantity(); if (lotQuantity.isMinus()) { continue; } DecimalQuantity withdrawQuantity = (lotQuantity.gteq(quantityLeftToWithdraw)) ? quantityLeftToWithdraw : lotQuantity; lot.sell(withdrawQuantity); lot.addAllocation(factory.createAllocation(withdrawQuantity.negate())); quantityLeftToWithdraw = quantityLeftToWithdraw.minus(withdrawQuantity); if (quantityLeftToWithdraw.isZero()) { break; } } // If some quantity remains (unusual case), just create a lot with // negative quantity. This case can happen if a long standing sell // order gets executed. if (quantityLeftToWithdraw.isPlus()) { Lot lot = new Lot( new DateTime(), symbol, quantityLeftToWithdraw.negate(), new Money("0.00")); brokerageAccountRepository.persistAndFlush(lot); this.addLot(lot); lot.addAllocation(factory.createAllocation(quantityLeftToWithdraw)); } } public Order placeOrder(OrderParams params, MarketDataService marketDataService) { // Check compliance OrderEstimate orderEstimate = this.calculateOrderEstimate(params, marketDataService); if (orderEstimate.getCompliance() != OrderCompliance.Compliant) { throw new IllegalArgumentException("Order is not compliant: " + orderEstimate.getCompliance()); } // Place order Order order = new Order(params); brokerageAccountRepository.persistAndFlush(order); this.addOrder(order); orderEventPublisher.publish(new OrderCreated(order)); return order; } public void processExecutionReport(ExecutionReport executionReport) { logger.debug("Processing ExecutionReport: {}", executionReport); Order order = brokerageAccountRepository.findOrder(executionReport.getClientOrderId()); Trade trade = order.processExecutionReport( executionReport, brokerageAccountRepository, orderEventPublisher); if (trade != null) { this.addTransaction(trade); if (trade.getSide() == OrderSide.Buy) { cashPosition = cashPosition.plus( trade.getAmount()); // trade.amount is negative depositSecurities( trade.getSymbol(), trade.getQuantity(), trade.getPricePerShare(), new TradeAllocationFactory(brokerageAccountRepository, trade)); } else { cashPosition = cashPosition.plus( trade.getAmount()); // trade.amount is positive withdrawSecurities( trade.getSymbol(), trade.getQuantity(), new TradeAllocationFactory(brokerageAccountRepository, trade)); } } } private void addOrder(Order order) { orders.add(order); order.setAccount(this); } private void addLot(Lot lot) { lots.add(lot); lot.setAccount(this); } // ----- Queries ----- @Transient public BrokerageAccountSummary getAccountSummary( User user, ReferenceDataService referenceDataService, MarketDataService marketDataService) { List<Position> positions = this.getPositions(referenceDataService, marketDataService); Money marketValue = new Money(); for (Position position : positions) { marketValue = marketValue.plus(position.getMarketValue()); } List<BrokerageAccountPermission> permissions = brokerageAccountRepository.findPermissionsForAccount(user, this); return new BrokerageAccountSummary( this.id, this.name, this.cashPosition, marketValue, permissions.contains(BrokerageAccountPermission.Edit), permissions.contains(BrokerageAccountPermission.Trade), permissions.contains(BrokerageAccountPermission.Transfer), positions); } @Transient public List<Position> getPositions( ReferenceDataService referenceDataService, MarketDataService marketDataService) { List<Lot> lots = brokerageAccountRepository.findActiveLots(this); // Process lots into a hierarchy of instrument and lot positions // Create instrument positions that will be returned List<Position> instrumentPositions = new ArrayList<Position>(); // Now process lots, breaking them by instrument positions String symbol = null; Position instrumentPosition = null; for (Lot lot : lots) { // If there is a change in symbol, then create a new instumentPosition if (symbol==null || !symbol.equals(lot.getSymbol())) { symbol = lot.getSymbol(); instrumentPosition = new Position(); instrumentPosition.setInstrumentPosition( this.id, this.name, lot.getSymbol(), referenceDataService.lookup(lot.getSymbol()).getName(), marketDataService.getMarketPrice(lot.getSymbol())); instrumentPositions.add(instrumentPosition); } Position lotPosition = new Position(); lotPosition.setLotPosition( this.id, this.name, lot.getSymbol(), referenceDataService.lookup(lot.getSymbol()).getName(), lot.getId(), lot.getCreationTime(), lot.getQuantity(), marketDataService.getMarketPrice(lot.getSymbol()), lot.getPricePaidPerShare()); instrumentPosition.addChild(lotPosition); } // Calculate instrument position values for (Position position : instrumentPositions) { position.calculateInstrumentPosition(); } // Add cash position Position cashPosition = new Position(); cashPosition.setCashPosition( this.id, this.name, this.cashPosition); instrumentPositions.add(cashPosition); return instrumentPositions; } @Override public boolean isCashAvailable( Money amount, MarketDataService marketDataService) { return amount.lteq(calculateCashAvailable(marketDataService)); } public Money calculateCashAvailable(MarketDataService marketDataService) { Money cashAvailable = cashPosition; // Reduce cash available by estimated cost of buy orders List<Order> orders = brokerageAccountRepository.findActiveBuyOrders(this); for (Order order : orders) { OrderEstimate orderEstimate = order.calculateOrderEstimate(marketDataService); cashAvailable = cashAvailable.minus(orderEstimate.getEstimatedValueInclFees()); } return cashAvailable; } @Override public boolean isSecurityAvailable( String symbol, DecimalQuantity quantity) { return quantity.lteq(calculateSecurityAvailable(symbol)); } public DecimalQuantity calculateSecurityAvailable(String symbol) { DecimalQuantity securityAvailable = brokerageAccountRepository.getNumberOfShares(this, symbol); // Reduce security available by estimated quantity of sell orders List<Order> orders = brokerageAccountRepository.findActiveSellOrders(this, symbol); for (Order order : orders) { securityAvailable = securityAvailable.minus(order.getQuantity()); } return securityAvailable; } public OrderEstimate calculateOrderEstimate( OrderParams params, MarketDataService marketDataService) { Order order = new Order(params); OrderEstimate orderEstimate = order.calculateOrderEstimate(marketDataService); // Determine account level compliance if (orderEstimate.getCompliance() == null) { OrderCompliance compliance = (order.getSide() == OrderSide.Buy) ? calculateBuyOrderCompliance(order, orderEstimate, marketDataService) : calculateSellOrderCompliance(order); orderEstimate.setCompliance(compliance); } return orderEstimate; } private OrderCompliance calculateBuyOrderCompliance( Order order, OrderEstimate orderEstimate, MarketDataService marketDataService) { // Check if sufficient cash is available return isCashAvailable( orderEstimate.getEstimatedValueInclFees(), marketDataService) ? OrderCompliance.Compliant : OrderCompliance.InsufficientFunds; } private OrderCompliance calculateSellOrderCompliance(Order order) { // Check if sufficient securities are available return isSecurityAvailable( order.getSymbol(), order.getQuantity()) ? OrderCompliance.Compliant : OrderCompliance.InsufficientQuantity; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(name).append(" - "); builder.append(id).append(" | "); builder.append(cashPosition); return builder.toString(); } // ----- Attributes ----- private OwnershipType ownershipType = OwnershipType.Individual; private Money cashPosition = new Money("0.00"); private Set<Order> orders = new HashSet<Order>(); private Set<Lot> lots = new HashSet<Lot>(); @Transient private BrokerageAccountRepository brokerageAccountRepository; @Transient private OrderEventPublisher orderEventPublisher; // ----- Getters and Setters ----- @Type( type = "org.archfirst.common.hibernate.GenericEnumUserType", parameters = { @Parameter ( name = "enumClass", value = "org.archfirst.bfoms.domain.account.OwnershipType") } ) @Column(length=Constants.ENUM_COLUMN_LENGTH) public OwnershipType getOwnershipType() { return ownershipType; } private void setOwnershipType(OwnershipType ownershipType) { this.ownershipType = ownershipType; } @Embedded @AttributeOverrides({ @AttributeOverride(name="amount", column = @Column( name="cashpos_amount", precision=Constants.MONEY_PRECISION, scale=Constants.MONEY_SCALE)), @AttributeOverride(name="currency", column = @Column( name="cashpos_currency", length=Money.CURRENCY_LENGTH)) }) public Money getCashPosition() { return cashPosition; } private void setCashPosition(Money cashPosition) { this.cashPosition = cashPosition; } @OneToMany(mappedBy="account", cascade=CascadeType.ALL) @OptimisticLock(excluded = true) public Set<Order> getOrders() { return orders; } private void setOrders(Set<Order> orders) { this.orders = orders; } @OneToMany(mappedBy="account", cascade=CascadeType.ALL) @OptimisticLock(excluded = true) public Set<Lot> getLots() { return lots; } private void setLots(Set<Lot> lots) { this.lots = lots; } // Needed by BrokerageAccountRepository void setBrokerageAccountRepository( BrokerageAccountRepository brokerageAccountRepository) { this.brokerageAccountRepository = brokerageAccountRepository; } // Needed by BrokerageAccountRepository void setOrderEventPublisher(OrderEventPublisher orderEventPublisher) { this.orderEventPublisher = orderEventPublisher; } // ----- AllocationFactory ----- private abstract class AllocationFactory { protected BrokerageAccountRepository accountRepository; public AllocationFactory(BrokerageAccountRepository accountRepository) { this.accountRepository = accountRepository; } public abstract Allocation createAllocation(DecimalQuantity quantity); } private class SecuritiesTransferAllocationFactory extends AllocationFactory { private SecuritiesTransfer transfer; public SecuritiesTransferAllocationFactory( BrokerageAccountRepository accountRepository, SecuritiesTransfer transfer) { super(accountRepository); this.transfer = transfer; } @Override public Allocation createAllocation(DecimalQuantity quantity) { SecuritiesTransferAllocation allocation = new SecuritiesTransferAllocation(quantity, transfer); accountRepository.persistAndFlush(allocation); return allocation; } } private class TradeAllocationFactory extends AllocationFactory { private Trade trade; public TradeAllocationFactory( BrokerageAccountRepository accountRepository, Trade trade) { super(accountRepository); this.trade = trade; } @Override public Allocation createAllocation(DecimalQuantity quantity) { TradeAllocation allocation = new TradeAllocation(quantity, trade); accountRepository.persistAndFlush(allocation); return allocation; } } }