/**
* 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.order;
import java.util.HashSet;
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.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.validation.constraints.NotNull;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlSchemaType;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.archfirst.bfoms.domain.account.brokerage.BrokerageAccount;
import org.archfirst.bfoms.domain.account.brokerage.BrokerageAccountRepository;
import org.archfirst.bfoms.domain.account.brokerage.Trade;
import org.archfirst.bfoms.domain.marketdata.MarketDataService;
import org.archfirst.bfoms.domain.util.Constants;
import org.archfirst.common.datetime.DateTimeAdapter;
import org.archfirst.common.datetime.DateTimeUtil;
import org.archfirst.common.domain.DomainEntity;
import org.archfirst.common.money.Money;
import org.archfirst.common.quantity.DecimalQuantity;
import org.archfirst.common.quantity.DecimalQuantityMin;
import org.hibernate.annotations.OptimisticLock;
import org.hibernate.annotations.Parameter;
import org.hibernate.annotations.Type;
import org.joda.time.DateTime;
/**
* Order
*
* @author Naresh Bhatia
*/
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "Order")
@Entity
@Table(name="Orders") // Oracle gets confused with table named "Order"
public class Order extends DomainEntity implements Comparable<Order> {
private static final long serialVersionUID = 1L;
// ----- Constructors -----
private Order() {
}
public Order(OrderParams params) {
this.setCreationTime(new DateTime());
this.side = params.getSide();
this.symbol = params.getSymbol();
this.quantity = new DecimalQuantity(params.getQuantity());
this.type = params.getType();
if (this.type == OrderType.Limit) {
this.limitPrice = params.getLimitPrice();
}
this.term = params.getTerm();
this.allOrNone = params.isAllOrNone();
}
// ----- Commands -----
/**
* Processes the specified execution report and returns a trade if the
* order is closed and all of the execution reports have been received.
* Note that because of parallel processing of incoming messages it is
* possible to receive execution reports in a random order. To work around
* this issue, this method "closes" the order only if it is in a closed
* state (Filled, Canceled or DoneForDay) and the filled quantity recorded
* at close time is equal to the sum of quantities of the executions.
*
* @param executionReport
* @return a trade if the order is closed, otherwise null
*/
public Trade processExecutionReport(
ExecutionReport executionReport,
BrokerageAccountRepository accountRepository,
OrderEventPublisher orderEventPublisher) {
// Record status if it is moving forward
OrderStatus newStatus = executionReport.getOrderStatus();
if (isStatusChangeForward(newStatus)) {
this.status = executionReport.getOrderStatus();
orderEventPublisher.publish(new OrderStatusChanged(this));
}
// Record CumQty if it is higher
if (executionReport.getCumQty().gt(this.cumQty)) {
this.cumQty = executionReport.getCumQty();
}
// If ExecutionReportType is Trade, then add an execution to this order
if (executionReport.getType() == ExecutionReportType.Trade) {
this.addExecution(
new Execution(
new DateTime(),
executionReport.getLastQty(),
executionReport.getLastPrice()));
}
// If order is closed and all executions received and something was traded,
// then return a trade
Trade trade = null;
if (isClosed() &&
(this.cumQty.eq(getCumQtyOfExecutions())) &&
(this.cumQty.gt(DecimalQuantity.ZERO))) {
trade = new Trade(
new DateTime(),
this.side,
this.symbol,
this.getCumQty(),
this.getTotalPriceOfExecutions(),
this.getFees(),
this);
}
return trade;
}
public void pendingCancel(OrderEventPublisher orderEventPublisher) {
OrderStatus newStatus = OrderStatus.PendingCancel;
if (isStatusChangeValid(newStatus)) {
this.status = newStatus;
orderEventPublisher.publish(new OrderStatusChanged(this));
}
else {
throw new IllegalArgumentException(
"Can't change status from " + this.status + " to " + newStatus);
}
}
public void cancelRequestRejected(
OrderStatus newStatus,
OrderEventPublisher orderEventPublisher) {
this.status = newStatus;
orderEventPublisher.publish(new OrderStatusChanged(this));
}
private void addExecution(Execution execution) {
executions.add(execution);
execution.setOrder(this);
}
// ----- Queries -----
@Transient
public DecimalQuantity getLeavesQty() {
return quantity.minus(getCumQty());
}
@Transient
public DecimalQuantity getCumQtyOfExecutions() {
DecimalQuantity total = new DecimalQuantity();
for (Execution execution : executions) {
total = total.plus(execution.getQuantity());
}
return total;
}
@Transient
public Money getTotalPriceOfExecutions() {
Money totalPrice = new Money("0.00");
for (Execution execution : executions) {
totalPrice = totalPrice.plus(
execution.getPrice().times(execution.getQuantity()));
}
return totalPrice.scaleToCurrency();
}
@Transient
public Money getWeightedAveragePriceOfExecutions() {
if (executions.size() == 0) {
return new Money("0.00");
}
// Calculate weighted average
Money totalPrice = new Money("0.00");
DecimalQuantity totalQuantity = new DecimalQuantity();
for (Execution execution : executions) {
totalPrice = totalPrice.plus(
execution.getPrice().times(execution.getQuantity()));
totalQuantity = totalQuantity.plus(execution.getQuantity());
}
totalPrice = totalPrice.scaleToCurrency();
return totalPrice.div(totalQuantity, Constants.PRICE_SCALE);
}
@Transient
private Money getFees() {
return FIXED_FEE;
}
/**
* Returns estimated price and compliance of the order.
*
* @param marketDataService
* @return
*/
public OrderEstimate calculateOrderEstimate(MarketDataService marketDataService) {
// Perform order level compliance
if (type == OrderType.Limit && limitPrice == null) {
return new OrderEstimate(OrderCompliance.LimitOrderWithNoLimitPrice);
}
// Calculate estimated values
Money unitPrice = (type == OrderType.Market) ?
marketDataService.getMarketPrice(symbol) : limitPrice;
Money estimatedValue = unitPrice.times(quantity).scaleToCurrency();
Money fees = getFees();
Money estimatedValueInclFees = (side==OrderSide.Buy) ?
estimatedValue.plus(fees) : estimatedValue.minus(fees);
return new OrderEstimate(estimatedValue, fees, estimatedValueInclFees);
}
/** Returns true if this order is closed (Filled, Canceled or DoneForDay) */
@Transient
public boolean isClosed() {
return
(status==OrderStatus.Filled) ||
(status==OrderStatus.Canceled) ||
(status==OrderStatus.DoneForDay);
}
@Transient
private boolean isStatusChangeValid(OrderStatus newStatus) {
boolean result = false;
switch (this.status) {
case PendingNew:
if (newStatus==OrderStatus.New)
result=true;
break;
case New:
case PartiallyFilled:
if (newStatus==OrderStatus.PartiallyFilled ||
newStatus==OrderStatus.Filled ||
newStatus==OrderStatus.PendingCancel ||
newStatus==OrderStatus.Canceled ||
newStatus==OrderStatus.DoneForDay)
result=true;
break;
case PendingCancel:
if (newStatus==OrderStatus.Canceled)
result=true;
break;
case Filled:
case Canceled:
case DoneForDay:
default:
break;
}
return result;
}
@Transient
private boolean isStatusChangeForward(OrderStatus newStatus) {
boolean result = false;
switch (this.status) {
case PendingNew:
if (newStatus==OrderStatus.New ||
newStatus==OrderStatus.PartiallyFilled ||
newStatus==OrderStatus.Filled ||
newStatus==OrderStatus.PendingCancel ||
newStatus==OrderStatus.Canceled ||
newStatus==OrderStatus.DoneForDay)
result=true;
break;
case New:
case PartiallyFilled:
if (newStatus==OrderStatus.PartiallyFilled ||
newStatus==OrderStatus.Filled ||
newStatus==OrderStatus.PendingCancel ||
newStatus==OrderStatus.Canceled ||
newStatus==OrderStatus.DoneForDay)
result=true;
break;
case PendingCancel:
if (newStatus==OrderStatus.New ||
newStatus==OrderStatus.PartiallyFilled ||
newStatus==OrderStatus.Filled ||
newStatus==OrderStatus.Canceled ||
newStatus==OrderStatus.DoneForDay)
result=true;
break;
case Filled:
case Canceled:
case DoneForDay:
default:
break;
}
return result;
}
@Override
public int compareTo(Order that) {
return this.id.compareTo(that.getId());
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(DateTimeUtil.toStringTimestamp(creationTime)).append(" ");
builder.append(id).append(": ");
builder.append(side).append(" ");
builder.append(symbol);
builder.append(", quantity=").append(quantity);
builder.append(", cumQy=").append(this.getCumQty());
builder.append(", orderType=").append(type);
builder.append(", limitPrice=").append(limitPrice);
builder.append(", term=").append(term);
builder.append(", allOrNone=").append(allOrNone);
builder.append(", status=").append(status);
builder.append(", accountId=").append(account.getId());
return builder.toString();
}
// ----- Attributes -----
private static final Money FIXED_FEE = new Money("10.00");
@XmlElement(name = "CreationTime", required = true)
@XmlJavaTypeAdapter(DateTimeAdapter.class)
@XmlSchemaType(name="dateTime")
private DateTime creationTime;
@XmlElement(name = "Side", required = true)
private OrderSide side;
@XmlElement(name = "Symbol", required = true)
private String symbol;
@XmlElement(name = "Quantity", required = true)
private DecimalQuantity quantity;
@XmlElement(name = "CumQty", required = true)
private DecimalQuantity cumQty = DecimalQuantity.ZERO;
@XmlElement(name = "OrderType", required = true)
private OrderType type;
// Limit price is not a required in case of market orders
@XmlElement(name = "LimitPrice")
private Money limitPrice;
@XmlElement(name = "Term", required = true)
private OrderTerm term;
@XmlElement(name = "AllOrNone", required = true)
private boolean allOrNone;
@XmlElement(name = "OrderStatus", required = true)
private OrderStatus status = OrderStatus.PendingNew;
@XmlTransient
private BrokerageAccount account;
@XmlElement(name = "Execution", required = true)
private Set<Execution> executions = new HashSet<Execution>();
// ----- Getters and Setters -----
@Type(type = "org.joda.time.contrib.hibernate.PersistentDateTime")
@Column(nullable = false)
public DateTime getCreationTime() {
return creationTime;
}
// Allow BrokerageAccount to access this method
private void setCreationTime(DateTime creationTime) {
this.creationTime = creationTime;
}
@NotNull
@Type(
type = "org.archfirst.common.hibernate.GenericEnumUserType",
parameters = {
@Parameter (
name = "enumClass",
value = "org.archfirst.bfoms.domain.account.brokerage.order.OrderSide")
}
)
@Column(nullable = false, length=Constants.ENUM_COLUMN_LENGTH)
public OrderSide getSide() {
return side;
}
private void setSide(OrderSide side) {
this.side = side;
}
@NotNull
@Column(nullable = false, length=10)
public String getSymbol() {
return symbol;
}
private void setSymbol(String symbol) {
this.symbol = symbol;
}
@NotNull
@DecimalQuantityMin(value="1")
@Embedded
@AttributeOverrides({
@AttributeOverride(name="value",
column = @Column(
name="quantity",
precision=Constants.QUANTITY_PRECISION,
scale=Constants.QUANTITY_SCALE))})
public DecimalQuantity getQuantity() {
return quantity;
}
private void setQuantity(DecimalQuantity quantity) {
this.quantity = quantity;
}
@NotNull
@Embedded
@AttributeOverrides({
@AttributeOverride(name="value",
column = @Column(
name="cum_qty",
precision=Constants.QUANTITY_PRECISION,
scale=Constants.QUANTITY_SCALE))})
public DecimalQuantity getCumQty() {
return cumQty;
}
private void setCumQty(DecimalQuantity cumQty) {
this.cumQty = cumQty;
}
@NotNull
@Type(
type = "org.archfirst.common.hibernate.GenericEnumUserType",
parameters = {
@Parameter (
name = "enumClass",
value = "org.archfirst.bfoms.domain.account.brokerage.order.OrderType")
}
)
@Column(nullable = false, length=Constants.ENUM_COLUMN_LENGTH)
public OrderType getType() {
return type;
}
private void setType(OrderType type) {
this.type = type;
}
@Embedded
@AttributeOverrides({
@AttributeOverride(name="amount",
column = @Column(
name="limit_price_amount",
precision=Constants.PRICE_PRECISION,
scale=Constants.PRICE_SCALE)),
@AttributeOverride(name="currency",
column = @Column(
name="limit_price_currency",
length=Money.CURRENCY_LENGTH))
})
public Money getLimitPrice() {
return limitPrice;
}
private void setLimitPrice(Money limitPrice) {
this.limitPrice = limitPrice;
}
@NotNull
@Type(
type = "org.archfirst.common.hibernate.GenericEnumUserType",
parameters = {
@Parameter (
name = "enumClass",
value = "org.archfirst.bfoms.domain.account.brokerage.order.OrderTerm")
}
)
@Column(nullable = false, length=Constants.ENUM_COLUMN_LENGTH)
public OrderTerm getTerm() {
return term;
}
private void setTerm(OrderTerm term) {
this.term = term;
}
public boolean isAllOrNone() {
return allOrNone;
}
private void setAllOrNone(boolean allOrNone) {
this.allOrNone = allOrNone;
}
@NotNull
@Type(
type = "org.archfirst.common.hibernate.GenericEnumUserType",
parameters = {
@Parameter (
name = "enumClass",
value = "org.archfirst.bfoms.domain.account.brokerage.order.OrderStatus")
}
)
@Column(nullable = false, length=Constants.ENUM_COLUMN_LENGTH)
public OrderStatus getStatus() {
return status;
}
private void setStatus(OrderStatus status) {
this.status = status;
}
@ManyToOne
public BrokerageAccount getAccount() {
return account;
}
// Allow BrokerageAccount to access this method
public void setAccount(BrokerageAccount account) {
this.account = account;
}
@OneToMany(mappedBy="order", cascade=CascadeType.ALL)
@OptimisticLock(excluded = true)
public Set<Execution> getExecutions() {
return executions;
}
private void setExecutions(Set<Execution> executions) {
this.executions = executions;
}
}