Files
quantconnect--lean/Tests/Common/Securities/SecurityPortfolioManagerTests.cs
2026-07-13 13:02:50 +08:00

2988 lines
144 KiB
C#

/*
* 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.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using NUnit.Framework;
using QuantConnect.Brokerages;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Indicators;
using QuantConnect.Orders;
using QuantConnect.Securities;
using QuantConnect.Util;
using QuantConnect.Securities.Option;
using QuantConnect.Lean.Engine.TransactionHandlers;
using QuantConnect.Brokerages.Backtesting;
using QuantConnect.Tests.Engine;
using QuantConnect.Algorithm;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Lean.Engine.Results;
using QuantConnect.Orders.Fees;
using QuantConnect.Tests.Engine.DataFeeds;
using QuantConnect.Securities.CurrencyConversion;
namespace QuantConnect.Tests.Common.Securities
{
[TestFixture]
public class SecurityPortfolioManagerTests
{
private static readonly SecurityExchangeHours SecurityExchangeHours = SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork);
private static readonly Symbol CASH = new Symbol(SecurityIdentifier.GenerateBase(null, "CASH", Market.USA), "CASH");
private static readonly Symbol MCHJWB = new Symbol(SecurityIdentifier.GenerateForex("MCHJWB", Market.FXCM), "MCHJWB");
private static readonly Symbol MCHUSD = new Symbol(SecurityIdentifier.GenerateForex("MCHUSD", Market.FXCM), "MCHUSD");
private static readonly Symbol USDJWB = new Symbol(SecurityIdentifier.GenerateForex("USDJWB", Market.FXCM), "USDJWB");
private static readonly Symbol JWBUSD = new Symbol(SecurityIdentifier.GenerateForex("JWBUSD", Market.FXCM), "JWBUSD");
private static readonly Dictionary<string, Symbol> SymbolMap = new Dictionary<string, Symbol>
{
{"CASH", CASH},
{"MCHJWB", MCHJWB},
{"MCHUSD", MCHUSD},
{"USDJWB", USDJWB},
{"JWBUSD", JWBUSD},
};
private QCAlgorithm _algorithm;
private SecurityManager _securities;
private SecurityTransactionManager _transactions;
private BacktestingTransactionHandler _transactionHandler;
private SecurityPortfolioManager _portfolio;
private BacktestingBrokerage _backtestingBrokerage;
private IResultHandler _resultHandler;
[SetUp]
public void SetUp()
{
_resultHandler = new TestResultHandler(Console.WriteLine);
_algorithm = new QCAlgorithm();
_securities = new SecurityManager(TimeKeeper);
_transactions = new SecurityTransactionManager(null, _securities);
_transactionHandler = new BacktestingTransactionHandler();
_portfolio = new SecurityPortfolioManager(_securities, _transactions, new AlgorithmSettings());
_algorithm.Securities = _securities;
_backtestingBrokerage = new BacktestingBrokerage(_algorithm);
_transactionHandler.Initialize(_algorithm, _backtestingBrokerage, _resultHandler);
_transactions.SetOrderProcessor(_transactionHandler);
}
[TearDown]
public void TearDown()
{
_transactionHandler.Exit();
_resultHandler.Exit();
_backtestingBrokerage.Dispose();
}
[Test]
public void TestCashFills()
{
// this test asserts the portfolio behaves according to the Test_Cash algo, see TestData\CashTestingStrategy.csv
// also "https://www.dropbox.com/s/oiliumoyqqj1ovl/2013-cash.csv?dl=1"
const string fillsFile = "TestData/test_cash_fills.xml";
const string equityFile = "TestData/test_cash_equity.xml";
var fills = XDocument.Load(fillsFile).Descendants("OrderEvent").Select(x => new OrderEvent(
x.Get<int>("OrderId"),
SymbolMap[x.Get<string>("Symbol")],
DateTime.MinValue,
x.Get<OrderStatus>("Status"),
x.Get<int>("FillQuantity") < 0 ? OrderDirection.Sell
: x.Get<int>("FillQuantity") > 0 ? OrderDirection.Buy
: OrderDirection.Hold,
x.Get<decimal>("FillPrice"),
x.Get<int>("FillQuantity"),
OrderFee.Zero)
).ToList();
var equity = XDocument.Load(equityFile).Descendants("decimal")
.Select(x => Parse.Decimal(x.Value))
.ToList();
Assert.AreEqual(fills.Count + 1, equity.Count);
// we're going to process fills and very our equity after each fill
var subscriptions = new SubscriptionManager(TimeKeeper);
subscriptions.SetDataManager(new DataManagerStub(TimeKeeper));
var securities = new SecurityManager(TimeKeeper);
MarketHoursDatabase.FromDataFolder().SetEntryAlwaysOpen(CASH.ID.Market, CASH.Value, CASH.SecurityType, TimeZones.NewYork);
var security = new Security(
SecurityExchangeHours,
subscriptions.Add(CASH, Resolution.Daily, TimeZones.NewYork, TimeZones.NewYork),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
);
security.SetLeverage(10m);
securities.Add(CASH, security);
var transactions = new SecurityTransactionManager(null, securities);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.SetCash(equity[0]);
for (int i = 0; i < fills.Count; i++)
{
// before processing the fill we must deduct the cost
var fill = fills[i];
var time = DateTime.Today.AddDays(i);
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
// the value of 'CASH' increments for each fill, the original test algo did this monthly
// the time doesn't really matter though
security.SetMarketPrice(new IndicatorDataPoint(CASH, time, i + 1));
portfolio.ProcessFills(new List<OrderEvent> { fill });
Assert.AreEqual(equity[i + 1], portfolio.TotalPortfolioValue, "Failed on " + i);
}
}
[Test]
public void ForexCashFills()
{
// this test asserts the portfolio behaves according to the Test_Cash algo, but for a Forex security,
// see TestData\CashTestingStrategy.csv; also "https://www.dropbox.com/s/oiliumoyqqj1ovl/2013-cash.csv?dl=1"
const string fillsFile = "TestData/test_forex_fills.xml";
const string equityFile = "TestData/test_forex_equity.xml";
const string mchQuantityFile = "TestData/test_forex_fills_mch_quantity.xml";
const string jwbQuantityFile = "TestData/test_forex_fills_jwb_quantity.xml";
var fills = XDocument.Load(fillsFile).Descendants("OrderEvent").Select(x => new OrderEvent(
x.Get<int>("OrderId"),
SymbolMap[x.Get<string>("Symbol")],
DateTime.MinValue,
x.Get<OrderStatus>("Status"),
x.Get<int>("FillQuantity") < 0 ? OrderDirection.Sell
: x.Get<int>("FillQuantity") > 0 ? OrderDirection.Buy
: OrderDirection.Hold,
x.Get<decimal>("FillPrice"),
x.Get<int>("FillQuantity"),
OrderFee.Zero)
).ToList();
var equity = XDocument.Load(equityFile).Descendants("decimal")
.Select(x => Parse.Decimal(x.Value))
.ToList();
var mchQuantity = XDocument.Load(mchQuantityFile).Descendants("decimal")
.Select(x => Parse.Decimal(x.Value))
.ToList();
var jwbQuantity = XDocument.Load(jwbQuantityFile).Descendants("decimal")
.Select(x => Parse.Decimal(x.Value))
.ToList();
Assert.AreEqual(fills.Count + 1, equity.Count);
// we're going to process fills and very our equity after each fill
var subscriptions = new SubscriptionManager(TimeKeeper);
var dataManager = new DataManagerStub(TimeKeeper);
subscriptions.SetDataManager(dataManager);
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.SetCash(equity[0]);
portfolio.CashBook.Add("MCH", mchQuantity[0], 0);
portfolio.CashBook.Add("JWB", jwbQuantity[0], 0);
var jwbCash = portfolio.CashBook["JWB"];
var mchCash = portfolio.CashBook["MCH"];
var usdCash = portfolio.CashBook[Currencies.USD];
var mchJwbSecurity = new QuantConnect.Securities.Forex.Forex(
SecurityExchangeHours,
jwbCash,
mchCash,
subscriptions.Add(MCHJWB, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork),
SymbolProperties.GetDefault(jwbCash.Symbol),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
);
mchJwbSecurity.SetLeverage(10m);
var mchUsdSecurity = new QuantConnect.Securities.Forex.Forex(
SecurityExchangeHours,
usdCash,
mchCash,
subscriptions.Add(MCHUSD, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork),
SymbolProperties.GetDefault(usdCash.Symbol),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
);
mchUsdSecurity.SetLeverage(10m);
var usdJwbSecurity = new QuantConnect.Securities.Forex.Forex(
SecurityExchangeHours,
mchCash,
usdCash,
subscriptions.Add(USDJWB, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork),
SymbolProperties.GetDefault(mchCash.Symbol),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
);
usdJwbSecurity.SetLeverage(10m);
// no fee model
mchJwbSecurity.FeeModel = new ConstantFeeModel(0);
mchUsdSecurity.FeeModel = new ConstantFeeModel(0);
usdJwbSecurity.FeeModel = new ConstantFeeModel(0);
securities.Add(mchJwbSecurity);
securities.Add(usdJwbSecurity);
securities.Add(mchUsdSecurity);
var algorithm = new QCAlgorithm();
algorithm.Securities = securities;
var securityService = new SecurityService(portfolio.CashBook, MarketHoursDatabase.FromDataFolder(), SymbolPropertiesDatabase.FromDataFolder(), dataManager.Algorithm, RegisteredSecurityDataTypesProvider.Null, new SecurityCacheProvider(portfolio), algorithm: algorithm);
portfolio.CashBook.EnsureCurrencyDataFeeds(securities, subscriptions, DefaultBrokerageModel.DefaultMarketMap, SecurityChanges.None, securityService);
for (int i = 0; i < fills.Count; i++)
{
// before processing the fill we must deduct the cost
var fill = fills[i];
var time = DateTime.Today.AddDays(i);
// the value of 'MCJWB' increments for each fill, the original test algo did this monthly
// the time doesn't really matter though
decimal mchJwb = i + 1;
decimal mchUsd = (i + 1) / (i + 2m);
decimal usdJwb = i + 2;
Assert.AreEqual((double)mchJwb, (double)(mchUsd * usdJwb), 1e-10);
//Console.WriteLine("Step: " + i + " -- MCHJWB: " + mchJwb);
var updateData = new Dictionary<Security, BaseData>
{
{mchJwbSecurity, new IndicatorDataPoint(MCHJWB, time, mchJwb)},
{mchUsdSecurity, new IndicatorDataPoint(MCHUSD, time, mchUsd)},
{usdJwbSecurity, new IndicatorDataPoint(JWBUSD, time, usdJwb)}
};
foreach (var kvp in updateData)
{
kvp.Key.SetMarketPrice(kvp.Value);
}
jwbCash.Update();
usdCash.Update();
mchCash.Update();
portfolio.ProcessFills(new List<OrderEvent> { fill });
//Console.WriteLine("-----------------------");
//Console.WriteLine(fill);
//Console.WriteLine("Post step: " + i);
//foreach (var cash in portfolio.CashBook)
//{
// Console.WriteLine(cash.Value);
//}
//Console.WriteLine("CashValue: " + portfolio.CashBook.TotalValueInAccountCurrency);
Console.WriteLine(i + 1 + " " + portfolio.TotalPortfolioValue.ToStringInvariant("C"));
//Assert.AreEqual((double) equity[i + 1], (double)portfolio.TotalPortfolioValue, 2e-2);
Assert.AreEqual((double)mchQuantity[i + 1], (double)portfolio.CashBook["MCH"].Amount);
Assert.AreEqual((double)jwbQuantity[i + 1], (double)portfolio.CashBook["JWB"].Amount);
//Console.WriteLine();
//Console.WriteLine();
}
}
[Test]
public void ComputeMarginProperlyAsSecurityPriceFluctuates()
{
const decimal leverage = 1m;
const int quantity = (int)(1000 * leverage);
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var orderProcessor = new OrderProcessor();
transactions.SetOrderProcessor(orderProcessor);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.CashBook[Currencies.USD].SetAmount(quantity);
var config = CreateTradeBarDataConfig(SecurityType.Equity, Symbols.AAPL);
securities.Add(
new Security(
SecurityExchangeHours,
config,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
var security = securities[Symbols.AAPL];
security.SetLeverage(leverage);
var time = DateTime.Now;
const decimal buyPrice = 1m;
security.SetMarketPrice(new TradeBar(time, Symbols.AAPL, buyPrice, buyPrice, buyPrice, buyPrice, 1));
var order = new MarketOrder(Symbols.AAPL, quantity, time) { Price = buyPrice };
var fill = new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero) { FillPrice = buyPrice, FillQuantity = quantity, Status = OrderStatus.Filled };
orderProcessor.AddOrder(order);
var request = new SubmitOrderRequest(OrderType.Market, security.Type, security.Symbol, order.Quantity, 0, 0, order.Time, null);
request.SetOrderId(0);
orderProcessor.AddTicket(new OrderTicket(null, request));
Assert.AreEqual(portfolio.CashBook[Currencies.USD].Amount, fill.FillPrice * fill.FillQuantity);
portfolio.ProcessFills(new List<OrderEvent> { fill });
Assert.AreEqual(0, portfolio.MarginRemaining);
Assert.AreEqual(quantity, portfolio.TotalMarginUsed);
Assert.AreEqual(quantity, portfolio.TotalPortfolioValue);
// we shouldn't be able to place a trader
var newOrder = new MarketOrder(Symbols.AAPL, 1, time.AddSeconds(1)) { Price = buyPrice };
var hasSufficientBuyingPower = security.BuyingPowerModel.HasSufficientBuyingPowerForOrder(portfolio, security, newOrder).IsSufficient;
Assert.IsFalse(hasSufficientBuyingPower);
// now the stock doubles, leverage is 1 we shouldn't have more margin remaining
time = time.AddDays(1);
const decimal highPrice = buyPrice * 2;
security.SetMarketPrice(new TradeBar(time, Symbols.AAPL, highPrice, highPrice, highPrice, highPrice, 1));
portfolio.InvalidateTotalPortfolioValue();
Assert.AreEqual(0, portfolio.MarginRemaining);
Assert.AreEqual(quantity * 2, portfolio.TotalMarginUsed);
Assert.AreEqual(quantity * 2, portfolio.TotalPortfolioValue);
// we shouldn't be able to place a trader
var anotherOrder = new MarketOrder(Symbols.AAPL, 1, time.AddSeconds(1)) { Price = highPrice };
hasSufficientBuyingPower = security.BuyingPowerModel.HasSufficientBuyingPowerForOrder(portfolio, security, anotherOrder).IsSufficient;
Assert.IsFalse(hasSufficientBuyingPower);
// now the stock plummets, leverage is 1 we shouldn't have margin remaining
time = time.AddDays(1);
const decimal lowPrice = buyPrice / 2;
security.SetMarketPrice(new TradeBar(time, Symbols.AAPL, lowPrice, lowPrice, lowPrice, lowPrice, 1));
portfolio.InvalidateTotalPortfolioValue();
Assert.AreEqual(0, portfolio.MarginRemaining);
Assert.AreEqual(quantity / 2m, portfolio.TotalMarginUsed);
Assert.AreEqual(quantity / 2m, portfolio.TotalPortfolioValue);
// this would not cause a margin call due to leverage = 1
bool issueMarginCallWarning;
var marginCallOrders = portfolio.MarginCallModel.GetMarginCallOrders(out issueMarginCallWarning);
Assert.IsFalse(issueMarginCallWarning);
Assert.AreEqual(0, marginCallOrders.Count);
}
[Test]
public void MarginWarningLeverage2()
{
var freeCash = 101;
const decimal leverage = 2m;
const int quantity = (int)(1000 * leverage);
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var orderProcessor = new OrderProcessor();
transactions.SetOrderProcessor(orderProcessor);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.CashBook[Currencies.USD].SetAmount(quantity / leverage + freeCash);
var config = CreateTradeBarDataConfig(SecurityType.Equity, Symbols.AAPL);
securities.Add(
new Security(
SecurityExchangeHours,
config,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
var security = securities[Symbols.AAPL];
security.SetLeverage(leverage);
var time = DateTime.Now;
const decimal buyPrice = 1m;
security.SetMarketPrice(new TradeBar(time, Symbols.AAPL, buyPrice, buyPrice, buyPrice, buyPrice, 1));
var order = new MarketOrder(Symbols.AAPL, quantity, time) { Price = buyPrice };
var fill = new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero) { FillPrice = buyPrice, FillQuantity = quantity, Status = OrderStatus.Filled };
orderProcessor.AddOrder(order);
var request = new SubmitOrderRequest(OrderType.Market, security.Type, security.Symbol, order.Quantity, 0, 0, order.Time, null);
request.SetOrderId(0);
orderProcessor.AddTicket(new OrderTicket(null, request));
portfolio.ProcessFills(new List<OrderEvent> { fill });
Assert.AreEqual(0 + freeCash, portfolio.MarginRemaining);
Assert.AreEqual(quantity / leverage, portfolio.TotalMarginUsed);
Assert.AreEqual(quantity / leverage + freeCash, portfolio.TotalPortfolioValue);
// now the stock loses 10%
time = time.AddDays(1);
const decimal lowPrice = buyPrice * 0.9m;
security.SetMarketPrice(new TradeBar(time, Symbols.AAPL, lowPrice, lowPrice, lowPrice, lowPrice, 1));
portfolio.InvalidateTotalPortfolioValue();
Assert.AreEqual(1, portfolio.MarginRemaining);
Assert.AreEqual((quantity * 0.9m) / leverage, portfolio.TotalMarginUsed);
Assert.AreEqual(901, portfolio.TotalPortfolioValue);
// this will cause a margin call warning, we still have $1 of margin available
bool issueMarginCallWarning;
var marginCallOrders = portfolio.MarginCallModel.GetMarginCallOrders(out issueMarginCallWarning);
Assert.IsTrue(issueMarginCallWarning);
Assert.AreEqual(0, marginCallOrders.Count);
}
[Test]
public void ComputeMarginProperlyAsSecurityPriceFluctuates_Leverage2()
{
const decimal leverage = 2m;
const int quantity = (int)(1000 * leverage);
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var orderProcessor = new OrderProcessor();
transactions.SetOrderProcessor(orderProcessor);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.CashBook[Currencies.USD].SetAmount(quantity / leverage);
var config = CreateTradeBarDataConfig(SecurityType.Equity, Symbols.AAPL);
securities.Add(
new Security(
SecurityExchangeHours,
config,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
var security = securities[Symbols.AAPL];
security.SetLeverage(leverage);
var time = DateTime.Now;
const decimal buyPrice = 1m;
security.SetMarketPrice(new TradeBar(time, Symbols.AAPL, buyPrice, buyPrice, buyPrice, buyPrice, 1));
var order = new MarketOrder(Symbols.AAPL, quantity, time) { Price = buyPrice };
var fill = new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero) { FillPrice = buyPrice, FillQuantity = quantity, Status = OrderStatus.Filled };
orderProcessor.AddOrder(order);
var request = new SubmitOrderRequest(OrderType.Market, security.Type, security.Symbol, order.Quantity, 0, 0, order.Time, null);
request.SetOrderId(0);
orderProcessor.AddTicket(new OrderTicket(null, request));
portfolio.ProcessFills(new List<OrderEvent> { fill });
Assert.AreEqual(0, portfolio.MarginRemaining);
Assert.AreEqual(quantity / leverage, portfolio.TotalMarginUsed);
Assert.AreEqual(quantity / leverage, portfolio.TotalPortfolioValue);
// we shouldn't be able to place a trader
var newOrder = new MarketOrder(Symbols.AAPL, 1, time.AddSeconds(1)) { Price = buyPrice };
var hasSufficientBuyingPower = security.BuyingPowerModel.HasSufficientBuyingPowerForOrder(portfolio, security, newOrder).IsSufficient;
Assert.IsFalse(hasSufficientBuyingPower);
// now the stock doubles
time = time.AddDays(1);
const decimal highPrice = buyPrice * 2;
security.SetMarketPrice(new TradeBar(time, Symbols.AAPL, highPrice, highPrice, highPrice, highPrice, 1));
portfolio.InvalidateTotalPortfolioValue();
// we have free margin now
Assert.AreEqual(quantity / leverage, portfolio.MarginRemaining);
// we are using a bit more margin too
Assert.AreEqual(quantity, portfolio.TotalMarginUsed);
// duplication increases our TPV by 'quantity'
Assert.AreEqual(quantity * 1.5, portfolio.TotalPortfolioValue);
// we should be able to place a trader
var anotherOrder = new MarketOrder(Symbols.AAPL, 1, time.AddSeconds(1)) { Price = highPrice };
hasSufficientBuyingPower = security.BuyingPowerModel.HasSufficientBuyingPowerForOrder(portfolio, security, anotherOrder).IsSufficient;
Assert.IsTrue(hasSufficientBuyingPower);
// now the stock plummets
time = time.AddDays(1);
const decimal lowPrice = buyPrice / 2;
security.SetMarketPrice(new TradeBar(time, Symbols.AAPL, lowPrice, lowPrice, lowPrice, lowPrice, 1));
portfolio.InvalidateTotalPortfolioValue();
Assert.AreEqual(-quantity / (leverage * 2), portfolio.MarginRemaining);
Assert.AreEqual(quantity / (leverage * 2), portfolio.TotalMarginUsed);
Assert.AreEqual(0, portfolio.TotalPortfolioValue);
// this will cause a margin call
bool issueMarginCallWarning;
var marginCallOrders = portfolio.MarginCallModel.GetMarginCallOrders(out issueMarginCallWarning);
Assert.IsTrue(issueMarginCallWarning);
Assert.AreNotEqual(0, marginCallOrders.Count);
Assert.AreEqual(-security.Holdings.Quantity, marginCallOrders[0].Quantity);
Assert.GreaterOrEqual(-portfolio.MarginRemaining, security.Price * marginCallOrders[0].Quantity);
}
[TestCase(OrderDirection.Buy)]
[TestCase(OrderDirection.Sell)]
public void InvertPositionLeverage2(OrderDirection direction)
{
const decimal leverage = 2m;
var invertedDirectionFactor = direction == OrderDirection.Buy ? -1 : 1;
var directionFactor = direction == OrderDirection.Buy ? 1 : -1;
var quantity = (int)(1000 * leverage * directionFactor);
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var orderProcessor = new OrderProcessor();
transactions.SetOrderProcessor(orderProcessor);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.CashBook[Currencies.USD].SetAmount(1000);
var config = CreateTradeBarDataConfig(SecurityType.Equity, Symbols.AAPL);
securities.Add(
new Security(
SecurityExchangeHours,
config,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
var security = securities[Symbols.AAPL];
security.FeeModel = new ConstantFeeModel(0);
security.SetLeverage(leverage);
var time = DateTime.Now;
const decimal buyPrice = 1m;
security.SetMarketPrice(new TradeBar(time, Symbols.AAPL, buyPrice, buyPrice, buyPrice, buyPrice, 1));
var order = new MarketOrder(Symbols.AAPL, quantity, time) { Price = buyPrice };
var fill = new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero) { FillPrice = buyPrice, FillQuantity = quantity, Status = OrderStatus.Filled };
orderProcessor.AddOrder(order);
var request = new SubmitOrderRequest(OrderType.Market, security.Type, security.Symbol, order.Quantity, 0, 0, order.Time, null);
request.SetOrderId(0);
orderProcessor.AddTicket(new OrderTicket(null, request));
portfolio.ProcessFills(new List<OrderEvent> { fill });
Assert.AreEqual(0, portfolio.MarginRemaining);
Assert.AreEqual(Math.Abs(quantity / leverage), portfolio.TotalMarginUsed);
Assert.AreEqual(Math.Abs(quantity / leverage), portfolio.TotalPortfolioValue);
var anotherOrder = new MarketOrder(Symbols.AAPL, 2 * Math.Abs(quantity) * invertedDirectionFactor, time.AddSeconds(1)) { Price = 1 };
var hasSufficientBuyingPower = security.BuyingPowerModel.HasSufficientBuyingPowerForOrder(portfolio, security, anotherOrder).IsSufficient;
Assert.IsTrue(hasSufficientBuyingPower);
}
[Test]
public void MarginComputesProperlyWithMultipleSecurities()
{
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var orderProcessor = new OrderProcessor();
transactions.SetOrderProcessor(orderProcessor);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.CashBook[Currencies.USD].SetAmount(1000);
portfolio.CashBook.Add("EUR", 1000, 1.1m);
portfolio.CashBook.Add("GBP", -1000, 2.0m);
var eurCash = portfolio.CashBook["EUR"];
var gbpCash = portfolio.CashBook["GBP"];
var usdCash = portfolio.CashBook[Currencies.USD];
var time = DateTime.Now;
var config1 = CreateTradeBarDataConfig(SecurityType.Equity, Symbols.AAPL);
securities.Add(
new Security(
SecurityExchangeHours,
config1,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
securities[Symbols.AAPL].SetLeverage(2m);
securities[Symbols.AAPL].Holdings.SetHoldings(100, 100);
securities[Symbols.AAPL].SetMarketPrice(new TradeBar { Time = time, Value = 100 });
var config2 = CreateTradeBarDataConfig(SecurityType.Forex, Symbols.EURUSD);
securities.Add(
new QuantConnect.Securities.Forex.Forex(
SecurityExchangeHours,
usdCash,
eurCash,
config2,
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
securities[Symbols.EURUSD].SetLeverage(100m);
securities[Symbols.EURUSD].Holdings.SetHoldings(1.1m, 1000);
securities[Symbols.EURUSD].SetMarketPrice(new TradeBar { Time = time, Value = 1.1m });
var config3 = CreateTradeBarDataConfig(SecurityType.Forex, Symbols.EURGBP);
securities.Add(
new QuantConnect.Securities.Forex.Forex(
SecurityExchangeHours,
gbpCash,
eurCash,
config3,
SymbolProperties.GetDefault(gbpCash.Symbol),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
securities[Symbols.EURGBP].SetLeverage(100m);
securities[Symbols.EURGBP].Holdings.SetHoldings(1m, 1000);
securities[Symbols.EURGBP].SetMarketPrice(new TradeBar { Time = time, Value = 1m });
var acceptedOrder = new MarketOrder(Symbols.AAPL, 101, DateTime.Now) { Price = 100 };
orderProcessor.AddOrder(acceptedOrder);
var request = new SubmitOrderRequest(OrderType.Market, acceptedOrder.SecurityType, acceptedOrder.Symbol, acceptedOrder.Quantity, 0, 0, acceptedOrder.Time, null);
request.SetOrderId(0);
orderProcessor.AddTicket(new OrderTicket(null, request));
var security = securities[Symbols.AAPL];
var hasSufficientBuyingPower = security.BuyingPowerModel.HasSufficientBuyingPowerForOrder(portfolio, security, acceptedOrder).IsSufficient;
Assert.IsTrue(hasSufficientBuyingPower);
var rejectedOrder = new MarketOrder(Symbols.AAPL, 102, DateTime.Now) { Price = 100 };
hasSufficientBuyingPower = security.BuyingPowerModel.HasSufficientBuyingPowerForOrder(portfolio, security, rejectedOrder).IsSufficient;
Assert.IsFalse(hasSufficientBuyingPower);
}
[Test]
public void BuyingSellingFuturesDoesntAddToCash()
{
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.SetCash(0);
securities.Add(
Symbols.Fut_SPY_Feb19_2016,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Future, Symbols.Fut_SPY_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
var fillBuy = new OrderEvent(1, Symbols.Fut_SPY_Feb19_2016, DateTime.MinValue, OrderStatus.Filled, OrderDirection.Buy, 100, 100, OrderFee.Zero);
portfolio.ProcessFills(new List<OrderEvent> { fillBuy });
Assert.AreEqual(0, portfolio.Cash);
Assert.AreEqual(100, securities[Symbols.Fut_SPY_Feb19_2016].Holdings.Quantity);
var fillSell = new OrderEvent(2, Symbols.Fut_SPY_Feb19_2016, DateTime.MinValue, OrderStatus.Filled, OrderDirection.Sell, 100, -100, OrderFee.Zero);
portfolio.ProcessFills(new List<OrderEvent> { fillSell });
Assert.AreEqual(0, portfolio.Cash);
Assert.AreEqual(0, securities[Symbols.Fut_SPY_Feb19_2016].Holdings.Quantity);
}
[Test]
public void BuyingSellingFuturesAddsToCashOnClose()
{
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.SetCash(0);
securities.Add(
Symbols.Fut_SPY_Feb19_2016,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Future, Symbols.Fut_SPY_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
new SymbolProperties("", Currencies.USD, 50, 0.01m, 1, string.Empty),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
var fillBuy = new OrderEvent(1, Symbols.Fut_SPY_Feb19_2016, DateTime.MinValue, OrderStatus.Filled, OrderDirection.Buy, 100, 100, OrderFee.Zero);
portfolio.ProcessFills(new List<OrderEvent> { fillBuy });
Assert.AreEqual(0, portfolio.Cash);
Assert.AreEqual(100, securities[Symbols.Fut_SPY_Feb19_2016].Holdings.Quantity);
var fillSell = new OrderEvent(2, Symbols.Fut_SPY_Feb19_2016, DateTime.MinValue, OrderStatus.Filled, OrderDirection.Sell, 99, -100, OrderFee.Zero);
portfolio.ProcessFills(new List<OrderEvent> { fillSell });
Assert.AreEqual(-100 * 50, portfolio.Cash);
Assert.AreEqual(0, securities[Symbols.Fut_SPY_Feb19_2016].Holdings.Quantity);
}
[Test]
public void BuyingSellingFuturesAddsCorrectSales()
{
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.SetCash(0);
securities.Add(
Symbols.Fut_SPY_Feb19_2016,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Future, Symbols.Fut_SPY_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
new SymbolProperties("", Currencies.USD, 50, 0.01m, 1, string.Empty),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
var fillBuy = new OrderEvent(1, Symbols.Fut_SPY_Feb19_2016, DateTime.MinValue, OrderStatus.Filled, OrderDirection.Buy, 100, 100, OrderFee.Zero);
portfolio.ProcessFills(new List<OrderEvent> { fillBuy });
var security = securities[Symbols.Fut_SPY_Feb19_2016];
Assert.AreEqual(100 * 100 * security.SymbolProperties.ContractMultiplier, security.Holdings.TotalSaleVolume);
var fillSell = new OrderEvent(2, Symbols.Fut_SPY_Feb19_2016, DateTime.MinValue, OrderStatus.Filled, OrderDirection.Sell, 100, -100, OrderFee.Zero);
portfolio.ProcessFills(new List<OrderEvent> { fillSell });
Assert.AreEqual(2 * 100 * 100 * security.SymbolProperties.ContractMultiplier, security.Holdings.TotalSaleVolume);
}
[Test]
public void BuyingSellingCfdDoesntAddToCash()
{
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.SetCash(0);
portfolio.SetCash("EUR", 0, 1.10m);
securities.Add(
Symbols.DE30EUR,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Cfd, Symbols.DE30EUR),
portfolio.CashBook["EUR"],
SymbolProperties.GetDefault("EUR"),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
var fillBuy = new OrderEvent(1, Symbols.DE30EUR, DateTime.MinValue, OrderStatus.Filled, OrderDirection.Buy, 10000, 5, OrderFee.Zero);
portfolio.ProcessFills(new List<OrderEvent> { fillBuy });
Assert.AreEqual(0, portfolio.Cash);
Assert.AreEqual(5, securities[Symbols.DE30EUR].Holdings.Quantity);
var fillSell = new OrderEvent(2, Symbols.DE30EUR, DateTime.MinValue, OrderStatus.Filled, OrderDirection.Sell, 10000, -5, OrderFee.Zero);
portfolio.ProcessFills(new List<OrderEvent> { fillSell });
Assert.AreEqual(0, portfolio.Cash);
Assert.AreEqual(0, securities[Symbols.DE30EUR].Holdings.Quantity);
}
[Test]
public void BuyingSellingCfdAddsToCashOnClose()
{
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.SetCash(0);
portfolio.SetCash("EUR", 0, 1.10m);
securities.Add(
Symbols.DE30EUR,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Cfd, Symbols.DE30EUR),
portfolio.CashBook["EUR"],
SymbolProperties.GetDefault("EUR"),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
securities[Symbols.DE30EUR].SettlementModel = new AccountCurrencyImmediateSettlementModel();
var fillBuy = new OrderEvent(1, Symbols.DE30EUR, DateTime.MinValue, OrderStatus.Filled, OrderDirection.Buy, 10000, 5, OrderFee.Zero);
portfolio.ProcessFills(new List<OrderEvent> { fillBuy });
Assert.AreEqual(0, portfolio.CashBook["EUR"].Amount);
Assert.AreEqual(0, portfolio.CashBook["USD"].Amount);
Assert.AreEqual(0, portfolio.Cash);
Assert.AreEqual(5, securities[Symbols.DE30EUR].Holdings.Quantity);
var fillSell = new OrderEvent(2, Symbols.DE30EUR, DateTime.MinValue, OrderStatus.Filled, OrderDirection.Sell, 10100, -5, OrderFee.Zero);
portfolio.ProcessFills(new List<OrderEvent> { fillSell });
// PNL = (10100 - 10000) * 5 * 1.10 = 550 USD
Assert.AreEqual(0, portfolio.CashBook["EUR"].Amount);
Assert.AreEqual(550, portfolio.CashBook["USD"].Amount);
Assert.AreEqual(550, portfolio.Cash);
Assert.AreEqual(0, securities[Symbols.DE30EUR].Holdings.Quantity);
}
[Test]
public void BuyingSellingCfdAddsCorrectSales()
{
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.SetCash(0);
portfolio.SetCash("EUR", 0, 1.10m);
securities.Add(
Symbols.DE30EUR,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Cfd, Symbols.DE30EUR),
portfolio.CashBook["EUR"],
SymbolProperties.GetDefault("EUR"),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
var fillBuy = new OrderEvent(1, Symbols.DE30EUR, DateTime.MinValue, OrderStatus.Filled, OrderDirection.Buy, 10000, 5, OrderFee.Zero);
portfolio.ProcessFills(new List<OrderEvent> { fillBuy });
// 10000 price * 5 quantity * 1.10 exchange rate = 55000 USD
Assert.AreEqual(55000, securities[Symbols.DE30EUR].Holdings.TotalSaleVolume);
var fillSell = new OrderEvent(2, Symbols.DE30EUR, DateTime.MinValue, OrderStatus.Filled, OrderDirection.Sell, 10000, -5, OrderFee.Zero);
portfolio.ProcessFills(new List<OrderEvent> { fillSell });
// 2 * 10000 price * 5 quantity * 1.10 exchange rate = 110000 USD
Assert.AreEqual(110000, securities[Symbols.DE30EUR].Holdings.TotalSaleVolume);
}
[Test]
public void SellingShortFromZeroAddsToCash()
{
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.SetCash(0);
securities.Add(
Symbols.AAPL,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.AAPL),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
var fill = new OrderEvent(1, Symbols.AAPL, DateTime.MinValue, OrderStatus.Filled, OrderDirection.Sell, 100, -100, OrderFee.Zero);
portfolio.ProcessFills(new List<OrderEvent> { fill });
Assert.AreEqual(100 * 100, portfolio.Cash);
Assert.AreEqual(-100, securities[Symbols.AAPL].Holdings.Quantity);
}
[Test]
public void SellingShortFromLongAddsToCash()
{
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.SetCash(0);
securities.Add(
Symbols.AAPL,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.AAPL),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
securities[Symbols.AAPL].Holdings.SetHoldings(100, 100);
var fill = new OrderEvent(1, Symbols.AAPL, DateTime.MinValue, OrderStatus.Filled, OrderDirection.Sell, 100, -100, OrderFee.Zero);
portfolio.ProcessFills(new List<OrderEvent> { fill });
Assert.AreEqual(100 * 100, portfolio.Cash);
Assert.AreEqual(0, securities[Symbols.AAPL].Holdings.Quantity);
}
[Test]
public void SellingShortFromShortAddsToCash()
{
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.SetCash(0);
securities.Add(
Symbols.AAPL,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.AAPL),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
securities[Symbols.AAPL].Holdings.SetHoldings(100, -100);
var fill = new OrderEvent(1, Symbols.AAPL, DateTime.MinValue, OrderStatus.Filled, OrderDirection.Sell, 100, -100, OrderFee.Zero);
Assert.AreEqual(-100, securities[Symbols.AAPL].Holdings.Quantity);
portfolio.ProcessFills(new List<OrderEvent> { fill });
Assert.AreEqual(100 * 100, portfolio.Cash);
Assert.AreEqual(-200, securities[Symbols.AAPL].Holdings.Quantity);
}
[Test]
public void ForexFillUpdatesCashCorrectly()
{
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.SetCash(1000);
var eurCash = portfolio.CashBook.Add("EUR", 0, 1.1000m);
securities.Add(
Symbols.EURUSD,
new QuantConnect.Securities.Forex.Forex(
SecurityExchangeHours,
portfolio.CashBook[Currencies.USD],
eurCash,
CreateTradeBarDataConfig(SecurityType.Forex, Symbols.EURUSD),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
var security = securities[Symbols.EURUSD];
Assert.AreEqual(0, security.Holdings.Quantity);
Assert.AreEqual(1000, portfolio.Cash);
var orderFee = security.FeeModel.GetOrderFee(new OrderFeeParameters(
security, new MarketOrder(Symbols.EURUSD, 100, DateTime.MinValue)));
var fill = new OrderEvent(1, Symbols.EURUSD, DateTime.MinValue, OrderStatus.Filled, OrderDirection.Buy, 1.1000m, 100, orderFee);
portfolio.ProcessFills(new List<OrderEvent> { fill });
Assert.AreEqual(100, security.Holdings.Quantity);
Assert.AreEqual(998, portfolio.Cash);
Assert.AreEqual(100, portfolio.CashBook["EUR"].Amount);
Assert.AreEqual(888, portfolio.CashBook[Currencies.USD].Amount);
}
[Test]
public void CryptoFillUpdatesCashCorrectly()
{
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.SetCash(10000);
var btcCash = portfolio.CashBook.Add("BTC", 0, 4000.01m);
securities.Add(
Symbols.BTCUSD,
new QuantConnect.Securities.Crypto.Crypto(
SecurityExchangeHours,
portfolio.CashBook[Currencies.USD],
btcCash,
CreateTradeBarDataConfig(
SecurityType.Crypto,
Symbols.BTCUSD
),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
var security = securities[Symbols.BTCUSD];
Assert.AreEqual(0, security.Holdings.Quantity);
Assert.AreEqual(10000, portfolio.Cash);
var orderFee = security.FeeModel.GetOrderFee(new OrderFeeParameters(
security, new MarketOrder(Symbols.BTCUSD, 2, DateTime.MinValue)));
var fill = new OrderEvent(1, Symbols.BTCUSD, DateTime.MinValue, OrderStatus.Filled, OrderDirection.Buy, 4000.01m, 2, OrderFee.Zero);
portfolio.ProcessFills(new List<OrderEvent> { fill });
Assert.AreEqual(2, security.Holdings.Quantity);
Assert.AreEqual(10000, portfolio.Cash);
Assert.AreEqual(2, portfolio.CashBook["BTC"].Amount);
Assert.AreEqual(1999.98, portfolio.CashBook[Currencies.USD].Amount);
}
[Test]
public void EquitySellAppliesSettlementCorrectly()
{
var securityExchangeHours = SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours();
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.SetCash(1000);
securities.Add(
Symbols.AAPL,
new QuantConnect.Securities.Equity.Equity(
securityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.AAPL),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
var security = securities[Symbols.AAPL];
security.SettlementModel = new DelayedSettlementModel(3, TimeSpan.FromHours(8));
Assert.AreEqual(0, security.Holdings.Quantity);
Assert.AreEqual(1000, portfolio.Cash);
Assert.AreEqual(0, portfolio.UnsettledCash);
// Buy on Monday
var timeUtc = new DateTime(2015, 10, 26, 15, 30, 0);
var orderFee = security.FeeModel.GetOrderFee(new OrderFeeParameters(
security, new MarketOrder(Symbols.AAPL, 10, timeUtc)));
var fill = new OrderEvent(1, Symbols.AAPL, timeUtc, OrderStatus.Filled, OrderDirection.Buy, 100, 10, orderFee);
portfolio.ProcessFills(new List<OrderEvent> { fill });
Assert.AreEqual(10, security.Holdings.Quantity); Assert.AreEqual(-1, portfolio.Cash);
Assert.AreEqual(0, portfolio.UnsettledCash);
// Sell on Tuesday, cash unsettled
timeUtc = timeUtc.AddDays(1);
orderFee = security.FeeModel.GetOrderFee(new OrderFeeParameters(
security, new MarketOrder(Symbols.AAPL, 10, timeUtc)));
fill = new OrderEvent(2, Symbols.AAPL, timeUtc, OrderStatus.Filled, OrderDirection.Sell, 100, -10, orderFee);
portfolio.ProcessFills(new List<OrderEvent> { fill });
Assert.AreEqual(0, security.Holdings.Quantity);
Assert.AreEqual(-2, portfolio.Cash);
Assert.AreEqual(1000, portfolio.UnsettledCash);
// Thursday, still cash unsettled
timeUtc = timeUtc.AddDays(2);
security.SettlementModel.Scan(new ScanSettlementModelParameters(portfolio, security, timeUtc));
Assert.AreEqual(-2, portfolio.Cash);
Assert.AreEqual(1000, portfolio.UnsettledCash);
// Friday at open, cash settled
var marketOpen = securityExchangeHours.MarketHours[timeUtc.DayOfWeek].GetMarketOpen(TimeSpan.Zero, false);
Assert.IsTrue(marketOpen.HasValue);
timeUtc = timeUtc.AddDays(1).Date.Add(marketOpen.Value).ConvertToUtc(securityExchangeHours.TimeZone);
security.SettlementModel.Scan(new ScanSettlementModelParameters(portfolio, security, timeUtc));
Assert.AreEqual(998, portfolio.Cash);
Assert.AreEqual(0, portfolio.UnsettledCash);
}
[Test]
public void ComputeMarginProperlyLongSellZeroShort()
{
const decimal leverage = 2m;
const int amount = 1000;
const int quantity = (int)(amount * leverage);
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var orderProcessor = new OrderProcessor();
transactions.SetOrderProcessor(orderProcessor);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.CashBook[Currencies.USD].SetAmount(amount);
var config = CreateTradeBarDataConfig(SecurityType.Equity, Symbols.AAPL);
securities.Add(
new Security(
SecurityExchangeHours,
config,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
var security = securities[Symbols.AAPL];
security.SetLeverage(leverage);
var time = DateTime.Now;
const decimal buyPrice = 1m;
security.SetMarketPrice(new TradeBar(time, Symbols.AAPL, buyPrice, buyPrice, buyPrice, buyPrice, 1));
var order = new MarketOrder(Symbols.AAPL, quantity, time) { Price = buyPrice };
var fill = new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero)
{ FillPrice = buyPrice, FillQuantity = quantity, Status = OrderStatus.Filled };
orderProcessor.AddOrder(order);
var request = new SubmitOrderRequest(OrderType.Market, security.Type, security.Symbol, order.Quantity, 0, 0, order.Time, null);
request.SetOrderId(0);
orderProcessor.AddTicket(new OrderTicket(null, request));
portfolio.ProcessFills(new List<OrderEvent> { fill });
// we shouldn't be able to place a new buy order
var newOrder = new MarketOrder(Symbols.AAPL, 1, time.AddSeconds(1)) { Price = buyPrice };
var hasSufficientBuyingPower = security.BuyingPowerModel.HasSufficientBuyingPowerForOrder(portfolio, security, newOrder).IsSufficient;
Assert.IsFalse(hasSufficientBuyingPower);
// we should be able to place sell to zero
newOrder = new MarketOrder(Symbols.AAPL, -quantity, time.AddSeconds(1)) { Price = buyPrice };
hasSufficientBuyingPower = security.BuyingPowerModel.HasSufficientBuyingPowerForOrder(portfolio, security, newOrder).IsSufficient;
Assert.IsTrue(hasSufficientBuyingPower);
// now the stock plummets, so we should have negative margin remaining
time = time.AddDays(1);
const decimal lowPrice = buyPrice / 2;
security.SetMarketPrice(new TradeBar(time, Symbols.AAPL, lowPrice, lowPrice, lowPrice, lowPrice, 1));
portfolio.InvalidateTotalPortfolioValue();
// we still should be able to place sell to zero
newOrder = new MarketOrder(Symbols.AAPL, -quantity, time.AddSeconds(1)) { Price = lowPrice };
hasSufficientBuyingPower = security.BuyingPowerModel.HasSufficientBuyingPowerForOrder(portfolio, security, newOrder).IsSufficient;
Assert.IsTrue(hasSufficientBuyingPower);
// we shouldn't be able to place sell to short
newOrder = new MarketOrder(Symbols.AAPL, -quantity - 1, time.AddSeconds(1)) { Price = lowPrice };
hasSufficientBuyingPower = security.BuyingPowerModel.HasSufficientBuyingPowerForOrder(portfolio, security, newOrder).IsSufficient;
Assert.IsFalse(hasSufficientBuyingPower);
}
[Test]
public void ComputeMarginProperlyShortCoverZeroLong()
{
const decimal leverage = 2m;
const int amount = 1000;
const int quantity = (int)(amount * leverage);
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var orderProcessor = new OrderProcessor();
transactions.SetOrderProcessor(orderProcessor);
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
portfolio.CashBook[Currencies.USD].SetAmount(amount);
var config = CreateTradeBarDataConfig(SecurityType.Equity, Symbols.AAPL);
securities.Add(
new Security(
SecurityExchangeHours,
config,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
var security = securities[Symbols.AAPL];
security.SetLeverage(leverage);
var time = DateTime.Now;
const decimal sellPrice = 1m;
security.SetMarketPrice(new TradeBar(time, Symbols.AAPL, sellPrice, sellPrice, sellPrice, sellPrice, 1));
var order = new MarketOrder(Symbols.AAPL, -quantity, time) { Price = sellPrice };
var fill = new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero)
{ FillPrice = sellPrice, FillQuantity = -quantity, Status = OrderStatus.Filled };
orderProcessor.AddOrder(order);
var request = new SubmitOrderRequest(OrderType.Market, security.Type, security.Symbol, order.Quantity, 0, 0, order.Time, null);
request.SetOrderId(0);
orderProcessor.AddTicket(new OrderTicket(null, request));
portfolio.ProcessFills(new List<OrderEvent> { fill });
// we shouldn't be able to place a new short order
var newOrder = new MarketOrder(Symbols.AAPL, -1, time.AddSeconds(1)) { Price = sellPrice };
var hasSufficientBuyingPower = security.BuyingPowerModel.HasSufficientBuyingPowerForOrder(portfolio, security, newOrder).IsSufficient;
Assert.IsFalse(hasSufficientBuyingPower);
// we should be able to place cover to zero
newOrder = new MarketOrder(Symbols.AAPL, quantity, time.AddSeconds(1)) { Price = sellPrice };
hasSufficientBuyingPower = security.BuyingPowerModel.HasSufficientBuyingPowerForOrder(portfolio, security, newOrder).IsSufficient;
Assert.IsTrue(hasSufficientBuyingPower);
// now the stock doubles, so we should have negative margin remaining
time = time.AddDays(1);
const decimal highPrice = sellPrice * 2;
security.SetMarketPrice(new TradeBar(time, Symbols.AAPL, highPrice, highPrice, highPrice, highPrice, 1));
portfolio.InvalidateTotalPortfolioValue();
// we still shouldn be able to place cover to zero
newOrder = new MarketOrder(Symbols.AAPL, quantity, time.AddSeconds(1)) { Price = highPrice };
hasSufficientBuyingPower = security.BuyingPowerModel.HasSufficientBuyingPowerForOrder(portfolio, security, newOrder).IsSufficient;
Assert.IsTrue(hasSufficientBuyingPower);
// we shouldn't be able to place cover to long
newOrder = new MarketOrder(Symbols.AAPL, quantity + 1, time.AddSeconds(1)) { Price = highPrice };
hasSufficientBuyingPower = security.BuyingPowerModel.HasSufficientBuyingPowerForOrder(portfolio, security, newOrder).IsSufficient;
Assert.IsFalse(hasSufficientBuyingPower);
}
[Test]
public void FullExerciseCallAddsUnderlyingPositionReducesCash()
{
// Adding cash: strike price times number of shares
_portfolio.SetCash(192 * 100);
_securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
_securities.Add(
Symbols.SPY_C_192_Feb19_2016,
new Option(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY_C_192_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
GetOptionSymbolProperties(Symbols.SPY_C_192_Feb19_2016),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
_securities[Symbols.SPY_C_192_Feb19_2016].Holdings.SetHoldings(1, 1);
_securities[Symbols.SPY].SetMarketPrice(new Tick { Value = 200 });
var holdings = _securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity;
_transactions.AddOrder(new SubmitOrderRequest(OrderType.OptionExercise, SecurityType.Option, Symbols.SPY_C_192_Feb19_2016, -holdings, 0, 0, _securities.UtcTime, ""));
var option = (Option)_securities[Symbols.SPY_C_192_Feb19_2016];
var order = (OptionExerciseOrder)_transactions.GetOrders(x => true).First();
option.Underlying = _securities[Symbols.SPY];
var fills = option.OptionExerciseModel.OptionExercise(option, order).ToList();
Assert.AreEqual(2, fills.Count);
Assert.IsFalse(fills[0].IsAssignment);
Assert.AreEqual(order.Quantity, fills[0].FillQuantity);
StringAssert.Contains("Automatic Exercise", fills[0].Message);
Assert.IsTrue(fills[0].IsInTheMoney);
Assert.AreEqual("Option Exercise", fills[1].Message);
foreach (var fill in fills)
{
fill.Ticket = order.ToOrderTicket(_transactions);
_portfolio.ProcessFills(new List<OrderEvent> { fill });
}
// now we have long position in SPY with average price equal to strike
var newUnderlyingHoldings = _securities[Symbols.SPY].Holdings;
Assert.AreEqual(0, _portfolio.Cash);
Assert.AreEqual(100, newUnderlyingHoldings.Quantity);
Assert.AreEqual(192.0, newUnderlyingHoldings.AveragePrice);
// and long call option position has disappeared
Assert.AreEqual(0, _securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity);
}
[Test]
public void ExerciseOTMCallDoesntChangeAnything()
{
_portfolio.SetCash(0);
_securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
_securities.Add(
Symbols.SPY_C_192_Feb19_2016,
new Option(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY_C_192_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
GetOptionSymbolProperties(Symbols.SPY_C_192_Feb19_2016),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
_securities[Symbols.SPY_C_192_Feb19_2016].Holdings.SetHoldings(1, 100);
_securities[Symbols.SPY].SetMarketPrice(new Tick { Value = 20 });
var holdings = _securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity;
_transactions.AddOrder(new SubmitOrderRequest(OrderType.OptionExercise, SecurityType.Option, Symbols.SPY_C_192_Feb19_2016, -holdings, 0, 0, _securities.UtcTime, ""));
var option = (Option)_securities[Symbols.SPY_C_192_Feb19_2016];
var order = (OptionExerciseOrder)_transactions.GetOrders(x => true).First();
option.Underlying = _securities[Symbols.SPY];
var fills = option.OptionExerciseModel.OptionExercise(option, order).ToList();
Assert.AreEqual(1, fills.Count);
StringAssert.Contains("OTM", fills[0].Message);
Assert.IsFalse(fills[0].IsInTheMoney);
Assert.AreEqual(0, fills[0].FillPrice);
Assert.AreEqual(order.Quantity, fills[0].FillQuantity);
foreach (var fill in fills)
{
fill.Ticket = order.ToOrderTicket(_transactions);
_portfolio.ProcessFills(new List<OrderEvent> { fill });
}
// now we have long position in SPY with average price equal to strike
var newUnderlyingHoldings = _securities[Symbols.SPY].Holdings;
Assert.AreEqual(0, _portfolio.Cash);
Assert.AreEqual(0, newUnderlyingHoldings.Quantity);
Assert.AreEqual(0, newUnderlyingHoldings.AveragePrice);
// and long call option position has disappeared
Assert.AreEqual(0, _securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity);
}
[Test]
public void CashSettledExerciseOTMPutDoesntChangeAnything()
{
_portfolio.SetCash(0);
_securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
_securities.Add(
Symbols.SPY_P_192_Feb19_2016,
new Option(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY_P_192_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
GetOptionSymbolProperties(Symbols.SPY_P_192_Feb19_2016),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
_securities[Symbols.SPY_P_192_Feb19_2016].Holdings.SetHoldings(1, 100);
_securities[Symbols.SPY].SetMarketPrice(new Tick { Value = 2000 });
var holdings = _securities[Symbols.SPY_P_192_Feb19_2016].Holdings.Quantity;
_transactions.AddOrder(new SubmitOrderRequest(OrderType.OptionExercise, SecurityType.Option, Symbols.SPY_P_192_Feb19_2016, -holdings, 0, 0, _securities.UtcTime, ""));
var option = (Option)_securities[Symbols.SPY_P_192_Feb19_2016];
var order = (OptionExerciseOrder)_transactions.GetOrders(x => true).First();
option.Underlying = _securities[Symbols.SPY];
option.ExerciseSettlement = SettlementType.Cash;
var fills = option.OptionExerciseModel.OptionExercise(option, order).ToList();
Assert.AreEqual(1, fills.Count);
StringAssert.Contains("OTM", fills[0].Message);
Assert.IsFalse(fills[0].IsInTheMoney);
Assert.AreEqual(0, fills[0].FillPrice);
foreach (var fill in fills)
{
fill.Ticket = order.ToOrderTicket(_transactions);
_portfolio.ProcessFills(new List<OrderEvent> { fill });
}
// now we have long position in SPY with average price equal to strike
var newUnderlyingHoldings = _securities[Symbols.SPY].Holdings;
Assert.AreEqual(0, _portfolio.Cash);
Assert.AreEqual(0, newUnderlyingHoldings.Quantity);
Assert.AreEqual(0, newUnderlyingHoldings.AveragePrice);
// and long call option position has disappeared
Assert.AreEqual(0, _securities[Symbols.SPY_P_192_Feb19_2016].Holdings.Quantity);
}
[Test]
public void FullExercisePutAddsUnderlyingPositionAddsCash()
{
_portfolio.SetCash(0);
_securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
_securities.Add(
Symbols.SPY_P_192_Feb19_2016,
new Option(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY_P_192_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
GetOptionSymbolProperties(Symbols.SPY_P_192_Feb19_2016),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
_securities[Symbols.SPY_P_192_Feb19_2016].Holdings.SetHoldings(1, 1);
var holdings = _securities[Symbols.SPY_P_192_Feb19_2016].Holdings.Quantity;
_transactions.AddOrder(new SubmitOrderRequest(OrderType.OptionExercise, SecurityType.Option, Symbols.SPY_P_192_Feb19_2016, -holdings, 0, 0, _securities.UtcTime, ""));
var option = (Option)_securities[Symbols.SPY_P_192_Feb19_2016];
var order = (OptionExerciseOrder)_transactions.GetOrders(x => true).First();
option.Underlying = _securities[Symbols.SPY];
var fills = option.OptionExerciseModel.OptionExercise(option, order).ToList();
Assert.AreEqual(2, fills.Count);
Assert.IsFalse(fills[0].IsAssignment);
Assert.AreEqual(order.Quantity, fills[0].FillQuantity);
StringAssert.Contains("Automatic Exercise", fills[0].Message);
Assert.IsTrue(fills[0].IsInTheMoney);
Assert.AreEqual("Option Exercise", fills[1].Message);
foreach (var fill in fills)
{
fill.Ticket = order.ToOrderTicket(_transactions);
_portfolio.ProcessFills(new List<OrderEvent> { fill });
}
// now we have short position in SPY with average price equal to strike
// and cash amount equal to strike price times number of shares
var newUnderlyingHoldings = _securities[Symbols.SPY].Holdings;
Assert.AreEqual(192 * 100, _portfolio.Cash);
Assert.AreEqual(-100, newUnderlyingHoldings.Quantity);
Assert.AreEqual(192.0, newUnderlyingHoldings.AveragePrice);
// and long put option position has disappeared
Assert.AreEqual(0, _securities[Symbols.SPY_P_192_Feb19_2016].Holdings.Quantity);
}
[Test]
public void PartialExerciseCallAddsUnderlyingPositionReducesCash()
{
// Adding cash: strike price times number of shares
_portfolio.SetCash(192 * 100);
_securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
_securities.Add(
Symbols.SPY_C_192_Feb19_2016,
new Option(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY_C_192_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
GetOptionSymbolProperties(Symbols.SPY_C_192_Feb19_2016),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
_securities[Symbols.SPY_C_192_Feb19_2016].Holdings.SetHoldings(1, 2);
_securities[Symbols.SPY].SetMarketPrice(new Tick { Value = 200 });
var holdings = _securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity;
_transactions.AddOrder(new SubmitOrderRequest(OrderType.OptionExercise, SecurityType.Option, Symbols.SPY_C_192_Feb19_2016, -holdings / 2, 0, 0, _securities.UtcTime, ""));
var option = (Option)_securities[Symbols.SPY_C_192_Feb19_2016];
var order = (OptionExerciseOrder)_transactions.GetOrders(x => true).First();
option.Underlying = _securities[Symbols.SPY];
var fills = option.OptionExerciseModel.OptionExercise(option, order).ToList();
Assert.AreEqual(2, fills.Count);
Assert.IsFalse(fills[0].IsAssignment);
Assert.AreEqual(order.Quantity, fills[0].FillQuantity);
StringAssert.Contains("Automatic Exercise", fills[0].Message);
Assert.IsTrue(fills[0].IsInTheMoney);
Assert.AreEqual("Option Exercise", fills[1].Message);
foreach (var fill in fills)
{
fill.Ticket = order.ToOrderTicket(_transactions);
_portfolio.ProcessFills(new List<OrderEvent> { fill });
}
// now we have long position in SPY with average price equal to strike
var newUnderlyingHoldings = _securities[Symbols.SPY].Holdings;
Assert.AreEqual(0, _portfolio.Cash);
Assert.AreEqual(100, newUnderlyingHoldings.Quantity);
Assert.AreEqual(192.0, newUnderlyingHoldings.AveragePrice);
// and call option position still has some value
Assert.AreEqual(1, _securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity);
}
[Test]
public void InternalCallAssignmentAddsUnderlyingPositionAddsCash()
{
_portfolio.SetCash(0);
_securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
_securities.Add(
Symbols.SPY_C_192_Feb19_2016,
new Option(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY_C_192_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
GetOptionSymbolProperties(Symbols.SPY_C_192_Feb19_2016),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
_securities[Symbols.SPY_C_192_Feb19_2016].Holdings.SetHoldings(1, -1);
_securities[Symbols.SPY].SetMarketPrice(new Tick { Value = 200 });
var holdings = _securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity;
_transactions.AddOrder(new SubmitOrderRequest(OrderType.OptionExercise, SecurityType.Option, Symbols.SPY_C_192_Feb19_2016, -holdings, 0, 0, _securities.UtcTime, ""));
var option = (Option)_securities[Symbols.SPY_C_192_Feb19_2016];
var order = (OptionExerciseOrder)_transactions.GetOrders(x => true).First();
option.Underlying = _securities[Symbols.SPY];
var fills = option.OptionExerciseModel.OptionExercise(option, order).ToList();
Assert.AreEqual(2, fills.Count);
Assert.IsTrue(fills[0].IsAssignment);
Assert.AreEqual(order.Quantity, fills[0].FillQuantity);
StringAssert.Contains("Assigned", fills[0].Message);
Assert.AreEqual("Option Assignment", fills[1].Message);
// we are simulating assignment by calling a method for this
var portfolioModel = (OptionPortfolioModel)option.PortfolioModel;
foreach (var fill in fills)
{
fill.Ticket = order.ToOrderTicket(_transactions);
portfolioModel.ProcessFill(_portfolio, option, fill);
}
// we just got assigned!
// now we have short position in SPY with average price equal to strike
// and cash amount equal to strike price times number of shares
var newUnderlyingHoldings = _securities[Symbols.SPY].Holdings;
Assert.AreEqual(192 * 100, _portfolio.Cash);
Assert.AreEqual(-100, newUnderlyingHoldings.Quantity);
Assert.AreEqual(192.0, newUnderlyingHoldings.AveragePrice);
// and short call option position has disappeared
Assert.AreEqual(0, _securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity);
}
[Test]
public void InternalPutAssignmentAddsUnderlyingPositionReducesCash()
{
// Adding cash: strike price times number of shares
_portfolio.SetCash(192 * 100);
_securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
_securities.Add(
Symbols.SPY_P_192_Feb19_2016,
new Option(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY_P_192_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
GetOptionSymbolProperties(Symbols.SPY_P_192_Feb19_2016),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
_securities[Symbols.SPY_P_192_Feb19_2016].Holdings.SetHoldings(1, -1);
var holdings = _securities[Symbols.SPY_P_192_Feb19_2016].Holdings.Quantity;
_transactions.AddOrder(new SubmitOrderRequest(OrderType.OptionExercise, SecurityType.Option, Symbols.SPY_P_192_Feb19_2016, -holdings, 0, 0, _securities.UtcTime, ""));
var option = (Option)_securities[Symbols.SPY_P_192_Feb19_2016];
var order = (OptionExerciseOrder)_transactions.GetOrders(x => true).First();
option.Underlying = _securities[Symbols.SPY];
var fills = option.OptionExerciseModel.OptionExercise(option, order).ToList();
Assert.AreEqual(2, fills.Count);
Assert.IsTrue(fills[0].IsAssignment);
Assert.AreEqual(order.Quantity, fills[0].FillQuantity);
StringAssert.Contains("Assigned", fills[0].Message);
Assert.AreEqual("Option Assignment", fills[1].Message);
// we are simulating assignment by calling a method for this
var portfolioModel = (OptionPortfolioModel)option.PortfolioModel;
foreach (var fill in fills)
{
fill.Ticket = order.ToOrderTicket(_transactions);
portfolioModel.ProcessFill(_portfolio, option, fill);
}
// we just got assigned!
// now we have long position in SPY with average price equal to strike
var newUnderlyingHoldings = _securities[Symbols.SPY].Holdings;
Assert.AreEqual(0, _portfolio.Cash);
Assert.AreEqual(100, newUnderlyingHoldings.Quantity);
Assert.AreEqual(192.0, newUnderlyingHoldings.AveragePrice);
// and short put option position has disappeared
Assert.AreEqual(0, _securities[Symbols.SPY_P_192_Feb19_2016].Holdings.Quantity);
}
[Test]
public void InternalPartialPutAssignmentAddsUnderlyingPositionReduces()
{
// Adding cash: strike price times number of shares
_portfolio.SetCash(192 * 100);
_securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
_securities.Add(
Symbols.SPY_P_192_Feb19_2016,
new Option(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY_P_192_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
GetOptionSymbolProperties(Symbols.SPY_P_192_Feb19_2016),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
_securities[Symbols.SPY_P_192_Feb19_2016].Holdings.SetHoldings(1, -2);
var holdings = _securities[Symbols.SPY_P_192_Feb19_2016].Holdings.Quantity;
_transactions.AddOrder(new SubmitOrderRequest(OrderType.OptionExercise, SecurityType.Option, Symbols.SPY_P_192_Feb19_2016, -holdings / 2, 0, 0, _securities.UtcTime, ""));
var option = (Option)_securities[Symbols.SPY_P_192_Feb19_2016];
var order = (OptionExerciseOrder)_transactions.GetOrders(x => true).First();
option.Underlying = _securities[Symbols.SPY];
var fills = option.OptionExerciseModel.OptionExercise(option, order).ToList();
Assert.AreEqual(2, fills.Count);
Assert.IsTrue(fills[0].IsAssignment);
Assert.AreEqual(order.Quantity, fills[0].FillQuantity);
StringAssert.Contains("Assigned", fills[0].Message);
Assert.AreEqual("Option Assignment", fills[1].Message);
// we are simulating assignment by calling a method for this
var portfolioModel = (OptionPortfolioModel)option.PortfolioModel;
foreach (var fill in fills)
{
fill.Ticket = order.ToOrderTicket(_transactions);
portfolioModel.ProcessFill(_portfolio, option, fill);
}
// we just got assigned!
// now we have long position in SPY with average price equal to strike
var newUnderlyingHoldings = _securities[Symbols.SPY].Holdings;
Assert.AreEqual(0, _portfolio.Cash);
Assert.AreEqual(100, newUnderlyingHoldings.Quantity);
Assert.AreEqual(192.0, newUnderlyingHoldings.AveragePrice);
// and short put option position still exists in the portfolio
Assert.AreEqual(-1, _securities[Symbols.SPY_P_192_Feb19_2016].Holdings.Quantity);
}
[Test]
public void FullExerciseCashSettledCallAddsCash()
{
_portfolio.SetCash(0);
_securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
_securities.Add(
Symbols.SPY_C_192_Feb19_2016,
new Option(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY_C_192_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
GetOptionSymbolProperties(Symbols.SPY_C_192_Feb19_2016),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
_securities[Symbols.SPY].SetMarketPrice(new TradeBar { Time = _securities.UtcTime, Symbol = Symbols.SPY, Close = 195 });
_securities[Symbols.SPY_C_192_Feb19_2016].Holdings.SetHoldings(1, 1);
var holdings = _securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity;
_transactions.AddOrder(new SubmitOrderRequest(OrderType.OptionExercise, SecurityType.Option, Symbols.SPY_C_192_Feb19_2016, -holdings, 0, 0, _securities.UtcTime, ""));
var option = (Option)_securities[Symbols.SPY_C_192_Feb19_2016];
var order = (OptionExerciseOrder)_transactions.GetOrders(x => true).First();
option.Underlying = _securities[Symbols.SPY];
option.ExerciseSettlement = SettlementType.Cash;
var fills = option.OptionExerciseModel.OptionExercise(option, order).ToList();
Assert.AreEqual(1, fills.Count);
Assert.IsFalse(fills[0].IsAssignment);
Assert.AreEqual(order.Quantity, fills[0].FillQuantity);
StringAssert.Contains("Automatic Exercise", fills[0].Message);
Assert.IsTrue(fills[0].IsInTheMoney);
foreach (var fill in fills)
{
fill.Ticket = order.ToOrderTicket(_transactions);
_portfolio.ProcessFills(new List<OrderEvent> { fill });
}
// (underlying price - strike price) times number of shares
Assert.AreEqual((195 - 192) * 100, _portfolio.Cash);
Assert.AreEqual(_portfolio.TotalNetProfit, option.Holdings.NetProfit);
// we paid 100
Assert.AreEqual(_portfolio.Cash - 100, option.Holdings.NetProfit);
Assert.AreEqual(0, _securities[Symbols.SPY].Holdings.Quantity);
// and long call option position has disappeared
Assert.AreEqual(0, _securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity);
}
[Test]
public void FullExerciseOTMCashSettledCallAddsNoCash()
{
_portfolio.SetCash(0);
_securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
_securities.Add(
Symbols.SPY_C_192_Feb19_2016,
new Option(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY_C_192_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
GetOptionSymbolProperties(Symbols.SPY_C_192_Feb19_2016),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
_securities[Symbols.SPY].SetMarketPrice(new TradeBar { Time = _securities.UtcTime, Symbol = Symbols.SPY, Close = 190 });
_securities[Symbols.SPY_C_192_Feb19_2016].Holdings.SetHoldings(1, 100);
var holdings = _securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity;
_transactions.AddOrder(new SubmitOrderRequest(OrderType.OptionExercise, SecurityType.Option, Symbols.SPY_C_192_Feb19_2016, -holdings, 0, 0, _securities.UtcTime, ""));
var option = (Option)_securities[Symbols.SPY_C_192_Feb19_2016];
var order = (OptionExerciseOrder)_transactions.GetOrders(x => true).First();
option.Underlying = _securities[Symbols.SPY];
option.ExerciseSettlement = SettlementType.Cash;
var fills = option.OptionExerciseModel.OptionExercise(option, order).ToList();
Assert.AreEqual(1, fills.Count);
Assert.IsFalse(fills[0].IsAssignment);
StringAssert.Contains("OTM", fills[0].Message);
Assert.IsFalse(fills[0].IsInTheMoney);
Assert.AreEqual(0, fills[0].FillPrice);
Assert.AreEqual(0, fills[0].FillPrice);
Assert.AreEqual(order.Quantity, fills[0].FillQuantity);
foreach (var fill in fills)
{
fill.Ticket = order.ToOrderTicket(_transactions);
_portfolio.ProcessFills(new List<OrderEvent> { fill });
}
// no cash comes to the account because our contract was OTM
Assert.AreEqual(0, _portfolio.Cash);
Assert.AreEqual(0, _securities[Symbols.SPY].Holdings.Quantity);
Assert.AreEqual(_portfolio.TotalNetProfit, option.Holdings.NetProfit);
Assert.AreEqual(-10000, option.Holdings.NetProfit);
// and long call option position has disappeared
Assert.AreEqual(0, _securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity);
}
[Test]
public void FullExerciseCashSettledPutAddsCash()
{
_portfolio.SetCash(0);
_securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
_securities.Add(
Symbols.SPY_P_192_Feb19_2016,
new Option(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY_P_192_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
GetOptionSymbolProperties(Symbols.SPY_P_192_Feb19_2016),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
_securities[Symbols.SPY].SetMarketPrice(new TradeBar { Time = _securities.UtcTime, Symbol = Symbols.SPY, Close = 189 });
_securities[Symbols.SPY_P_192_Feb19_2016].Holdings.SetHoldings(1, 1);
var holdings = _securities[Symbols.SPY_P_192_Feb19_2016].Holdings.Quantity;
_transactions.AddOrder(new SubmitOrderRequest(OrderType.OptionExercise, SecurityType.Option, Symbols.SPY_P_192_Feb19_2016, -holdings, 0, 0, _securities.UtcTime, ""));
var option = (Option)_securities[Symbols.SPY_P_192_Feb19_2016];
var order = (OptionExerciseOrder)_transactions.GetOrders(x => true).First();
option.Underlying = _securities[Symbols.SPY];
option.ExerciseSettlement = SettlementType.Cash;
var fills = option.OptionExerciseModel.OptionExercise(option, order).ToList();
Assert.AreEqual(1, fills.Count);
Assert.IsFalse(fills[0].IsAssignment);
Assert.AreEqual(order.Quantity, fills[0].FillQuantity);
StringAssert.Contains("Automatic Exercise", fills[0].Message);
Assert.IsTrue(fills[0].IsInTheMoney);
foreach (var fill in fills)
{
fill.Ticket = order.ToOrderTicket(_transactions);
_portfolio.ProcessFills(new List<OrderEvent> { fill });
}
// (strike price - underlying price) times number of shares
Assert.AreEqual((192 - 189) * 100, _portfolio.Cash);
Assert.AreEqual(_portfolio.TotalNetProfit, option.Holdings.NetProfit);
// we paid 100 => 1 price, 1 quantity, 100x multiplier
Assert.AreEqual(_portfolio.Cash - 100, option.Holdings.NetProfit);
Assert.AreEqual(0, _securities[Symbols.SPY].Holdings.Quantity);
// and long put option position has disappeared
Assert.AreEqual(0, _securities[Symbols.SPY_P_192_Feb19_2016].Holdings.Quantity);
}
[Test]
public void ComputeMarginProperlyOnOptionExercise()
{
var algorithm = new QCAlgorithm();
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var orderProcessor = new OrderProcessor();
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
var time = DateTime.Now;
algorithm.Securities = securities;
transactions.SetOrderProcessor(orderProcessor);
portfolio.SetCash(1000);
securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
securities.Add(
Symbols.SPY_C_192_Feb19_2016,
new Option(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY_C_192_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
GetOptionSymbolProperties(Symbols.SPY_C_192_Feb19_2016),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
securities[Symbols.SPY].SetMarketPrice(new TradeBar { Time = securities.UtcTime, Symbol = Symbols.SPY, Close = 195 });
securities[Symbols.SPY_C_192_Feb19_2016].Holdings.SetHoldings(1, 10);
var option = (Option)securities[Symbols.SPY_C_192_Feb19_2016];
option.Underlying = securities[Symbols.SPY];
var holdings = securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity;
var order = new OptionExerciseOrder(Symbols.SPY_C_192_Feb19_2016, -holdings, time.AddSeconds(1));
orderProcessor.AddOrder(order);
var request = new SubmitOrderRequest(OrderType.OptionExercise, option.Type, option.Symbol, order.Quantity, 0, 0, order.Time, null);
request.SetOrderId(0);
orderProcessor.AddTicket(new OrderTicket(null, request));
var hasSufficientBuyingPower = option.BuyingPowerModel.HasSufficientBuyingPowerForOrder(portfolio, option, order).IsSufficient;
Assert.IsFalse(hasSufficientBuyingPower);
securities[Symbols.SPY].SetMarketPrice(new TradeBar { Time = securities.UtcTime, Symbol = Symbols.SPY, Close = 150 });
order = new OptionExerciseOrder(Symbols.SPY_C_192_Feb19_2016, -holdings, time.AddSeconds(1));
orderProcessor.AddOrder(order);
request = new SubmitOrderRequest(OrderType.OptionExercise, option.Type, option.Symbol, order.Quantity, 0, 0, order.Time, null);
request.SetOrderId(0);
orderProcessor.AddTicket(new OrderTicket(null, request));
hasSufficientBuyingPower = option.BuyingPowerModel.HasSufficientBuyingPowerForOrder(portfolio, option, order).IsSufficient;
Assert.IsTrue(hasSufficientBuyingPower);
}
[Test]
public void ComputeMarginProperlyOnOptionAssignment()
{
var algorithm = new QCAlgorithm();
var securities = new SecurityManager(TimeKeeper);
var transactions = new SecurityTransactionManager(null, securities);
var orderProcessor = new OrderProcessor();
var portfolio = new SecurityPortfolioManager(securities, transactions, new AlgorithmSettings());
var time = DateTime.Now;
algorithm.Securities = securities;
transactions.SetOrderProcessor(orderProcessor);
portfolio.SetCash(1000);
securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
securities.Add(
Symbols.SPY_C_192_Feb19_2016,
new Option(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY_C_192_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
GetOptionSymbolProperties(Symbols.SPY_C_192_Feb19_2016),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
securities[Symbols.SPY].SetMarketPrice(new TradeBar { Time = securities.UtcTime, Symbol = Symbols.SPY, Close = 195 });
securities[Symbols.SPY_C_192_Feb19_2016].Holdings.SetHoldings(1, -10);
var option = (Option)securities[Symbols.SPY_C_192_Feb19_2016];
option.Underlying = securities[Symbols.SPY];
var holdings = securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity;
var order = new OptionExerciseOrder(Symbols.SPY_C_192_Feb19_2016, -holdings, time.AddSeconds(1));
orderProcessor.AddOrder(order);
var request = new SubmitOrderRequest(OrderType.OptionExercise, option.Type, option.Symbol, order.Quantity, 0, 0, order.Time, null);
request.SetOrderId(0);
orderProcessor.AddTicket(new OrderTicket(null, request));
var hasSufficientBuyingPower = option.BuyingPowerModel.HasSufficientBuyingPowerForOrder(portfolio, option, order).IsSufficient;
Assert.IsFalse(hasSufficientBuyingPower);
securities[Symbols.SPY].SetMarketPrice(new TradeBar { Time = securities.UtcTime, Symbol = Symbols.SPY, Close = 150 });
order = new OptionExerciseOrder(Symbols.SPY_C_192_Feb19_2016, -holdings, time.AddSeconds(1));
orderProcessor.AddOrder(order);
request = new SubmitOrderRequest(OrderType.OptionExercise, option.Type, option.Symbol, order.Quantity, 0, 0, order.Time, null);
request.SetOrderId(0);
orderProcessor.AddTicket(new OrderTicket(null, request));
hasSufficientBuyingPower = option.BuyingPowerModel.HasSufficientBuyingPowerForOrder(portfolio, option, order).IsSufficient;
Assert.IsTrue(hasSufficientBuyingPower);
}
[Test]
public void PartialExerciseCashSettledCallAddsSomeCash()
{
_portfolio.SetCash(0);
_securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
_securities.Add(
Symbols.SPY_C_192_Feb19_2016,
new Option(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY_C_192_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
GetOptionSymbolProperties(Symbols.SPY_C_192_Feb19_2016),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
_securities[Symbols.SPY].SetMarketPrice(new TradeBar { Time = _securities.UtcTime, Symbol = Symbols.SPY, Close = 195 });
_securities[Symbols.SPY_C_192_Feb19_2016].Holdings.SetHoldings(1, 2);
var holdings = _securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity;
_transactions.AddOrder(new SubmitOrderRequest(OrderType.OptionExercise, SecurityType.Option, Symbols.SPY_C_192_Feb19_2016, -holdings / 2, 0, 0, _securities.UtcTime, ""));
var option = (Option)_securities[Symbols.SPY_C_192_Feb19_2016];
var order = (OptionExerciseOrder)_transactions.GetOrders(x => true).First();
option.Underlying = _securities[Symbols.SPY];
option.ExerciseSettlement = SettlementType.Cash;
var fills = option.OptionExerciseModel.OptionExercise(option, order).ToList();
Assert.AreEqual(1, fills.Count);
Assert.IsFalse(fills[0].IsAssignment);
Assert.AreEqual(order.Quantity, fills[0].FillQuantity);
StringAssert.Contains("Automatic Exercise", fills[0].Message);
Assert.IsTrue(fills[0].IsInTheMoney);
foreach (var fill in fills)
{
fill.Ticket = order.ToOrderTicket(_transactions);
_portfolio.ProcessFills(new List<OrderEvent> { fill });
}
// (underlying price - strike price) times number of shares
Assert.AreEqual((195 - 192) * 100, _portfolio.Cash);
Assert.AreEqual(0, _securities[Symbols.SPY].Holdings.Quantity);
// and call option position still has some value
Assert.AreEqual(1, _securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity);
}
[Test]
public void InternalCashSettledCallAssignmentReducesCash()
{
// (underlying price - strike price) times number of shares
_portfolio.SetCash((195 - 192) * 100);
_securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
_securities.Add(
Symbols.SPY_C_192_Feb19_2016,
new Option(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY_C_192_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
GetOptionSymbolProperties(Symbols.SPY_C_192_Feb19_2016),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
_securities[Symbols.SPY].SetMarketPrice(new TradeBar { Time = _securities.UtcTime, Symbol = Symbols.SPY, Close = 195 });
_securities[Symbols.SPY_C_192_Feb19_2016].Holdings.SetHoldings(1, -1);
var holdings = _securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity;
_transactions.AddOrder(new SubmitOrderRequest(OrderType.OptionExercise, SecurityType.Option, Symbols.SPY_C_192_Feb19_2016, -holdings, 0, 0, _securities.UtcTime, ""));
var option = (Option)_securities[Symbols.SPY_C_192_Feb19_2016];
var order = (OptionExerciseOrder)_transactions.GetOrders(x => true).First();
option.Underlying = _securities[Symbols.SPY];
option.ExerciseSettlement = SettlementType.Cash;
var fills = option.OptionExerciseModel.OptionExercise(option, order).ToList();
Assert.AreEqual(1, fills.Count);
Assert.IsTrue(fills[0].IsAssignment);
Assert.AreEqual(order.Quantity, fills[0].FillQuantity);
StringAssert.Contains("Assigned", fills[0].Message);
// we are simulating assignment by calling a method for this
var portfolioModel = (OptionPortfolioModel)option.PortfolioModel;
foreach (var fill in fills)
{
fill.Ticket = order.ToOrderTicket(_transactions);
portfolioModel.ProcessFill(_portfolio, option, fill);
}
// we just got assigned!
var newUnderlyingHoldings = _securities[Symbols.SPY].Holdings;
Assert.AreEqual(0, _portfolio.Cash);
Assert.AreEqual(0, _securities[Symbols.SPY].Holdings.Quantity);
// and short call option position has disappeared
Assert.AreEqual(0, _securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity);
}
[Test]
public void InternalCashSettledOTMCallAssignmentDoesntChangeAnything()
{
_portfolio.SetCash(0);
_securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
_securities.Add(
Symbols.SPY_C_192_Feb19_2016,
new Option(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY_C_192_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
GetOptionSymbolProperties(Symbols.SPY_C_192_Feb19_2016),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
_securities[Symbols.SPY].SetMarketPrice(new TradeBar { Time = _securities.UtcTime, Symbol = Symbols.SPY, Close = 10 });
_securities[Symbols.SPY_C_192_Feb19_2016].Holdings.SetHoldings(1, -100);
var holdings = _securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity;
_transactions.AddOrder(new SubmitOrderRequest(OrderType.OptionExercise, SecurityType.Option, Symbols.SPY_C_192_Feb19_2016, -holdings, 0, 0, _securities.UtcTime, ""));
var option = (Option)_securities[Symbols.SPY_C_192_Feb19_2016];
var order = (OptionExerciseOrder)_transactions.GetOrders(x => true).First();
option.Underlying = _securities[Symbols.SPY];
option.ExerciseSettlement = SettlementType.Cash;
var fills = option.OptionExerciseModel.OptionExercise(option, order).ToList();
Assert.AreEqual(1, fills.Count);
Assert.IsFalse(fills[0].IsAssignment);
StringAssert.Contains("OTM", fills[0].Message);
Assert.IsFalse(fills[0].IsInTheMoney);
Assert.AreEqual(0, fills[0].FillPrice);
Assert.AreEqual(order.Quantity, fills[0].FillQuantity);
// we are simulating assignment by calling a method for this
var portfolioModel = (OptionPortfolioModel)option.PortfolioModel;
foreach (var fill in fills)
{
fill.Ticket = order.ToOrderTicket(_transactions);
portfolioModel.ProcessFill(_portfolio, option, fill);
}
// we just got assigned! nothing changed...
var newUnderlyingHoldings = _securities[Symbols.SPY].Holdings;
Assert.AreEqual(0, _portfolio.Cash);
Assert.AreEqual(0, _securities[Symbols.SPY].Holdings.Quantity);
// and short call option position has disappeared
Assert.AreEqual(0, _securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity);
}
[Test]
public void InternalCashSettledPutAssignmentReducesCash()
{
// (strike price - underlying price) times number of shares
_portfolio.SetCash((192 - 189) * 100);
_securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
_securities.Add(
Symbols.SPY_P_192_Feb19_2016,
new Option(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY_P_192_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
GetOptionSymbolProperties(Symbols.SPY_P_192_Feb19_2016),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
_securities[Symbols.SPY].SetMarketPrice(new TradeBar { Time = _securities.UtcTime, Symbol = Symbols.SPY, Close = 189 });
_securities[Symbols.SPY_P_192_Feb19_2016].Holdings.SetHoldings(1, -1);
var holdings = _securities[Symbols.SPY_P_192_Feb19_2016].Holdings.Quantity;
_transactions.AddOrder(new SubmitOrderRequest(OrderType.OptionExercise, SecurityType.Option, Symbols.SPY_P_192_Feb19_2016, -holdings, 0, 0, _securities.UtcTime, ""));
var option = (Option)_securities[Symbols.SPY_P_192_Feb19_2016];
var order = (OptionExerciseOrder)_transactions.GetOrders(x => true).First();
option.Underlying = _securities[Symbols.SPY];
option.ExerciseSettlement = SettlementType.Cash;
var fills = option.OptionExerciseModel.OptionExercise(option, order).ToList();
Assert.AreEqual(1, fills.Count);
Assert.IsTrue(fills[0].IsAssignment);
Assert.AreEqual(order.Quantity, fills[0].FillQuantity);
StringAssert.Contains("Assigned", fills[0].Message);
// we are simulating assignment by calling a method for this
var portfolioModel = (OptionPortfolioModel)option.PortfolioModel;
foreach (var fill in fills)
{
fill.Ticket = order.ToOrderTicket(_transactions);
portfolioModel.ProcessFill(_portfolio, option, fill);
}
// we just got assigned!
var newUnderlyingHoldings = _securities[Symbols.SPY].Holdings;
Assert.AreEqual(0, _portfolio.Cash);
Assert.AreEqual(0, _securities[Symbols.SPY].Holdings.Quantity);
// and short put option position has disappeared
Assert.AreEqual(0, _securities[Symbols.SPY_P_192_Feb19_2016].Holdings.Quantity);
}
[Test]
public void InternalPartialCashSettledPutAssignmentReducesSomeCash()
{
// (strike price - underlying price) times number of shares
_portfolio.SetCash((192 - 189) * 100);
_securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
_securities.Add(
Symbols.SPY_P_192_Feb19_2016,
new Option(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY_P_192_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
GetOptionSymbolProperties(Symbols.SPY_P_192_Feb19_2016),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
_securities[Symbols.SPY].SetMarketPrice(new TradeBar { Time = _securities.UtcTime, Symbol = Symbols.SPY, Close = 189 });
_securities[Symbols.SPY_P_192_Feb19_2016].Holdings.SetHoldings(1, -2);
var holdings = _securities[Symbols.SPY_P_192_Feb19_2016].Holdings.Quantity;
_transactions.AddOrder(new SubmitOrderRequest(OrderType.OptionExercise, SecurityType.Option, Symbols.SPY_P_192_Feb19_2016, -holdings / 2, 0, 0, _securities.UtcTime, ""));
var option = (Option)_securities[Symbols.SPY_P_192_Feb19_2016];
var order = (OptionExerciseOrder)_transactions.GetOrders(x => true).First();
option.Underlying = _securities[Symbols.SPY];
option.ExerciseSettlement = SettlementType.Cash;
var fills = option.OptionExerciseModel.OptionExercise(option, order).ToList();
Assert.AreEqual(1, fills.Count);
Assert.IsTrue(fills[0].IsAssignment);
Assert.AreEqual(order.Quantity, fills[0].FillQuantity);
StringAssert.Contains("Assigned", fills[0].Message);
// we are simulating assignment by calling a method for this
var portfolioModel = (OptionPortfolioModel)option.PortfolioModel;
foreach (var fill in fills)
{
fill.Ticket = order.ToOrderTicket(_transactions);
portfolioModel.ProcessFill(_portfolio, option, fill);
}
// we just got assigned!
var newUnderlyingHoldings = _securities[Symbols.SPY].Holdings;
Assert.AreEqual(0, _portfolio.Cash);
Assert.AreEqual(0, _securities[Symbols.SPY].Holdings.Quantity);
// and short put option position still exists in the portfolio
Assert.AreEqual(-1, _securities[Symbols.SPY_P_192_Feb19_2016].Holdings.Quantity);
}
[TestCase(DataNormalizationMode.Adjusted)]
[TestCase(DataNormalizationMode.Raw)]
[TestCase(DataNormalizationMode.SplitAdjusted)]
[TestCase(DataNormalizationMode.TotalReturn)]
public void AlwaysAppliesSplitInLiveMode(DataNormalizationMode mode)
{
var algorithm = new QCAlgorithm();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
algorithm.UniverseSettings.DataNormalizationMode = mode;
algorithm.SetLiveMode(true);
var initialCash = algorithm.Portfolio.CashBook.TotalValueInAccountCurrency;
var spy = algorithm.AddEquity("SPY");
spy.SetMarketPrice(new Tick(new DateTime(2000, 01, 01), Symbols.SPY, 100m, 99m, 101m) { TickType = TickType.Trade });
spy.Holdings.SetHoldings(100m, 100);
var split = new Split(Symbols.SPY, new DateTime(2000, 01, 01), 100, 0.5m, SplitType.SplitOccurred);
algorithm.Portfolio.ApplySplit(split,
spy,
algorithm.LiveMode,
algorithm.SubscriptionManager.SubscriptionDataConfigService
.GetSubscriptionDataConfigs(spy.Symbol)
.DataNormalizationMode());
// confirm the split was properly applied to our holdings, no left over cash from split
Assert.AreEqual(50m, spy.Price);
Assert.AreEqual(200, spy.Holdings.Quantity);
Assert.AreEqual(initialCash, algorithm.Portfolio.CashBook.TotalValueInAccountCurrency);
}
[TestCase(true)]
[TestCase(false)]
public void SplitPartialSharesHandling(bool hasData)
{
var algorithm = new QCAlgorithm();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
algorithm.SetLiveMode(true);
var initialCash = algorithm.Portfolio.CashBook.TotalValueInAccountCurrency;
var spy = algorithm.AddEquity("SPY");
if (hasData)
{
spy.SetMarketPrice(new Tick(new DateTime(2000, 01, 01), Symbols.SPY, 100m, 99m, 101m) { TickType = TickType.Trade });
}
spy.Holdings.SetHoldings(100m, 100);
var split = new Split(Symbols.SPY, new DateTime(2000, 01, 01), 100, 0.49999m, SplitType.SplitOccurred);
var newAvgPrice = spy.Holdings.AveragePrice * split.SplitFactor;
var newQuantity = spy.Holdings.Quantity / split.SplitFactor;
var leftOver = newQuantity - (int)newQuantity;
algorithm.Portfolio.ApplySplit(split,
spy,
algorithm.LiveMode,
algorithm.SubscriptionManager.SubscriptionDataConfigService
.GetSubscriptionDataConfigs(spy.Symbol)
.DataNormalizationMode());
// confirm the split was properly applied to our holdings, no left over cash from split
Assert.AreEqual(newAvgPrice, spy.Holdings.AveragePrice);
Assert.AreEqual((int)newQuantity, spy.Holdings.Quantity);
var cashDifference = leftOver * split.Price * split.SplitFactor;
Assert.AreEqual(initialCash + cashDifference, algorithm.Portfolio.CashBook.TotalValueInAccountCurrency);
}
[Test]
public void HoldingsPriceIsUpdatedOnSplit()
{
var algorithm = new QCAlgorithm();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
var spy = algorithm.AddEquity("SPY", dataNormalizationMode: DataNormalizationMode.Raw);
// Update with both a trade and quote bar
spy.SetMarketPrice(new TradeBar(new DateTime(2000, 01, 01), Symbols.SPY, 100m, 100m, 100m, 100m, 100m, Time.OneMinute));
spy.SetMarketPrice(new QuoteBar(new DateTime(2000, 01, 01), Symbols.SPY, new Bar(100m, 100m, 100m, 100m), 100m, new Bar(100m, 100m, 100m, 100m), 100m, Time.OneMinute));
spy.Holdings.SetHoldings(100m, 100);
var split = new Split(Symbols.SPY, new DateTime(2000, 01, 01), 100, 0.5m, SplitType.SplitOccurred);
algorithm.Portfolio.ApplySplit(split,
spy,
algorithm.LiveMode,
algorithm.SubscriptionManager.SubscriptionDataConfigService
.GetSubscriptionDataConfigs(spy.Symbol)
.DataNormalizationMode());
// confirm the split was properly applied to our holdings
Assert.AreEqual(50m, spy.Holdings.AveragePrice);
Assert.AreEqual(200, spy.Holdings.Quantity);
// Market price should have also been updated
Assert.AreEqual(50m, spy.Holdings.Price);
}
[TestCase(DataNormalizationMode.Adjusted)]
[TestCase(DataNormalizationMode.Raw)]
[TestCase(DataNormalizationMode.SplitAdjusted)]
[TestCase(DataNormalizationMode.TotalReturn)]
public void NeverAppliesDividendInLiveMode(DataNormalizationMode mode)
{
var algorithm = new QCAlgorithm();
algorithm.UniverseSettings.DataNormalizationMode = mode;
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
algorithm.SetLiveMode(true);
var initialCash = algorithm.Portfolio.CashBook.TotalValueInAccountCurrency;
var spy = algorithm.AddEquity("SPY");
spy.SetMarketPrice(new Tick(new DateTime(2000, 01, 01), Symbols.SPY, 100m, 99m, 101m));
spy.Holdings.SetHoldings(100m, 100);
var dividend = new Dividend(Symbols.SPY, new DateTime(2000, 01, 01), 100, 0.5m);
algorithm.Portfolio.ApplyDividend(dividend,
algorithm.LiveMode,
algorithm.SubscriptionManager.SubscriptionDataConfigService
.GetSubscriptionDataConfigs(spy.Symbol)
.DataNormalizationMode());
// confirm no changes were made
Assert.AreEqual(100m, spy.Price);
Assert.AreEqual(100, spy.Holdings.Quantity);
Assert.AreEqual(initialCash, algorithm.Portfolio.CashBook.TotalValueInAccountCurrency);
}
[TestCase(DataNormalizationMode.Adjusted, 200, 0)]
[TestCase(DataNormalizationMode.Raw, 100, 100)]
[TestCase(DataNormalizationMode.SplitAdjusted, 100, 100)]
[TestCase(DataNormalizationMode.TotalReturn, 200, 0)]
public void NormalizationModeDoesNotChangeNetProfit(DataNormalizationMode mode, decimal profitLoss, decimal dividendPayment)
{
const decimal fee = 1;
const decimal quantity = 100;
var algorithm = new QCAlgorithm();
algorithm.UniverseSettings.DataNormalizationMode = mode;
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
var spy = algorithm.AddEquity("SPY");
spy.SetMarketPrice(new Tick(new DateTime(2000, 01, 01), Symbols.SPY, 100m, 99m, 101m));
spy.Holdings.SetHoldings(100m, quantity);
spy.Holdings.AddNewFee(fee);
spy.Holdings.AddNewProfit(profitLoss);
var distribution = dividendPayment / quantity;
var dividend = new Dividend(Symbols.SPY, new DateTime(2000, 01, 01), distribution, 0.5m);
algorithm.Portfolio.ApplyDividend(dividend,
algorithm.LiveMode,
algorithm.SubscriptionManager.SubscriptionDataConfigService
.GetSubscriptionDataConfigs(spy.Symbol)
.DataNormalizationMode());
// TotalProfit is the sum of profit loss and dividend
Assert.AreEqual(profitLoss + dividendPayment, algorithm.Portfolio.TotalProfit);
// TotalNetProfit is the sum of profit loss and dividend minus fees
Assert.AreEqual(profitLoss + dividendPayment - fee, algorithm.Portfolio.TotalNetProfit);
}
[TestCase()]
[TestCase(200000)]
public void SetAccountCurrency(decimal? startingCash = null)
{
var algorithm = new QCAlgorithm();
Assert.AreEqual(Currencies.USD, algorithm.AccountCurrency);
Assert.AreEqual(Currencies.USD, algorithm.Portfolio.CashBook.AccountCurrency);
var expectedAmount = algorithm.Portfolio.CashBook[Currencies.USD].Amount;
if (startingCash == null)
{
algorithm.SetAccountCurrency("btc");
}
else
{
algorithm.SetAccountCurrency("btc", (decimal)startingCash);
expectedAmount = (decimal)startingCash;
}
Assert.AreEqual("BTC", algorithm.AccountCurrency);
Assert.AreEqual("BTC", algorithm.Portfolio.CashBook.AccountCurrency);
Assert.AreEqual(expectedAmount, algorithm.Portfolio.CashBook["BTC"].Amount);
}
[Test]
public void CanNotChangeAccountCurrencyAfterAddingASecurity()
{
var algorithm = new QCAlgorithm();
algorithm.SetDateTime(TimeKeeper.UtcTime);
algorithm.Securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
Assert.Throws<InvalidOperationException>(() => algorithm.Portfolio.SetAccountCurrency(Currencies.USD));
}
[Test]
public void ChangeAccountCurrencyAfterImplicitSetCashRelabelsAmountToNewCurrency()
{
var algorithm = new QCAlgorithm();
// SetCash(decimal) means "this many units of the (eventual) account currency",
// so switching the account currency re-labels the amount instead of preserving USD.
algorithm.Portfolio.SetCash(1);
Assert.DoesNotThrow(() => algorithm.Portfolio.SetAccountCurrency("BTC"));
Assert.AreEqual("BTC", algorithm.Portfolio.CashBook.AccountCurrency);
Assert.AreEqual(1m, algorithm.Portfolio.CashBook["BTC"].Amount);
Assert.IsFalse(algorithm.Portfolio.CashBook.ContainsKey(Currencies.USD));
}
[Test]
public void ChangeAccountCurrencyAfterExplicitSetCashKeepsPreviousCash()
{
var algorithm = new QCAlgorithm();
// SetCash(symbol, ...) commits cash to a specific currency, so switching the account
// currency must keep that balance in its own entry and start the new one at zero.
algorithm.Portfolio.SetCash(Currencies.USD, 100000, 1);
Assert.DoesNotThrow(() => algorithm.Portfolio.SetAccountCurrency("BTC"));
Assert.AreEqual("BTC", algorithm.Portfolio.CashBook.AccountCurrency);
Assert.AreEqual(100000m, algorithm.Portfolio.CashBook[Currencies.USD].Amount);
Assert.AreEqual(0m, algorithm.Portfolio.CashBook["BTC"].Amount);
}
[Test]
public void ChangeAccountCurrencyAfterImplicitSetCashAppliesStartingCashToNewCurrency()
{
var algorithm = new QCAlgorithm();
algorithm.Portfolio.SetCash(100000);
algorithm.Portfolio.SetAccountCurrency(Currencies.EUR, 50000);
Assert.AreEqual(Currencies.EUR, algorithm.Portfolio.CashBook.AccountCurrency);
Assert.AreEqual(50000m, algorithm.Portfolio.CashBook[Currencies.EUR].Amount);
Assert.IsFalse(algorithm.Portfolio.CashBook.ContainsKey(Currencies.USD));
}
[Test]
public void ChangeAccountCurrencyAfterExplicitSetCashAppliesStartingCashToNewCurrency()
{
var algorithm = new QCAlgorithm();
algorithm.Portfolio.SetCash(Currencies.USD, 100000, 1);
algorithm.Portfolio.SetAccountCurrency(Currencies.EUR, 50000);
Assert.AreEqual(Currencies.EUR, algorithm.Portfolio.CashBook.AccountCurrency);
Assert.AreEqual(100000m, algorithm.Portfolio.CashBook[Currencies.USD].Amount);
Assert.AreEqual(50000m, algorithm.Portfolio.CashBook[Currencies.EUR].Amount);
}
[Test]
public void SetAccountCurrencyWithSameCurrencyOverridesStartingCash()
{
var algorithm = new QCAlgorithm();
algorithm.Portfolio.SetCash(100000);
// Calling SetAccountCurrency with the same currency must overwrite the previously
// set cash amount instead of keeping the older value.
algorithm.Portfolio.SetAccountCurrency(Currencies.USD, 200000);
Assert.AreEqual(Currencies.USD, algorithm.Portfolio.CashBook.AccountCurrency);
Assert.AreEqual(200000m, algorithm.Portfolio.CashBook[Currencies.USD].Amount);
}
[Test]
public void SetAccountCurrencyWithSameCurrencyAndNoStartingCashKeepsExistingAmount()
{
var algorithm = new QCAlgorithm();
algorithm.Portfolio.SetCash(100000);
algorithm.Portfolio.SetAccountCurrency(Currencies.USD);
Assert.AreEqual(Currencies.USD, algorithm.Portfolio.CashBook.AccountCurrency);
Assert.AreEqual(100000m, algorithm.Portfolio.CashBook[Currencies.USD].Amount);
}
[Test]
public void AddsEmptyUnsettledCashInstancesAsNewCashInstancesAreAddedToTheCashBook()
{
var algorithm = new QCAlgorithm();
var additions = 0;
algorithm.Portfolio.UnsettledCashBook.Updated += (sender, args) =>
{
if (args.UpdateType == CashBookUpdateType.Added)
{
additions++;
}
};
algorithm.SetCash(Currencies.EUR, 1000, 1.08m);
algorithm.SetCash("AUD", 1000, 0.7m);
// expected only 2 additions, USD is the account currency so it its supposed to already be there
Assert.AreEqual(2, additions);
Assert.AreEqual(3, algorithm.Portfolio.UnsettledCashBook.Count);
Assert.IsTrue(algorithm.Portfolio.UnsettledCashBook.ContainsKey(Currencies.USD));
Assert.IsTrue(algorithm.Portfolio.UnsettledCashBook.ContainsKey(Currencies.EUR));
Assert.IsTrue(algorithm.Portfolio.UnsettledCashBook.ContainsKey("AUD"));
// When added, the amount should be 0
Assert.IsTrue(algorithm.Portfolio.UnsettledCashBook
.Where(kvp => kvp.Key != Currencies.USD)
.All(kvp => kvp.Value.Amount == 0));
}
[Test]
public void UpdatesUnsettledCashCurrencyConversionAsItIsUpdatedForSettledCash()
{
var algorithm = new QCAlgorithm();
algorithm.SetCash(Currencies.EUR, 1000, 1.08m);
algorithm.SetCash("AUD", 1000, 0.7m);
var unsettledCashBook = algorithm.Portfolio.UnsettledCashBook;
var currencyConversionUpdates = 0;
foreach (var unsettledCash in unsettledCashBook.Values)
{
unsettledCash.CurrencyConversionUpdated += (sender, args) =>
{
currencyConversionUpdates++;
};
}
var cashBook = algorithm.Portfolio.CashBook;
var settledEurCash = cashBook["EUR"];
var settledAudCash = cashBook["AUD"];
var unsettledEurCash = unsettledCashBook["EUR"];
var unsettledAudCash = unsettledCashBook["AUD"];
settledEurCash.CurrencyConversion = new TestCurrencyConversion(cashBook.AccountCurrency, "EUR", 1.08m);
Assert.AreEqual(1, currencyConversionUpdates);
settledAudCash.CurrencyConversion = new TestCurrencyConversion(cashBook.AccountCurrency, "AUD", 0.7m);
Assert.AreEqual(2, currencyConversionUpdates);
var prevEurConversionRate = unsettledEurCash.ConversionRate;
var prevAudConversionRate = unsettledAudCash.ConversionRate;
foreach (var cash in cashBook.Values)
{
cash.Update();
}
Assert.AreEqual(prevEurConversionRate * 1.01m, settledEurCash.ConversionRate);
Assert.AreEqual(prevAudConversionRate * 1.01m, settledAudCash.ConversionRate);
Assert.AreEqual(settledEurCash.ConversionRate, unsettledEurCash.ConversionRate);
Assert.AreEqual(settledAudCash.ConversionRate, unsettledAudCash.ConversionRate);
}
private static TestCaseData[] MarginRemainingTestCases => new[]
{
new TestCaseData(SecurityType.Equity, 0, OrderDirection.Buy),
new TestCaseData(SecurityType.Equity, 0, OrderDirection.Sell),
new TestCaseData(SecurityType.Equity, +100, OrderDirection.Buy),
new TestCaseData(SecurityType.Equity, +100, OrderDirection.Sell),
new TestCaseData(SecurityType.Equity, -100, OrderDirection.Buy),
new TestCaseData(SecurityType.Equity, -100, OrderDirection.Sell),
new TestCaseData(SecurityType.Option, 0, OrderDirection.Buy),
new TestCaseData(SecurityType.Option, 0, OrderDirection.Sell),
new TestCaseData(SecurityType.Option, +10, OrderDirection.Buy),
new TestCaseData(SecurityType.Option, +10, OrderDirection.Sell).Explicit(),
new TestCaseData(SecurityType.Option, -10, OrderDirection.Buy),
new TestCaseData(SecurityType.Option, -10, OrderDirection.Sell),
new TestCaseData(SecurityType.FutureOption, 0, OrderDirection.Buy),
new TestCaseData(SecurityType.FutureOption, 0, OrderDirection.Sell),
new TestCaseData(SecurityType.FutureOption, +10, OrderDirection.Buy),
new TestCaseData(SecurityType.FutureOption, +10, OrderDirection.Sell),
new TestCaseData(SecurityType.FutureOption, -10, OrderDirection.Buy),
new TestCaseData(SecurityType.FutureOption, -10, OrderDirection.Sell),
};
[TestCaseSource(nameof(MarginRemainingTestCases))]
public void GetsMarginRemainingForSecurity(SecurityType securityType, int initialHoldingsQuantity,
OrderDirection direction)
{
var algorithm = new AlgorithmStub();
algorithm.SetSecurityInitializer(security => security.FeeModel = new ConstantFeeModel(0));
algorithm.SetCash(1000000);
Security security = null;
switch (securityType)
{
case SecurityType.Equity:
security = algorithm.AddEquity("SPY");
break;
case SecurityType.Option:
security = algorithm.AddOptionContract(Symbols.CreateOptionSymbol("SPY", OptionRight.Call, 300, new DateTime(2023, 05, 19)));
break;
case SecurityType.FutureOption:
var underlying = algorithm.AddFuture("ES");
security = algorithm.AddFutureOptionContract(Symbols.CreateFutureOptionSymbol(underlying.Symbol,
OptionRight.Call, 300, new DateTime(2023, 05, 19)));
break;
default:
Assert.Fail("Invalid security type.");
break;
}
security.SetMarketPrice(new Tick { Value = 100m });
security.Holdings.SetHoldings(security.Price, initialHoldingsQuantity);
var goingInSameDirection = security.Holdings.IsLong && direction == OrderDirection.Buy
|| security.Holdings.IsShort && direction == OrderDirection.Sell;
var marginRemaining = algorithm.Portfolio.GetMarginRemaining(security.Symbol, direction);
if (goingInSameDirection)
{
Assert.AreEqual(algorithm.Portfolio.MarginRemaining, marginRemaining);
}
else
{
var expectedMarginRemaining = algorithm.Portfolio.MarginRemaining
+ security.BuyingPowerModel.GetMaintenanceMargin(MaintenanceMarginParameters.ForCurrentHoldings(security))
+ Math.Abs(security.BuyingPowerModel.GetInitialMarginRequirement(new InitialMarginParameters(security, security.Holdings.Quantity)));
Assert.AreEqual(expectedMarginRemaining, marginRemaining);
}
}
[Test]
public void CashSettledAssignmentWithProfitOrLossShowsCorrectTag()
{
_portfolio.SetCash(0);
// Add underlying SPY
_securities.Add(
Symbols.SPY,
new Security(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
)
);
// Add a call option that will be ITM to force assignment
_securities.Add(
Symbols.SPY_C_192_Feb19_2016,
new Option(
SecurityExchangeHours,
CreateTradeBarDataConfig(SecurityType.Equity, Symbols.SPY_C_192_Feb19_2016),
new Cash(Currencies.USD, 0, 1m),
GetOptionSymbolProperties(Symbols.SPY_C_192_Feb19_2016),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
)
);
// SHORT position
_securities[Symbols.SPY_C_192_Feb19_2016].Holdings.SetHoldings(1, -2);
// Underlying > Strike (192)
_securities[Symbols.SPY].SetMarketPrice(new Tick { Value = 200 });
var option = (Option)_securities[Symbols.SPY_C_192_Feb19_2016];
option.Underlying = _securities[Symbols.SPY];
option.ExerciseSettlement = SettlementType.Cash;
var holdings = _securities[Symbols.SPY_C_192_Feb19_2016].Holdings.Quantity;
_transactions.AddOrder(new SubmitOrderRequest(
OrderType.OptionExercise,
SecurityType.Option,
Symbols.SPY_C_192_Feb19_2016,
-holdings,
0, 0,
_securities.UtcTime,
""));
var order = (OptionExerciseOrder)_transactions.GetOrders(x => true).First();
var fills = option.OptionExerciseModel.OptionExercise(option, order).ToList();
var assignmentFill = fills.FirstOrDefault(f => f.IsAssignment);
Assert.IsNotNull(assignmentFill);
foreach (var fill in fills)
{
fill.Ticket = order.ToOrderTicket(_transactions);
_portfolio.ProcessFills(new List<OrderEvent> { fill });
}
Assert.AreEqual("Assigned. Underlying: 200. Loss: -1400", assignmentFill.Message);
}
private SubscriptionDataConfig CreateTradeBarDataConfig(SecurityType type, Symbol symbol)
{
if (type == SecurityType.Equity)
return new SubscriptionDataConfig(typeof(TradeBar), symbol, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, true);
if (type == SecurityType.Forex)
return new SubscriptionDataConfig(typeof(TradeBar), symbol, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, true);
if (type == SecurityType.Future)
return new SubscriptionDataConfig(typeof(TradeBar), symbol, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, true);
if (type == SecurityType.Crypto)
return new SubscriptionDataConfig(typeof(TradeBar), symbol, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, true);
if (type == SecurityType.Cfd)
return new SubscriptionDataConfig(typeof(TradeBar), symbol, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, true);
throw new NotImplementedException(type.ToString());
}
private static OptionSymbolProperties GetOptionSymbolProperties(Symbol symbol)
{
return new OptionSymbolProperties(SymbolPropertiesDatabase.FromDataFolder().GetSymbolProperties(symbol.ID.Market, symbol, symbol.SecurityType, Currencies.USD));
}
private static TimeKeeper TimeKeeper
{
get { return new TimeKeeper(DateTime.Now, new[] { TimeZones.NewYork }); }
}
class OrderProcessor : IOrderProcessor
{
private readonly ConcurrentDictionary<int, Order> _orders = new ConcurrentDictionary<int, Order>();
private readonly ConcurrentDictionary<int, OrderTicket> _tickets = new ConcurrentDictionary<int, OrderTicket>();
public void AddOrder(Order order)
{
_orders[order.Id] = order;
}
public void AddTicket(OrderTicket ticket)
{
_tickets[ticket.OrderId] = ticket;
}
public int OrdersCount { get; private set; }
public Order GetOrderById(int orderId)
{
Order order;
_orders.TryGetValue(orderId, out order);
return order;
}
public List<Order> GetOrdersByBrokerageId(string brokerageId)
{
return _orders.Values.Where(o => o.BrokerId.Contains(brokerageId)).Select(o => o.Clone()).ToList();
}
public IEnumerable<OrderTicket> GetOrderTickets(Func<OrderTicket, bool> filter = null)
{
return _tickets.Values.Where(filter ?? (x => true));
}
public IEnumerable<OrderTicket> GetOpenOrderTickets(Func<OrderTicket, bool> filter = null)
{
return _tickets.Values.Where(x => x.Status.IsOpen() && (filter == null || filter(x)));
}
public OrderTicket GetOrderTicket(int orderId)
{
OrderTicket ticket;
_tickets.TryGetValue(orderId, out ticket);
return ticket;
}
public IEnumerable<Order> GetOrders(Func<Order, bool> filter = null)
{
return _orders.Values.Where(filter ?? (x => true));
}
public List<Order> GetOpenOrders(Func<Order, bool> filter = null)
{
return _orders.Values.Where(x => x.Status.IsOpen() && (filter == null || filter(x))).ToList();
}
public OrderTicket Process(OrderRequest request)
{
throw new NotImplementedException();
}
public ProjectedHoldings GetProjectedHoldings(Security security)
{
throw new NotImplementedException();
}
}
class TestCurrencyConversion : ICurrencyConversion
{
public event EventHandler<decimal> ConversionRateUpdated;
public string SourceCurrency { get; }
public string DestinationCurrency { get; }
public decimal ConversionRate { get; set; }
public IEnumerable<Security> ConversionRateSecurities { get; } = Enumerable.Empty<Security>();
public TestCurrencyConversion(string sourceCurrency, string destinationCurrency, decimal conversionRate)
{
SourceCurrency = sourceCurrency;
DestinationCurrency = destinationCurrency;
ConversionRate = conversionRate;
}
public void Update()
{
ConversionRate *= 1.01m;
}
}
}
}