/// <summary> /// Get the maximum market order quantity to obtain a position with a given value in account currency /// </summary> /// <param name="model">The <see cref="IBuyingPowerModel"/></param> /// <param name="portfolio">The algorithm's portfolio</param> /// <param name="security">The security to be traded</param> /// <param name="target">The target percent holdings</param> /// <returns>Returns the maximum allowed market order quantity and if zero, also the reason</returns> public static GetMaximumOrderQuantityForTargetValueResult GetMaximumOrderQuantityForTargetValue( this IBuyingPowerModel model, SecurityPortfolioManager portfolio, Security security, decimal target ) { var parameters = new GetMaximumOrderQuantityForTargetValueParameters(portfolio, security, target); return(model.GetMaximumOrderQuantityForTargetValue(parameters)); }
/// <summary> /// Get the maximum market order quantity to obtain a position with a given value in account currency. /// Will not take into account buying power. /// </summary> /// <param name="parameters">An object containing the portfolio, the security and the target percentage holdings</param> /// <returns>Returns the maximum allowed market order quantity and if zero, also the reason</returns> public virtual GetMaximumOrderQuantityForTargetValueResult GetMaximumOrderQuantityForTargetValue(GetMaximumOrderQuantityForTargetValueParameters parameters) { // adjust target portfolio value to comply with required Free Buying Power Percent var targetPortfolioValue = parameters.Target * (parameters.Portfolio.TotalPortfolioValue - parameters.Portfolio.TotalPortfolioValue * RequiredFreeBuyingPowerPercent); // if targeting zero, simply return the negative of the quantity if (targetPortfolioValue == 0) { return(new GetMaximumOrderQuantityForTargetValueResult(-parameters.Security.Holdings.Quantity, string.Empty, false)); } var currentHoldingsValue = parameters.Security.Holdings.HoldingsValue; // remove directionality, we'll work in the land of absolutes var targetOrderValue = Math.Abs(targetPortfolioValue - currentHoldingsValue); var direction = targetPortfolioValue > currentHoldingsValue ? OrderDirection.Buy : OrderDirection.Sell; // determine the unit price in terms of the account currency var unitPrice = new MarketOrder(parameters.Security.Symbol, 1, DateTime.UtcNow).GetValue(parameters.Security); if (unitPrice == 0) { var reason = $"The price of the {parameters.Security.Symbol.Value} security is zero because it does not have any market " + "data yet. When the security price is set this security will be ready for trading."; return(new GetMaximumOrderQuantityForTargetValueResult(0, reason)); } // calculate the total margin available var marginRemaining = GetMarginRemaining(parameters.Portfolio, parameters.Security, direction); if (marginRemaining <= 0) { var reason = "The portfolio does not have enough margin available."; return(new GetMaximumOrderQuantityForTargetValueResult(0, reason)); } // continue iterating while we do not have enough margin for the order decimal orderValue = 0; decimal orderFees = 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) { var reason = $"The order quantity is less than the lot size of {parameters.Security.SymbolProperties.LotSize} " + "and has been rounded to zero."; return(new GetMaximumOrderQuantityForTargetValueResult(0, reason, false)); } var loopCount = 0; // Just in case... var lastOrderQuantity = 0m; do { // Each loop will reduce the order quantity based on the difference between orderValue and targetOrderValue if (orderValue > targetOrderValue) { var currentOrderValuePerUnit = orderValue / orderQuantity; var amountOfOrdersToRemove = (orderValue - targetOrderValue) / currentOrderValuePerUnit; if (amountOfOrdersToRemove < parameters.Security.SymbolProperties.LotSize) { // we will always substract at leat 1 LotSize amountOfOrdersToRemove = parameters.Security.SymbolProperties.LotSize; } orderQuantity -= amountOfOrdersToRemove; orderQuantity -= orderQuantity % parameters.Security.SymbolProperties.LotSize; } if (orderQuantity <= 0) { var reason = $"The order quantity is less than the lot size of {parameters.Security.SymbolProperties.LotSize} " + $"and has been rounded to zero.Target order value {targetOrderValue}. Order fees " + $"{orderFees}. Order quantity {orderQuantity}."; return(new GetMaximumOrderQuantityForTargetValueResult(0, reason)); } // generate the order var order = new MarketOrder(parameters.Security.Symbol, orderQuantity, DateTime.UtcNow); var fees = parameters.Security.FeeModel.GetOrderFee( new OrderFeeParameters(parameters.Security, order)).Value; orderFees = parameters.Portfolio.CashBook.ConvertToAccountCurrency(fees).Amount; // The TPV, take out the fees(unscaled) => yields available value for trading(less fees) // then scale that by the target -- finally remove currentHoldingsValue to get targetOrderValue targetOrderValue = Math.Abs( (parameters.Portfolio.TotalPortfolioValue - orderFees - parameters.Portfolio.TotalPortfolioValue * RequiredFreeBuyingPowerPercent) * parameters.Target - currentHoldingsValue ); // After the first loop we need to recalculate order quantity since now we have fees included if (loopCount == 0) { // re compute the initial order quantity orderQuantity = targetOrderValue / unitPrice; orderQuantity -= orderQuantity % parameters.Security.SymbolProperties.LotSize; } else { // Start safe check after first loop if (lastOrderQuantity == orderQuantity) { var message = "GetMaximumOrderQuantityForTargetValue failed to converge to target order value " + $"{targetOrderValue}. Current order value is {orderValue}. Order quantity {orderQuantity}. " + $"Lot size is {parameters.Security.SymbolProperties.LotSize}. Order fees {orderFees}. Security symbol " + $"{parameters.Security.Symbol}"; throw new Exception(message); } lastOrderQuantity = orderQuantity; } orderValue = orderQuantity * unitPrice; loopCount++; // we always have to loop at least twice }while (loopCount < 2 || orderValue > targetOrderValue); // add directionality back in return(new GetMaximumOrderQuantityForTargetValueResult((direction == OrderDirection.Sell ? -1 : 1) * orderQuantity)); }
/// <summary> /// Get the maximum market order quantity to obtain a position with a given value in account currency. Will not take into account buying power. /// </summary> /// <param name="parameters">An object containing the portfolio, the security and the target percentage holdings</param> /// <returns>Returns the maximum allowed market order quantity and if zero, also the reason</returns> public override GetMaximumOrderQuantityForTargetValueResult GetMaximumOrderQuantityForTargetValue(GetMaximumOrderQuantityForTargetValueParameters parameters) { var targetPortfolioValue = parameters.Target * parameters.Portfolio.TotalPortfolioValue; // no shorting allowed if (targetPortfolioValue < 0) { return(new GetMaximumOrderQuantityForTargetValueResult(0, "The cash model does not allow shorting.")); } var baseCurrency = parameters.Security as IBaseCurrencySymbol; if (baseCurrency == null) { return(new GetMaximumOrderQuantityForTargetValueResult(0, "The security type must be SecurityType.Crypto or SecurityType.Forex.")); } // if target value is zero, return amount of base currency available to sell if (targetPortfolioValue == 0) { return(new GetMaximumOrderQuantityForTargetValueResult(-parameters.Portfolio.CashBook[baseCurrency.BaseCurrencySymbol].Amount)); } // convert base currency cash to account currency var baseCurrencyPosition = parameters.Portfolio.CashBook.ConvertToAccountCurrency( parameters.Portfolio.CashBook[baseCurrency.BaseCurrencySymbol].Amount, baseCurrency.BaseCurrencySymbol); // convert quote currency cash to account currency var quoteCurrencyPosition = parameters.Portfolio.CashBook.ConvertToAccountCurrency( parameters.Portfolio.CashBook[parameters.Security.QuoteCurrency.Symbol].Amount, parameters.Security.QuoteCurrency.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 GetMaximumOrderQuantityForTargetValueResult(0, $"The internal cash feed required for converting {parameters.Security.QuoteCurrency.Symbol} to {CashBook.AccountCurrency} does not have any data yet (or market may be closed).")); } if (parameters.Security.SymbolProperties.ContractMultiplier == 0) { return(new GetMaximumOrderQuantityForTargetValueResult(0, $"The contract multiplier for the {parameters.Security.Symbol.Value} security is zero. The symbol properties database may be out of date.")); } // security.Price == 0 return(new GetMaximumOrderQuantityForTargetValueResult(0, $"The price of the {parameters.Security.Symbol.Value} security is zero because it does not have any market data yet. When the security price is set this security will be ready for trading.")); } // calculate the total cash available var cashRemaining = direction == OrderDirection.Buy ? quoteCurrencyPosition : baseCurrencyPosition; var currency = direction == OrderDirection.Buy ? parameters.Security.QuoteCurrency.Symbol : baseCurrency.BaseCurrencySymbol; if (cashRemaining <= 0) { return(new GetMaximumOrderQuantityForTargetValueResult(0, $"The portfolio does not hold any {currency} for the order.")); } // 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) { return(new GetMaximumOrderQuantityForTargetValueResult(0, $"The order quantity is less than the lot size of {parameters.Security.SymbolProperties.LotSize} and has been rounded to zero.", false)); } // Just in case... var lastOrderQuantity = 0m; 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 GetMaximumOrderQuantityForTargetValueResult(0, $"The order quantity is less than the lot size of {parameters.Security.SymbolProperties.LotSize} and has been rounded to zero." + $"Target order value {targetOrderValue}. Order fees {orderFees}. Order quantity {orderQuantity}.")); } if (lastOrderQuantity == orderQuantity) { throw new Exception($"GetMaximumOrderQuantityForTargetValue failed to converge to target order value {targetOrderValue}. " + $"Current order value is {currentOrderValue}. Order quantity {orderQuantity}. Lot size is " + $"{parameters.Security.SymbolProperties.LotSize}. Order fees {orderFees}. Security symbol {parameters.Security.Symbol}"); } lastOrderQuantity = orderQuantity; // generate the order var order = new MarketOrder(parameters.Security.Symbol, orderQuantity, DateTime.UtcNow); var orderValue = orderQuantity * unitPrice; orderFees = parameters.Security.FeeModel.GetOrderFee(parameters.Security, order); currentOrderValue = orderValue + orderFees; } while (currentOrderValue > targetOrderValue); // add directionality back in return(new GetMaximumOrderQuantityForTargetValueResult((direction == OrderDirection.Sell ? -1 : 1) * orderQuantity)); }