Skip to content

Commit 63720f4

Browse files
authored
Fix snake_case error messages for Python algorithms (#9345)
* Fix snake_case error messages for Python algorithms * Minor fix * improve FormatCode robustness and fix test state isolation * migrate QCAlgorithm inline messages to Messages classes * Solve review comments * Minor fix * Fix broken regression tests * Minor fix * Minor fix * Add FormatCodeRoot helper for Python self-referenced properties * Address review comments * Minor fix
1 parent 329d91b commit 63720f4

15 files changed

Lines changed: 226 additions & 45 deletions

Algorithm/QCAlgorithm.History.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1474,7 +1474,7 @@ private void SetWarmup(int? barCount, TimeSpan? timeSpan, Resolution? resolution
14741474
{
14751475
if (_locked)
14761476
{
1477-
throw new InvalidOperationException("QCAlgorithm.SetWarmup(): This method cannot be used after algorithm initialized");
1477+
throw new InvalidOperationException(Messages.QCAlgorithm.SetWarmupAlreadyInitialized());
14781478
}
14791479

14801480
_warmupTimeSpan = timeSpan;

Algorithm/QCAlgorithm.cs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,7 +1315,7 @@ public void SetTimeZone(DateTimeZone timeZone)
13151315
{
13161316
if (_locked)
13171317
{
1318-
throw new InvalidOperationException("Algorithm.SetTimeZone(): Cannot change time zone after algorithm running.");
1318+
throw new InvalidOperationException(Messages.QCAlgorithm.SetTimeZoneAlreadyRunning());
13191319
}
13201320

13211321
if (timeZone == null) throw new ArgumentNullException(nameof(timeZone));
@@ -1450,7 +1450,7 @@ public void SetBenchmark(SecurityType securityType, string symbol)
14501450
{
14511451
if (_locked)
14521452
{
1453-
throw new InvalidOperationException("Algorithm.SetBenchmark(): Cannot change Benchmark after algorithm initialized.");
1453+
throw new InvalidOperationException(Messages.QCAlgorithm.SetBenchmarkAlreadyInitialized());
14541454
}
14551455

14561456
var market = GetMarket(null, symbol, securityType, defaultMarket: Market.USA);
@@ -1503,7 +1503,7 @@ public void SetBenchmark(Symbol symbol)
15031503
{
15041504
if (_locked)
15051505
{
1506-
throw new InvalidOperationException("Algorithm.SetBenchmark(): Cannot change Benchmark after algorithm initialized.");
1506+
throw new InvalidOperationException(Messages.QCAlgorithm.SetBenchmarkAlreadyInitialized());
15071507
}
15081508

15091509
// Create our security benchmark
@@ -1522,7 +1522,7 @@ public void SetBenchmark(Func<DateTime, decimal> benchmark)
15221522
{
15231523
if (_locked)
15241524
{
1525-
throw new InvalidOperationException("Algorithm.SetBenchmark(): Cannot change Benchmark after algorithm initialized.");
1525+
throw new InvalidOperationException(Messages.QCAlgorithm.SetBenchmarkAlreadyInitialized());
15261526
}
15271527

15281528
Benchmark = new FuncBenchmark(benchmark);
@@ -1599,8 +1599,7 @@ public void SetAccountCurrency(string accountCurrency, decimal? startingCash = n
15991599
{
16001600
if (_locked)
16011601
{
1602-
throw new InvalidOperationException("Algorithm.SetAccountCurrency(): " +
1603-
"Cannot change AccountCurrency after algorithm initialized.");
1602+
throw new InvalidOperationException(Messages.QCAlgorithm.SetAccountCurrencyAlreadyInitialized());
16041603
}
16051604

16061605
if (startingCash == null)
@@ -1653,7 +1652,7 @@ public void SetCash(decimal startingCash)
16531652
}
16541653
else
16551654
{
1656-
throw new InvalidOperationException("Algorithm.SetCash(): Cannot change cash available after algorithm initialized.");
1655+
throw new InvalidOperationException(Messages.QCAlgorithm.SetCashAlreadyInitialized());
16571656
}
16581657
}
16591658

@@ -1672,7 +1671,7 @@ public void SetCash(string symbol, decimal startingCash, decimal conversionRate
16721671
}
16731672
else
16741673
{
1675-
throw new InvalidOperationException("Algorithm.SetCash(): Cannot change cash available after algorithm initialized.");
1674+
throw new InvalidOperationException(Messages.QCAlgorithm.SetCashAlreadyInitialized());
16761675
}
16771676
}
16781677

@@ -1778,7 +1777,7 @@ public void SetStartDate(DateTime start)
17781777
}
17791778
else
17801779
{
1781-
throw new InvalidOperationException("Algorithm.SetStartDate(): Cannot change start date after algorithm initialized.");
1780+
throw new InvalidOperationException(Messages.QCAlgorithm.SetStartDateAlreadyInitialized());
17821781
}
17831782
}
17841783

@@ -1797,7 +1796,7 @@ public void SetEndDate(DateTime end)
17971796
//1. Check not locked already:
17981797
if (_locked)
17991798
{
1800-
throw new InvalidOperationException("Algorithm.SetEndDate(): Cannot change end date after algorithm initialized.");
1799+
throw new InvalidOperationException(Messages.QCAlgorithm.SetEndDateAlreadyInitialized());
18011800
}
18021801

18031802
//Validate:

AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -901,7 +901,7 @@ public void OnMarginCall(List<SubmitOrderRequest> requests)
901901
// If the method does not return or returns a non-iterable PyObject, throw an exception
902902
if (result == null || !result.IsIterable())
903903
{
904-
throw new Exception("OnMarginCall must return a non-empty list of SubmitOrderRequest");
904+
throw new Exception(Messages.AlgorithmPythonWrapper.OnMarginCallMustReturnNonEmptyList());
905905
}
906906

907907
requests.Clear();
@@ -919,7 +919,7 @@ public void OnMarginCall(List<SubmitOrderRequest> requests)
919919
// If the PyObject is an empty list or its items are not SubmitOrderRequest objects, throw an exception
920920
if (requests.Count == 0)
921921
{
922-
throw new Exception("OnMarginCall must return a non-empty list of SubmitOrderRequest");
922+
throw new Exception(Messages.AlgorithmPythonWrapper.OnMarginCallMustReturnNonEmptyList());
923923
}
924924
}
925925
}

Common/Messages/Messages.Algorithm.Framework.Portfolio.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ public static class PortfolioTarget
3939
public static string InvalidTargetPercent(IAlgorithm algorithm, decimal percent)
4040
{
4141
return Invariant($@"The portfolio target percent: {
42-
percent}, does not comply with the current 'Algorithm.Settings' 'MaxAbsolutePortfolioTargetPercentage': {
43-
algorithm.Settings.MaxAbsolutePortfolioTargetPercentage} or 'MinAbsolutePortfolioTargetPercentage': {
42+
percent}, does not comply with the current '{FormatCodeRoot("Settings")}.{FormatCode("MaxAbsolutePortfolioTargetPercentage")}': {
43+
algorithm.Settings.MaxAbsolutePortfolioTargetPercentage} or '{FormatCodeRoot("Settings")}.{FormatCode("MinAbsolutePortfolioTargetPercentage")}': {
4444
algorithm.Settings.MinAbsolutePortfolioTargetPercentage}. Skipping");
4545
}
4646

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System.Runtime.CompilerServices;
17+
18+
namespace QuantConnect
19+
{
20+
/// <summary>
21+
/// Provides user-facing message construction methods and static messages for the <see cref="Algorithm"/> namespace
22+
/// </summary>
23+
public static partial class Messages
24+
{
25+
/// <summary>
26+
/// Provides user-facing messages for the <see cref="Algorithm.QCAlgorithm"/> class and its consumers or related classes
27+
/// </summary>
28+
public static class QCAlgorithm
29+
{
30+
/// <summary>
31+
/// Returns a string message saying the time zone cannot be changed after the algorithm is running
32+
/// </summary>
33+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
34+
public static string SetTimeZoneAlreadyRunning()
35+
{
36+
return $"{AlgorithmPrefix()}.{FormatCode("SetTimeZone")}(): Cannot change time zone after algorithm running.";
37+
}
38+
39+
/// <summary>
40+
/// Returns a string message saying the benchmark cannot be changed after the algorithm is initialized
41+
/// </summary>
42+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
43+
public static string SetBenchmarkAlreadyInitialized()
44+
{
45+
return $"{AlgorithmPrefix()}.{FormatCode("SetBenchmark")}(): Cannot change Benchmark after algorithm initialized.";
46+
}
47+
48+
/// <summary>
49+
/// Returns a string message saying the account currency cannot be changed after the algorithm is initialized
50+
/// </summary>
51+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
52+
public static string SetAccountCurrencyAlreadyInitialized()
53+
{
54+
return $"{AlgorithmPrefix()}.{FormatCode("SetAccountCurrency")}(): Cannot change AccountCurrency after algorithm initialized.";
55+
}
56+
57+
/// <summary>
58+
/// Returns a string message saying the cash cannot be changed after the algorithm is initialized
59+
/// </summary>
60+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
61+
public static string SetCashAlreadyInitialized()
62+
{
63+
return $"{AlgorithmPrefix()}.{FormatCode("SetCash")}(): Cannot change cash available after algorithm initialized.";
64+
}
65+
66+
/// <summary>
67+
/// Returns a string message saying the start date cannot be changed after the algorithm is initialized
68+
/// </summary>
69+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
70+
public static string SetStartDateAlreadyInitialized()
71+
{
72+
return $"{AlgorithmPrefix()}.{FormatCode("SetStartDate")}(): Cannot change start date after algorithm initialized.";
73+
}
74+
75+
/// <summary>
76+
/// Returns a string message saying the end date cannot be changed after the algorithm is initialized
77+
/// </summary>
78+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
79+
public static string SetEndDateAlreadyInitialized()
80+
{
81+
return $"{AlgorithmPrefix()}.{FormatCode("SetEndDate")}(): Cannot change end date after algorithm initialized.";
82+
}
83+
84+
/// <summary>
85+
/// Returns a string message saying SetWarmup cannot be used after the algorithm is initialized
86+
/// </summary>
87+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
88+
public static string SetWarmupAlreadyInitialized()
89+
{
90+
return $"{AlgorithmPrefix()}.{FormatCode("SetWarmup")}(): This method cannot be used after algorithm initialized";
91+
}
92+
}
93+
94+
/// <summary>
95+
/// Provides user-facing messages for the <see cref="AlgorithmFactory.Python.Wrappers.AlgorithmPythonWrapper"/> class
96+
/// and its consumers or related classes
97+
/// </summary>
98+
public static class AlgorithmPythonWrapper
99+
{
100+
/// <summary>
101+
/// Returns a string message saying OnMarginCall must return a non-empty list of SubmitOrderRequest
102+
/// </summary>
103+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
104+
public static string OnMarginCallMustReturnNonEmptyList()
105+
{
106+
return $"{FormatCode("OnMarginCall")} must return a non-empty list of SubmitOrderRequest";
107+
}
108+
}
109+
}
110+
}

Common/Messages/Messages.Brokerages.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ public static string UnsupportedMarketOnOpenOrderTime(
163163
in TimeOnly windowStart,
164164
in TimeOnly windowEnd)
165165
{
166-
return Invariant($"MarketOnOpen submission time is invalid. Valid local times are {windowStart: hh\\:mm}{windowEnd: hh\\:mm}. Consider setting DailyPreciseEndTime = false or using {nameof(Schedule)}.{nameof(Schedule.On)}.");
166+
return Invariant($"MarketOnOpen submission time is invalid. Valid local times are {windowStart: hh\\:mm}{windowEnd: hh\\:mm}. Consider setting {FormatCode(nameof(AlgorithmSettings.DailyPreciseEndTime))} = false or using {FormatCodeRoot(nameof(Schedule))}.{FormatCode(nameof(Schedule.On))}.");
167167
}
168168
}
169169

@@ -411,7 +411,7 @@ public static class FxcmBrokerageModel
411411
[MethodImpl(MethodImplOptions.AggressiveInlining)]
412412
public static string InvalidOrderQuantityForLotSize(Securities.Security security)
413413
{
414-
return Invariant($"The order quantity must be a multiple of LotSize: [{security.SymbolProperties.LotSize}].");
414+
return Invariant($"The order quantity must be a multiple of {FormatCode("LotSize")}: [{security.SymbolProperties.LotSize}].");
415415
}
416416

417417
/// <summary>

Common/Messages/Messages.Orders.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ public static string ZeroQuantity(Orders.OrderRequest request)
354354
[MethodImpl(MethodImplOptions.AggressiveInlining)]
355355
public static string MissingSecurity(Orders.SubmitOrderRequest request)
356356
{
357-
return Invariant($"You haven't requested {request.Symbol} data. Add this with AddSecurity() in the Initialize() Method.");
357+
return Invariant($"You haven't requested {request.Symbol} data. Add this with {FormatCode("AddSecurity")}() in the {FormatCode("Initialize")}() method.");
358358
}
359359

360360
/// <summary>
@@ -365,8 +365,8 @@ public static string MissingSecurity(Orders.SubmitOrderRequest request)
365365
[MethodImpl(MethodImplOptions.AggressiveInlining)]
366366
public static string WarmingUp(Orders.OrderRequest request)
367367
{
368-
return Invariant($@"This operation is not allowed in Initialize or during warm up: OrderRequest.{
369-
request.OrderRequestType}. Please move this code to the OnWarmupFinished() method.");
368+
return Invariant($@"This operation is not allowed in {FormatCode("Initialize")} or during warm up: OrderRequest.{
369+
FormatCode(request.OrderRequestType)}. Please move this code to the {FormatCode("OnWarmupFinished")}() method.");
370370
}
371371
}
372372

