Skip to content

Commit 8d148ac

Browse files
AlexCatarinoclaude
andcommitted
Allow SetAccountCurrency after SetCash without throwing
Previously, calling SetAccountCurrency after SetCash threw an InvalidOperationException. The portfolio manager now switches the base account currency in place: the previous Cash entry (and its balance) is preserved in the CashBook, and a notice is logged. When the new account currency matches the existing one, an optional startingCash overrides the previously set amount and the override is logged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3f5eefd commit 8d148ac

3 files changed

Lines changed: 105 additions & 15 deletions

File tree

Common/Messages/Messages.Securities.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,25 @@ public static class SecurityPortfolioManager
896896
public static string CannotChangeAccountCurrencyAfterSettingCash =
897897
"Cannot change AccountCurrency after setting cash. Please move SetAccountCurrency() before SetCash().";
898898

899+
/// <summary>
900+
/// Returns a string message saying the AccountCurrency has been changed after setting cash, reporting the
901+
/// remaining amount held in the previous account currency
902+
/// </summary>
903+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
904+
public static string AccountCurrencyChangedAfterSettingCash(Securities.Cash previousCash)
905+
{
906+
return Invariant($"Account currency was changed after SetCash() was called. Algorithm still holds {previousCash.Amount} {previousCash.Symbol} in the previous account currency.");
907+
}
908+
909+
/// <summary>
910+
/// Returns a string message saying the account currency starting cash has been updated from a previous amount to a new one
911+
/// </summary>
912+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
913+
public static string AccountCurrencyCashUpdated(string accountCurrency, decimal previousAmount, decimal newAmount)
914+
{
915+
return Invariant($"Account currency cash updated to {newAmount} {accountCurrency} from {previousAmount} {accountCurrency}.");
916+
}
917+
899918
/// <summary>
900919
/// Returns a string message saying the AccountCurrency has already been set and that the new value for this property
901920
/// will be ignored

Common/Securities/SecurityPortfolioManager.cs

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -616,10 +616,17 @@ public override SecurityHolding this[Symbol symbol]
616616

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

648-
if (_setCashWasCalled)
649-
{
650-
throw new InvalidOperationException("SecurityPortfolioManager.SetAccountCurrency(): " +
651-
Messages.SecurityPortfolioManager.CannotChangeAccountCurrencyAfterSettingCash);
652-
}
653-
654-
Log.Trace("SecurityPortfolioManager.SetAccountCurrency(): " +
655-
Messages.SecurityPortfolioManager.SettingAccountCurrency(accountCurrency));
655+
// Capture the previous base cash and amount if SetCash() was called earlier so we can
656+
// either report the leftover balance in the old currency or detect a same-currency override.
657+
var previousCash = _setCashWasCalled ? _baseCurrencyCash : null;
658+
var previousAmount = previousCash?.Amount;
659+
var message = Messages.SecurityPortfolioManager.SettingAccountCurrency(accountCurrency);
656660

657661
UnsettledCashBook.AccountCurrency = accountCurrency;
658662
CashBook.AccountCurrency = accountCurrency;
659663

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

667+
if (previousCash != null && previousCash.Symbol != accountCurrency)
668+
{
669+
// The CashBook.AccountCurrency setter migrates the previous amount onto the new
670+
// currency entry and removes the old one. Undo that migration so the previous
671+
// balance is kept in its own currency, and the new account currency starts at zero
672+
// (a subsequent SetCash below will apply startingCash if provided).
673+
_baseCurrencyCash.SetAmount(0);
674+
CashBook.Add(previousCash.Symbol, previousAmount.Value, previousCash.ConversionRate);
675+
message += ". " + Messages.SecurityPortfolioManager.AccountCurrencyChangedAfterSettingCash(previousCash);
676+
}
677+
662678
if (startingCash != null)
663679
{
664-
SetCash((decimal)startingCash);
680+
SetCash(startingCash.Value);
681+
// When the account currency is unchanged, report the override of the prior amount.
682+
if (previousCash?.Symbol == accountCurrency && previousAmount != startingCash)
683+
{
684+
message = Messages.SecurityPortfolioManager.AccountCurrencyCashUpdated(accountCurrency, previousAmount.Value, startingCash.Value);
685+
}
665686
}
687+
688+
Log.Trace("SecurityPortfolioManager.SetAccountCurrency(): " + message);
666689
}
667690

