Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Common/Messages/Messages.Securities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,25 @@ public static class SecurityPortfolioManager
public static string CannotChangeAccountCurrencyAfterSettingCash =
"Cannot change AccountCurrency after setting cash. Please move SetAccountCurrency() before SetCash().";

/// <summary>
/// Returns a string message saying the AccountCurrency has been changed after setting cash, reporting the
/// remaining amount held in the previous account currency
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string AccountCurrencyChangedAfterSettingCash(Securities.Cash previousCash)
{
return Invariant($"Account currency was changed after SetCash() was called. Algorithm still holds {previousCash.Amount} {previousCash.Symbol} in the previous account currency.");
}

/// <summary>
/// Returns a string message saying the account currency starting cash has been updated from a previous amount to a new one
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string AccountCurrencyCashUpdated(string accountCurrency, decimal previousAmount, decimal newAmount)
{
return Invariant($"Account currency cash updated to {newAmount} {accountCurrency} from {previousAmount} {accountCurrency}.");
}

/// <summary>
/// Returns a string message saying the AccountCurrency has already been set and that the new value for this property
/// will be ignored
Expand Down
47 changes: 35 additions & 12 deletions Common/Securities/SecurityPortfolioManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -616,10 +616,17 @@ public override SecurityHolding this[Symbol symbol]

/// <summary>
/// Sets the account currency cash symbol this algorithm is to manage, as well
/// as the starting cash in this currency if given
/// as the starting cash in this currency if given.
/// </summary>
/// <remarks>Has to be called before calling <see cref="SetCash(decimal)"/>
/// or adding any <see cref="Security"/></remarks>
/// <remarks>
/// Should be called before adding any <see cref="Security"/>.
/// If <see cref="SetCash(decimal)"/> was called beforehand, the previous cash entry
/// is preserved in the <see cref="CashBook"/>: the algorithm continues to hold that
/// amount in the previous currency, while <see cref="_baseCurrencyCash"/> is
/// repointed to the new account currency. When the new account currency matches the
/// existing one, the optional <paramref name="startingCash"/> simply overrides the
/// previously set amount.
/// </remarks>
/// <param name="accountCurrency">The account currency cash symbol to set</param>
/// <param name="startingCash">The account currency starting cash to set</param>
public void SetAccountCurrency(string accountCurrency, decimal? startingCash = null)
Expand All @@ -645,24 +652,40 @@ public void SetAccountCurrency(string accountCurrency, decimal? startingCash = n
Messages.SecurityPortfolioManager.CannotChangeAccountCurrencyAfterAddingSecurity);
}

if (_setCashWasCalled)
{
throw new InvalidOperationException("SecurityPortfolioManager.SetAccountCurrency(): " +
Messages.SecurityPortfolioManager.CannotChangeAccountCurrencyAfterSettingCash);
}

Log.Trace("SecurityPortfolioManager.SetAccountCurrency(): " +
Messages.SecurityPortfolioManager.SettingAccountCurrency(accountCurrency));
// Capture the previous base cash and amount if SetCash() was called earlier so we can
// either report the leftover balance in the old currency or detect a same-currency override.
var previousCash = _setCashWasCalled ? _baseCurrencyCash : null;
var previousAmount = previousCash?.Amount;
var message = Messages.SecurityPortfolioManager.SettingAccountCurrency(accountCurrency);

UnsettledCashBook.AccountCurrency = accountCurrency;
CashBook.AccountCurrency = accountCurrency;

// Repoint the base cash to the new account currency entry.
_baseCurrencyCash = CashBook[accountCurrency];

if (previousCash != null && previousCash.Symbol != accountCurrency)
{
// The CashBook.AccountCurrency setter migrates the previous amount onto the new
// currency entry and removes the old one. Undo that migration so the previous
// balance is kept in its own currency, and the new account currency starts at zero
// (a subsequent SetCash below will apply startingCash if provided).
_baseCurrencyCash.SetAmount(0);
CashBook.Add(previousCash.Symbol, previousAmount.Value, previousCash.ConversionRate);
message += ". " + Messages.SecurityPortfolioManager.AccountCurrencyChangedAfterSettingCash(previousCash);
}

if (startingCash != null)
{
SetCash((decimal)startingCash);
SetCash(startingCash.Value);
// When the account currency is unchanged, report the override of the prior amount.
if (previousCash?.Symbol == accountCurrency && previousAmount != startingCash)
{
message = Messages.SecurityPortfolioManager.AccountCurrencyCashUpdated(accountCurrency, previousAmount.Value, startingCash.Value);
}
}

Log.Trace("SecurityPortfolioManager.SetAccountCurrency(): " + message);
}

/// <summary>
Expand Down
54 changes: 51 additions & 3 deletions Tests/Common/Securities/SecurityPortfolioManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2578,7 +2578,7 @@ public void CanNotChangeAccountCurrencyAfterAddingASecurity()

[TestCase("SetCash(decimal cash)")]
[TestCase("SetCash(string symbol, ...)")]
public void CanNotChangeAccountCurrencyAfterSettingCash(string overload)
public void ChangeAccountCurrencyAfterSettingCashKeepsPreviousCash(string overload)
{
var algorithm = new QCAlgorithm();
if (overload == "SetCash(decimal cash)")
Expand All @@ -2587,9 +2587,57 @@ public void CanNotChangeAccountCurrencyAfterSettingCash(string overload)
}
else
{
algorithm.Portfolio.SetCash(Currencies.USD, 1, 1);
algorithm.Portfolio.SetCash(Currencies.USD, 10, 1);
}
Assert.Throws<InvalidOperationException>(() => algorithm.Portfolio.SetAccountCurrency(Currencies.USD));

// Switch the account currency to EUR; previous USD balance must be preserved
// in the CashBook and the base currency cash must be repointed to EUR.
Assert.DoesNotThrow(() => algorithm.Portfolio.SetAccountCurrency(Currencies.EUR));

Assert.AreEqual(Currencies.EUR, algorithm.Portfolio.CashBook.AccountCurrency);
Assert.AreEqual(10m, algorithm.Portfolio.CashBook[Currencies.USD].Amount);
Assert.AreEqual(0m, algorithm.Portfolio.CashBook[Currencies.EUR].Amount);
}

[Test]
public void ChangeAccountCurrencyAfterSettingCashAppliesStartingCashToNewCurrency()
{
var algorithm = new QCAlgorithm();
algorithm.Portfolio.SetCash(100000);

algorithm.Portfolio.SetAccountCurrency(Currencies.EUR, 50000);

Assert.AreEqual(Currencies.EUR, algorithm.Portfolio.CashBook.AccountCurrency);
// Previous USD cash is retained as a residual balance in the cash book.
Assert.AreEqual(100000m, algorithm.Portfolio.CashBook[Currencies.USD].Amount);
// Starting cash applies to the new account currency.
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]
Expand Down
Loading