/*
* Author: Balch
* Created: 9/4/14 12:26 AM
*
* This file is part of MockTrade.
*
* MockTrade is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* MockTrade is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MockTrade. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright (C) 2014
*/
package com.balch.mocktrade.order;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import com.balch.android.app.framework.sql.SqlConnection;
import com.balch.android.app.framework.sql.SqlMapper;
import com.balch.android.app.framework.types.Money;
import com.balch.mocktrade.NetworkRequestProvider;
import com.balch.mocktrade.account.Account;
import com.balch.mocktrade.account.AccountSqliteModel;
import com.balch.mocktrade.account.Transaction;
import com.balch.mocktrade.finance.GoogleFinanceModel;
import com.balch.mocktrade.finance.Quote;
import com.balch.mocktrade.investment.Investment;
import com.balch.mocktrade.investment.InvestmentSqliteModel;
import com.balch.mocktrade.settings.Settings;
import java.lang.reflect.InvocationTargetException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
public class OrderSqliteModel implements SqlMapper<Order>, OrderModel, OrderManager.OrderManagerListener {
private static final String TAG = OrderSqliteModel.class.getSimpleName();
private static final String TABLE_NAME = "[order]";
private static final String COLUMN_ACCOUNT_ID = "account_id";
private static final String COLUMN_SYMBOL = "symbol";
private static final String COLUMN_STATUS = "status";
private static final String COLUMN_ACTION = "action";
private static final String COLUMN_STRATEGY = "strategy";
private static final String COLUMN_DURATION = "duration";
private static final String COLUMN_LIMIT_PRICE = "limit_price";
private static final String COLUMN_STOP_PRICE = "stop_price";
private static final String COLUMN_STOP_PERCENT = "stop_percent";
private static final String COLUMN_QUANTITY = "quantity";
private static final String COLUMN_HIGHEST_PRICE = "highest_price";
private final InvestmentSqliteModel mInvestmentModel;
private final AccountSqliteModel mAccountModel;
private final OrderManager mOrderManager;
private final SqlConnection sqlConnection;
public OrderSqliteModel(Context context, NetworkRequestProvider networkRequestProvider,
SqlConnection sqlConnection, Settings settings) {
this.sqlConnection = sqlConnection;
this.mInvestmentModel = new InvestmentSqliteModel(sqlConnection);
this.mAccountModel = new AccountSqliteModel(context, networkRequestProvider,
sqlConnection, settings);
this.mOrderManager = new OrderManager(context,
new GoogleFinanceModel(context, networkRequestProvider, settings),
settings, this);
}
@Override
public String getTableName() {
return TABLE_NAME;
}
public List<Order> getOpenOrders() {
return getOpenOrders(null);
}
public List<Order> getOpenOrders(Long accountId) {
try {
StringBuilder where = new StringBuilder(COLUMN_STATUS+"=?");
List<String> whereArgs = new ArrayList<>();
whereArgs.add(Order.OrderStatus.OPEN.name());
if (accountId != null) {
where.append(" AND ").append(COLUMN_ACCOUNT_ID).append("=?");
whereArgs.add(String.valueOf(accountId));
}
return sqlConnection.query(this, Order.class, where.toString(),
whereArgs.toArray(new String[whereArgs.size()]), null);
} catch (Exception e) {
Log.e(TAG, "Error in getOpenOrders", e);
throw new RuntimeException(e);
}
}
@Override
public void cancelOrder(Order order) throws OrderCancelException {
try {
order.setStatus(Order.OrderStatus.CANCELED);
// only update the order if the status is still open
// still may need some locking to make sure the order
// is not executed after is has been canceled
String where = " AND "+COLUMN_STATUS+"=?";
String [] whereArgs = new String[]{Order.OrderStatus.OPEN.name()};
if (!sqlConnection.update(this, order, where, whereArgs,
sqlConnection.getWritableDatabase())) {
throw new OrderCancelException("Order cannot be canceled");
}
} catch (OrderCancelException ex) {
throw ex;
} catch (Exception ex) {
Log.e(TAG, "Error in cancelOrder", ex);
throw new RuntimeException(ex);
}
}
public void createOrder(Order order) {
try {
sqlConnection.insert(this, order);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public OrderResult attemptExecuteOrder(Order order, Quote quote) throws OrderExecutionException {
try {
return mOrderManager.attemptExecuteOrder(order, quote);
} catch (Exception ex) {
if (order != null) {
order.setStatus(Order.OrderStatus.ERROR);
sqlConnection.update(this, order);
}
throw new OrderExecutionException(ex);
}
}
public OrderResult executeOrder(Order order, Quote quote, Money price) throws SQLException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
SQLiteDatabase db = sqlConnection.getWritableDatabase();
db.beginTransaction();
try {
Money cost = order.getCost(price);
Money profit = new Money(0);
Account account = sqlConnection.queryById(mAccountModel, Account.class, order.getAccount().getId());
Money transactionCost;
Transaction.TransactionType transactionType;
if (order.getAction() == Order.OrderAction.BUY) {
if (account.getAvailableFunds().getDollars() < cost.getDollars()) {
throw new IllegalAccessException("Insufficient funds");
}
transactionType = Transaction.TransactionType.WITHDRAWAL;
} else {
transactionType = Transaction.TransactionType.DEPOSIT;
}
transactionCost = Money.multiply(cost, -1);
Transaction transaction = new Transaction(account, transactionCost, transactionType, "Order Id=" + order.getId());
long transactionId = sqlConnection.insert(transaction, transaction, db);
account.getAvailableFunds().add(transactionCost);
if (!sqlConnection.update(mAccountModel, account, db)) {
throw new IllegalAccessException("Error updating account");
}
Investment investment = mInvestmentModel.getInvestmentBySymbol(order.getSymbol(), order.getAccount().getId());
if (investment == null) {
if (order.getAction() == Order.OrderAction.SELL) {
throw new IllegalAccessException("Can't sell and investment you don't own");
}
investment = new Investment(account, quote.getSymbol(),
Investment.InvestmentStatus.OPEN, quote.getName(), quote.getExchange(),
cost, price, new Date(0), order.getQuantity());
sqlConnection.insert(mInvestmentModel, investment, db);
} else {
if (order.getAction() == Order.OrderAction.SELL) {
if (order.getQuantity() > investment.getQuantity()) {
throw new IllegalAccessException("Selling too many shares");
}
profit = Money.subtract(transactionCost, investment.getCostBasis());
}
investment.aggregateOrder(order, price);
if (investment.getQuantity() > 0) {
if (!sqlConnection.update(mInvestmentModel, investment, db)) {
throw new IllegalAccessException("Error updating investment");
}
} else {
// delete the investment if we sold everything
if (!sqlConnection.delete(mInvestmentModel, investment, db)) {
throw new IllegalAccessException("Error updating investment");
}
}
}
order.setStatus(Order.OrderStatus.FULFILLED);
if (!sqlConnection.update(this, order, db)) {
throw new IllegalAccessException("Error updating order");
}
db.setTransactionSuccessful();
return new OrderResult(true, price, cost, profit, transactionId);
} finally {
db.endTransaction();
}
}
@Override
public Investment getInvestmentBySymbol(String symbol, Long accountId) {
return mInvestmentModel.getInvestmentBySymbol(symbol, accountId);
}
@Override
public boolean updateOrder(Order order) throws IllegalAccessException {
return sqlConnection.update(this, order);
}
public void scheduleOrderServiceAlarm(boolean isMarketOpen){
mOrderManager.scheduleOrderServiceAlarm(isMarketOpen);
}
@Override
public ContentValues getContentValues(Order order) {
ContentValues values = new ContentValues();
values.put(COLUMN_ACCOUNT_ID, order.getAccount().getId());
values.put(COLUMN_SYMBOL, order.getSymbol());
values.put(COLUMN_STATUS, order.getStatus().name());
values.put(COLUMN_ACTION, order.getAction().name());
values.put(COLUMN_STRATEGY, order.getStrategy().name());
values.put(COLUMN_DURATION, order.getDuration().name());
values.put(COLUMN_LIMIT_PRICE, order.getLimitPrice().getMicroCents());
values.put(COLUMN_STOP_PRICE, order.getStopPrice().getMicroCents());
values.put(COLUMN_STOP_PERCENT, order.getStopPercent());
values.put(COLUMN_QUANTITY, order.getQuantity());
values.put(COLUMN_HIGHEST_PRICE, order.getHighestPrice().getMicroCents());
return values;
}
@Override
public void populate(Order order, Cursor cursor, Map<String, Integer> columnMap) {
order.setId(cursor.getLong(columnMap.get(COLUMN_ID)));
Account account = new Account();
account.setId(cursor.getLong(columnMap.get(COLUMN_ACCOUNT_ID)));
order.setAccount(account);
order.setSymbol(cursor.getString(columnMap.get(COLUMN_SYMBOL)));
order.setStatus(Order.OrderStatus.valueOf(cursor.getString(columnMap.get(COLUMN_STATUS))));
order.setAction(Order.OrderAction.valueOf(cursor.getString(columnMap.get(COLUMN_ACTION))));
order.setStrategy(Order.OrderStrategy.valueOf(cursor.getString(columnMap.get(COLUMN_STRATEGY))));
order.setDuration(Order.OrderDuration.valueOf(cursor.getString(columnMap.get(COLUMN_DURATION))));
order.setLimitPrice(new Money(cursor.getLong(columnMap.get(COLUMN_LIMIT_PRICE))));
order.setStopPrice(new Money(cursor.getLong(columnMap.get(COLUMN_STOP_PRICE))));
order.setStopPercent(cursor.getDouble(columnMap.get(COLUMN_STOP_PERCENT)));
order.setQuantity(cursor.getLong(columnMap.get(COLUMN_QUANTITY)));
order.setHighestPrice(new Money(cursor.getLong(columnMap.get(COLUMN_HIGHEST_PRICE))));
}
}