668691
/// <summary>

Tests/Common/Securities/SecurityPortfolioManagerTests.cs

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2578,7 +2578,7 @@ public void CanNotChangeAccountCurrencyAfterAddingASecurity()
25782578

25792579
[TestCase("SetCash(decimal cash)")]
25802580
[TestCase("SetCash(string symbol, ...)")]
2581-
public void CanNotChangeAccountCurrencyAfterSettingCash(string overload)
2581+
public void ChangeAccountCurrencyAfterSettingCashKeepsPreviousCash(string overload)
25822582
{
25832583
var algorithm = new QCAlgorithm();
25842584
if (overload == "SetCash(decimal cash)")
@@ -2587,9 +2587,57 @@ public void CanNotChangeAccountCurrencyAfterSettingCash(string overload)
25872587
}
25882588
else
25892589
{
2590-
algorithm.Portfolio.SetCash(Currencies.USD, 1, 1);
2590+
algorithm.Portfolio.SetCash(Currencies.USD, 10, 1);
25912591
}
2592-
Assert.Throws<InvalidOperationException>(() => algorithm.Portfolio.SetAccountCurrency(Currencies.USD));
2592+
2593+
// Switch the account currency to EUR; previous USD balance must be preserved
2594+
// in the CashBook and the base currency cash must be repointed to EUR.
2595+
Assert.DoesNotThrow(() => algorithm.Portfolio.SetAccountCurrency(Currencies.EUR));
2596+
2597+
Assert.AreEqual(Currencies.EUR, algorithm.Portfolio.CashBook.AccountCurrency);
2598+
Assert.AreEqual(10m, algorithm.Portfolio.CashBook[Currencies.USD].Amount);
2599+
Assert.AreEqual(0m, algorithm.Portfolio.CashBook[Currencies.EUR].Amount);
2600+
}
2601+
2602+
[Test]
2603+
public void ChangeAccountCurrencyAfterSettingCashAppliesStartingCashToNewCurrency()
2604+
{
2605+
var algorithm = new QCAlgorithm();
2606+
algorithm.Portfolio.SetCash(100000);
2607+
2608+
algorithm.Portfolio.SetAccountCurrency(Currencies.EUR, 50000);
2609+
2610+
Assert.AreEqual(Currencies.EUR, algorithm.Portfolio.CashBook.AccountCurrency);
2611+
// Previous USD cash is retained as a residual balance in the cash book.
2612+
Assert.AreEqual(100000m, algorithm.Portfolio.CashBook[Currencies.USD].Amount);
2613+
// Starting cash applies to the new account currency.
2614+
Assert.AreEqual(50000m, algorithm.Portfolio.CashBook[Currencies.EUR].Amount);
2615+
}
2616+
2617+
[Test]
2618+
public void SetAccountCurrencyWithSameCurrencyOverridesStartingCash()
2619+
{
2620+
var algorithm = new QCAlgorithm();
2621+
algorithm.Portfolio.SetCash(100000);
2622+
2623+
// Calling SetAccountCurrency with the same currency must overwrite the previously
2624+
// set cash amount instead of keeping the older value.
2625+
algorithm.Portfolio.SetAccountCurrency(Currencies.USD, 200000);
2626+
2627+
Assert.AreEqual(Currencies.USD, algorithm.Portfolio.CashBook.AccountCurrency);
2628+
Assert.AreEqual(200000m, algorithm.Portfolio.CashBook[Currencies.USD].Amount);
2629+
}
2630+
2631+
[Test]
2632+
public void SetAccountCurrencyWithSameCurrencyAndNoStartingCashKeepsExistingAmount()
2633+
{
2634+
var algorithm = new QCAlgorithm();
2635+
algorithm.Portfolio.SetCash(100000);
2636+
2637+
algorithm.Portfolio.SetAccountCurrency(Currencies.USD);
2638+
2639+
Assert.AreEqual(Currencies.USD, algorithm.Portfolio.CashBook.AccountCurrency);
2640+
Assert.AreEqual(100000m, algorithm.Portfolio.CashBook[Currencies.USD].Amount);
25932641
}
25942642

25952643
[Test]

0 commit comments

Comments
 (0)