Common/Messages/Messages.QuantConnect.cs

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,48 @@ namespace QuantConnect
3030
/// </summary>
3131
public static partial class Messages
3232
{
33+
private static Language _algorithmLanguage = Language.CSharp;
34+
35+
/// <summary>
36+
/// Sets the algorithm language used to format code identifiers in error messages.
37+
/// </summary>
38+
public static void SetAlgorithmLanguage(Language language)
39+
{
40+
_algorithmLanguage = language;
41+
}
42+
43+
/// <summary>
44+
/// Returns the code identifier formatted for the current algorithm language.
45+
/// For Python, converts PascalCase/camelCase to snake_case.
46+
/// </summary>
47+
private static string FormatCode(string code)
48+
{
49+
return _algorithmLanguage switch
50+
{
51+
Language.Python => code.ToSnakeCase(),
52+
_ => code
53+
};
54+
}
55+
56+
private static string FormatCodeRoot(string code)
57+
{
58+
return _algorithmLanguage switch
59+
{
60+
Language.Python => "self." + code.ToSnakeCase(),
61+
_ => code
62+
};
63+
}
64+
65+
private static string FormatCode<T>(T value) where T : Enum
66+
{
67+
return FormatCode(value.ToString());
68+
}
69+
70+
private static string AlgorithmPrefix()
71+
{
72+
return _algorithmLanguage == Language.Python ? "self" : "QCAlgorithm";
73+
}
74+
3375
/// <summary>
3476
/// Provides user-facing messages for the <see cref="AlphaRuntimeStatistics"/> class and its consumers or related classes
3577
/// </summary>
@@ -187,8 +229,8 @@ public static string RemoveInvalidOperation<TKey, TValue>(ExtendedDictionary<TKe
187229
public static string TickerNotFoundInSymbolCache(string ticker)
188230
{
189231
return $"The ticker {ticker} was not found in the SymbolCache. Use the Symbol object as key instead. " +
190-
"Accessing the securities collection/slice object by string ticker is only available for securities added with " +
191-
"the AddSecurity-family methods. For more details, please check out the documentation.";
232+
$"Accessing the securities collection/slice object by string ticker is only available for securities added with " +
233+
$"the {FormatCode("AddSecurity")}-family methods. For more details, please check out the documentation.";
192234
}
193235

194236
/// <summary>
@@ -282,7 +324,7 @@ public static string DownloadDataFailed(string url)
282324
public static string ZeroPriceForSecurity(QuantConnect.Symbol symbol)
283325
{
284326
return $"{symbol}: The security does not have an accurate price as it has not yet received a bar of data. " +
285-
"Before placing a trade (or using SetHoldings) warm up your algorithm with SetWarmup, or use slice.Contains(symbol) " +
327+
$"Before placing a trade (or using {FormatCode("SetHoldings")}) warm up your algorithm with {FormatCode("SetWarmup")}, or use slice.{FormatCode("Contains")}(symbol) " +
286328
"to confirm the Slice object has price before using the data. Data does not necessarily all arrive at the same " +
287329
"time so your algorithm should confirm the data is ready before using it. In live trading this can mean you do " +
288330
"not have an active subscription to the asset class you're trying to trade. If using custom data make sure you've " +
@@ -724,7 +766,7 @@ public static string ErrorParsingSecurityIdentifier(string value, Exception exce
724766
[MethodImpl(MethodImplOptions.AggressiveInlining)]
725767
public static string MarketNotFound(string market)
726768
{
727-
return $@"The specified market wasn't found in the markets lookup. Requested: {market}. You can add markets by calling QuantConnect.Market.Add(string,int)";
769+
return $@"The specified market wasn't found in the markets lookup. Requested: {market}. You can add markets by calling QuantConnect.Market.{FormatCode("Add")}(string,int)";
728770
}
729771
}
730772

0 commit comments

Comments
 (0)