/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* 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.
*/
using System;
using System.Collections.Generic;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
namespace QuantConnect.Securities
{
///
/// Represents a buying power model for cash accounts
///
public class CashBuyingPowerModel : BuyingPowerModel
{
///
/// Initializes a new instance of the class
///
public CashBuyingPowerModel()
: base(1m, 0m, 0m)
{
}
///
/// Gets the current leverage of the security
///
/// The security to get leverage for
/// The current leverage in the security
public override decimal GetLeverage(Security security)
{
// Always returns 1. Cash accounts have no leverage.
return 1m;
}
///
/// Sets the leverage for the applicable securities, i.e, equities
///
///
/// This is added to maintain backwards compatibility with the old margin/leverage system
///
/// The security to set leverage for
/// The new leverage
public override void SetLeverage(Security security, decimal leverage)
{
if (leverage != 1)
{
throw new InvalidOperationException(Messages.CashBuyingPowerModel.UnsupportedLeverage);
}
}
///
/// The margin that must be held in order to increase the position by the provided quantity
///
/// An object containing the security and quantity of shares
public override InitialMargin GetInitialMarginRequirement(InitialMarginParameters parameters)
{
var security = parameters.Security;
var quantity = parameters.Quantity;
return security.QuoteCurrency.ConversionRate
* security.SymbolProperties.ContractMultiplier
* security.Price
* quantity;
}
///
/// Check if there is sufficient buying power to execute this order.
///
/// An object containing the portfolio, the security and the order
/// Returns buying power information for an order
public override HasSufficientBuyingPowerForOrderResult HasSufficientBuyingPowerForOrder(HasSufficientBuyingPowerForOrderParameters parameters)
{
var baseCurrency = parameters.Security as IBaseCurrencySymbol;
if (baseCurrency == null)
{
return parameters.Insufficient(Messages.CashBuyingPowerModel.UnsupportedSecurity(parameters.Security));
}
decimal totalQuantity;
decimal orderQuantity;
if (parameters.Order.Direction == OrderDirection.Buy)
{
// quantity available for buying in quote currency
totalQuantity = parameters.Security.QuoteCurrency.Amount;
orderQuantity = parameters.Order.AbsoluteQuantity * GetOrderPrice(parameters.Security, parameters.Order);
}
else
{
// quantity available for selling in base currency
totalQuantity = baseCurrency.BaseCurrency.Amount;
orderQuantity = parameters.Order.AbsoluteQuantity;
}
// calculate reserved quantity for open orders (in quote or base currency depending on direction)
var openOrdersReservedQuantity = GetOpenOrdersReservedQuantity(parameters.Portfolio, parameters.Security, parameters.Order);
if (parameters.Order.Direction == OrderDirection.Sell)
{
// can sell available and non-reserved quantities
if (orderQuantity <= totalQuantity - openOrdersReservedQuantity)
{
return parameters.Sufficient();
}
return parameters.Insufficient(Messages.CashBuyingPowerModel.SellOrderShortHoldingsNotSupported(totalQuantity,
openOrdersReservedQuantity, orderQuantity, baseCurrency));
}
var maximumQuantity = 0m;
if (parameters.Order.Type == OrderType.Market)
{
// include existing holdings (in quote currency)
var holdingsValue =
parameters.Portfolio.CashBook.Convert(baseCurrency.BaseCurrency.Amount, baseCurrency.BaseCurrency.Symbol, parameters.Security.QuoteCurrency.Symbol);
// find a target value in account currency for buy market orders
var targetValue =
parameters.Portfolio.CashBook.ConvertToAccountCurrency(totalQuantity - openOrdersReservedQuantity + holdingsValue,
parameters.Security.QuoteCurrency.Symbol);
// convert the target into a percent in relation to TPV
var targetPercent = parameters.Portfolio.TotalPortfolioValue == 0 ? 0 : targetValue / parameters.Portfolio.TotalPortfolioValue;
// maximum quantity that can be bought (in quote currency)
maximumQuantity =
GetMaximumOrderQuantityForTargetBuyingPower(
new GetMaximumOrderQuantityForTargetBuyingPowerParameters(parameters.Portfolio, parameters.Security, targetPercent, 0)).Quantity * GetOrderPrice(parameters.Security, parameters.Order);
if (orderQuantity <= Math.Abs(maximumQuantity))
{
return parameters.Sufficient();
}
return parameters.Insufficient(Messages.CashBuyingPowerModel.BuyOrderQuantityGreaterThanMaxForBuyingPower(totalQuantity,
maximumQuantity, openOrdersReservedQuantity, orderQuantity, baseCurrency, parameters.Security, parameters.Order));
}
// for limit orders, add fees to the order cost
var orderFee = 0m;
if (parameters.Order.Type == OrderType.Limit)
{
var fee = parameters.Security.FeeModel.GetOrderFee(
new OrderFeeParameters(parameters.Security,
parameters.Order)).Value;
orderFee = parameters.Portfolio.CashBook.Convert(
fee.Amount,
fee.Currency,
parameters.Security.QuoteCurrency.Symbol);
}
maximumQuantity = totalQuantity - openOrdersReservedQuantity - orderFee;
if (orderQuantity <= maximumQuantity)
{
return parameters.Sufficient();
}
return parameters.Insufficient(Messages.CashBuyingPowerModel.BuyOrderQuantityGreaterThanMaxForBuyingPower(totalQuantity,
maximumQuantity, openOrdersReservedQuantity, orderQuantity, baseCurrency, parameters.Security, parameters.Order));
}
///
/// Get the maximum market order quantity to obtain a delta in the buying power used by a security.
/// The deltas sign defines the position side to apply it to, positive long, negative short.
///
/// An object containing the portfolio, the security and the delta buying power
/// Returns the maximum allowed market order quantity and if zero, also the reason
/// Used by the margin call model to reduce the position by a delta percent.
public override GetMaximumOrderQuantityResult GetMaximumOrderQuantityForDeltaBuyingPower(
GetMaximumOrderQuantityForDeltaBuyingPowerParameters parameters)
{
throw new NotImplementedException(Messages.CashBuyingPowerModel.GetMaximumOrderQuantityForDeltaBuyingPowerNotImplemented);
}
///
/// Get the maximum market order quantity to obtain a position with a given buying power percentage.
/// Will not take into account free buying power.
///
/// An object containing the portfolio, the security and the target signed buying power percentage
/// Returns the maximum allowed market order quantity and if zero, also the reason
public override GetMaximumOrderQuantityResult GetMaximumOrderQuantityForTargetBuyingPower(GetMaximumOrderQuantityForTargetBuyingPowerParameters parameters)
{
var targetPortfolioValue = parameters.TargetBuyingPower * parameters.Portfolio.TotalPortfolioValue;
// no shorting allowed
if (targetPortfolioValue < 0)
{
return new GetMaximumOrderQuantityResult(0, Messages.CashBuyingPowerModel.ShortingNotSupported);
}
var baseCurrency = parameters.Security as IBaseCurrencySymbol;
if (baseCurrency == null)
{
return new GetMaximumOrderQuantityResult(0, Messages.CashBuyingPowerModel.InvalidSecurity);
}
// if target value is zero, return amount of base currency available to sell
if (targetPortfolioValue == 0)
{
return new GetMaximumOrderQuantityResult(-baseCurrency.BaseCurrency.Amount);
}
// convert base currency cash to account currency
var baseCurrencyPosition = parameters.Portfolio.CashBook.ConvertToAccountCurrency(
baseCurrency.BaseCurrency.Amount,
baseCurrency.BaseCurrency.Symbol);
// remove directionality, we'll work in the land of absolutes
var targetOrderValue = Math.Abs(targetPortfolioValue - baseCurrencyPosition);
var direction = targetPortfolioValue > baseCurrencyPosition ? OrderDirection.Buy : OrderDirection.Sell;
// determine the unit price in terms of the account currency
var unitPrice = direction == OrderDirection.Buy ? parameters.Security.AskPrice : parameters.Security.BidPrice;
unitPrice *= parameters.Security.QuoteCurrency.ConversionRate * parameters.Security.SymbolProperties.ContractMultiplier;
if (unitPrice == 0)
{
if (parameters.Security.QuoteCurrency.ConversionRate == 0)
{
return new GetMaximumOrderQuantityResult(0,
Messages.CashBuyingPowerModel.NoDataInInternalCashFeedYet(parameters.Security, parameters.Portfolio));
}
if (parameters.Security.SymbolProperties.ContractMultiplier == 0)
{
return new GetMaximumOrderQuantityResult(0, Messages.CashBuyingPowerModel.ZeroContractMultiplier(parameters.Security));
}
// security.Price == 0
return new GetMaximumOrderQuantityResult(0, parameters.Security.Symbol.GetZeroPriceMessage());
}
// continue iterating while we do not have enough cash for the order
decimal orderFees = 0;
decimal currentOrderValue = 0;
// compute the initial order quantity
var orderQuantity = targetOrderValue / unitPrice;
// rounding off Order Quantity to the nearest multiple of Lot Size
orderQuantity -= orderQuantity % parameters.Security.SymbolProperties.LotSize;
if (orderQuantity == 0)
{
string reason = null;
if (!parameters.SilenceNonErrorReasons)
{
reason = Messages.CashBuyingPowerModel.OrderQuantityLessThanLotSize(parameters.Security);
}
return new GetMaximumOrderQuantityResult(0, reason, false);
}
// Just in case...
var lastOrderQuantity = 0m;
var utcTime = parameters.Security.LocalTime.ConvertToUtc(parameters.Security.Exchange.TimeZone);
do
{
// Each loop will reduce the order quantity based on the difference between
// (cashRequired + orderFees) and targetOrderValue
if (currentOrderValue > targetOrderValue)
{
var currentOrderValuePerUnit = currentOrderValue / orderQuantity;
var amountOfOrdersToRemove = (currentOrderValue - targetOrderValue) / currentOrderValuePerUnit;
if (amountOfOrdersToRemove < parameters.Security.SymbolProperties.LotSize)
{
// we will always substract at leat 1 LotSize
amountOfOrdersToRemove = parameters.Security.SymbolProperties.LotSize;
}
orderQuantity -= amountOfOrdersToRemove;
}
// rounding off Order Quantity to the nearest multiple of Lot Size
orderQuantity -= orderQuantity % parameters.Security.SymbolProperties.LotSize;
if (orderQuantity <= 0)
{
return new GetMaximumOrderQuantityResult(0,
Messages.CashBuyingPowerModel.OrderQuantityLessThanLotSize(parameters.Security) +
Messages.CashBuyingPowerModel.OrderQuantityLessThanLotSizeOrderDetails(targetOrderValue, orderQuantity, orderFees)
);
}
if (lastOrderQuantity == orderQuantity)
{
throw new ArgumentException(Messages.CashBuyingPowerModel.FailedToConvergeOnTargetOrderValue(targetOrderValue, currentOrderValue,
orderQuantity, orderFees, parameters.Security));
}
lastOrderQuantity = orderQuantity;
// generate the order
var order = new MarketOrder(parameters.Security.Symbol, orderQuantity, utcTime);
var orderValue = orderQuantity * unitPrice;
var fees = parameters.Security.FeeModel.GetOrderFee(
new OrderFeeParameters(parameters.Security,
order)).Value;
orderFees = parameters.Portfolio.CashBook.ConvertToAccountCurrency(fees).Amount;
currentOrderValue = orderValue + orderFees;
} while (currentOrderValue > targetOrderValue);
// add directionality back in
return new GetMaximumOrderQuantityResult((direction == OrderDirection.Sell ? -1 : 1) * orderQuantity);
}
///
/// Gets the amount of buying power reserved to maintain the specified position
///
/// A parameters object containing the security
/// The reserved buying power in account currency
public override ReservedBuyingPowerForPosition GetReservedBuyingPowerForPosition(ReservedBuyingPowerForPositionParameters parameters)
{
// Always returns 0. Since we're purchasing currencies outright, the position doesn't consume buying power
return parameters.ResultInAccountCurrency(0m);
}
///
/// Gets the buying power available for a trade
///
/// A parameters object containing the algorithm's portfolio, security, and order direction
/// The buying power available for the trade
public override BuyingPower GetBuyingPower(BuyingPowerParameters parameters)
{
var security = parameters.Security;
var portfolio = parameters.Portfolio;
var direction = parameters.Direction;
var baseCurrency = security as IBaseCurrencySymbol;
if (baseCurrency == null)
{
return parameters.ResultInAccountCurrency(0m);
}
var baseCurrencyPosition = baseCurrency.BaseCurrency.Amount;
var quoteCurrencyPosition = portfolio.CashBook[security.QuoteCurrency.Symbol].Amount;
// determine the unit price in terms of the quote currency
var utcTime = parameters.Security.LocalTime.ConvertToUtc(parameters.Security.Exchange.TimeZone);
var unitPrice = new MarketOrder(security.Symbol, 1, utcTime).GetValue(security) / security.QuoteCurrency.ConversionRate;
if (unitPrice == 0)
{
return parameters.ResultInAccountCurrency(0m);
}
// NOTE: This is returning in units of the BASE currency
if (direction == OrderDirection.Buy)
{
// invert units for math, 6500USD per BTC, currency pairs aren't real fractions
// (USD)/(BTC/USD) => 10kUSD/ (6500 USD/BTC) => 10kUSD * (1BTC/6500USD) => ~ 1.5BTC
return parameters.Result(quoteCurrencyPosition / unitPrice, baseCurrency.BaseCurrency.Symbol);
}
if (direction == OrderDirection.Sell)
{
return parameters.Result(baseCurrencyPosition, baseCurrency.BaseCurrency.Symbol);
}
return parameters.ResultInAccountCurrency(0m);
}
private static decimal GetOrderPrice(Security security, Order order)
{
var orderPrice = 0m;
switch (order.Type)
{
case OrderType.Market:
orderPrice = security.Price;
break;
case OrderType.Limit:
orderPrice = ((LimitOrder)order).LimitPrice;
break;
case OrderType.StopMarket:
orderPrice = ((StopMarketOrder)order).StopPrice;
break;
case OrderType.StopLimit:
orderPrice = ((StopLimitOrder)order).LimitPrice;
break;
case OrderType.LimitIfTouched:
orderPrice = ((LimitIfTouchedOrder)order).LimitPrice;
break;
case OrderType.TrailingStop:
orderPrice = ((TrailingStopOrder)order).StopPrice;
break;
}
return orderPrice;
}
private static decimal GetOpenOrdersReservedQuantity(SecurityPortfolioManager portfolio, Security security, Order order)
{
var baseCurrency = security as IBaseCurrencySymbol;
if (baseCurrency == null) return 0;
// find the target currency for the requested direction and the securities potentially involved
var targetCurrency = order.Direction == OrderDirection.Buy
? security.QuoteCurrency.Symbol
: baseCurrency.BaseCurrency.Symbol;
var symbolDirectionPairs = new Dictionary();
foreach (var portfolioSecurity in portfolio.Securities.Values)
{
var basePortfolioSecurity = portfolioSecurity as IBaseCurrencySymbol;
if (basePortfolioSecurity == null) continue;
if (basePortfolioSecurity.BaseCurrency.Symbol == targetCurrency)
{
symbolDirectionPairs.Add(portfolioSecurity.Symbol, OrderDirection.Sell);
}
else if (portfolioSecurity.QuoteCurrency.Symbol == targetCurrency)
{
symbolDirectionPairs.Add(portfolioSecurity.Symbol, OrderDirection.Buy);
}
}
// fetch open orders with matching symbol/side
var openOrders = portfolio.Transactions.GetOpenOrders(x =>
{
OrderDirection dir;
return symbolDirectionPairs.TryGetValue(x.Symbol, out dir) &&
// same direction of our order
dir == x.Direction &&
// don't count our current order
x.Id != order.Id &&
// only count working orders
(x.Type == OrderType.Limit || x.Type == OrderType.StopMarket);
}
);
// calculate reserved quantity for selected orders
var openOrdersReservedQuantity = 0m;
foreach (var openOrder in openOrders)
{
var orderSecurity = portfolio.Securities[openOrder.Symbol];
var orderBaseCurrency = orderSecurity as IBaseCurrencySymbol;
if (orderBaseCurrency != null)
{
// convert order value to target currency
var quantityInTargetCurrency = openOrder.AbsoluteQuantity;
if (orderSecurity.QuoteCurrency.Symbol == targetCurrency)
{
quantityInTargetCurrency *= GetOrderPrice(security, openOrder);
}
openOrdersReservedQuantity += quantityInTargetCurrency;
}
}
return openOrdersReservedQuantity;
}
}
}