Skip to content

Commit 38a78c4

Browse files
authored
Add arithmetic operators to numeric MultiPeriodField classes (#9571)
* Add arithmetic operators to numeric MultiPeriodField classes Implement +, -, *, / and % operators on the numeric MultiPeriodField and MultiPeriodFieldLong base classes so fundamental fields can be combined arithmetically. Operators act on each field's default-period Value and return decimal, consistent with the existing implicit decimal conversion. Includes unit tests covering field-to-field and field-to-scalar arithmetic for both base classes. * Add cross-type operators between double and long backed fields Support arithmetic between the double-backed MultiPeriodField and the long-backed MultiPeriodFieldLong by adding the mixed-type operator overloads (both orderings) for +, -, *, / and %. Includes a unit test covering double/long arithmetic in both directions.
1 parent b5b7b64 commit 38a78c4

2 files changed

Lines changed: 231 additions & 0 deletions

File tree

Common/Data/Fundamental/MultiPeriodField.cs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,83 @@ public static implicit operator decimal(MultiPeriodField instance)
191191
{
192192
return (decimal)instance.Value;
193193
}
194+
195+
#pragma warning disable CA2225 // Operator overloads have named alternates
196+
/// <summary>
197+
/// Adds the default values of two fields
198+
/// </summary>
199+
public static decimal operator +(MultiPeriodField left, MultiPeriodField right) => (decimal)left.Value + (decimal)right.Value;
200+
201+
/// <summary>
202+
/// Subtracts the right field's default value from the left field's default value
203+
/// </summary>
204+
public static decimal operator -(MultiPeriodField left, MultiPeriodField right) => (decimal)left.Value - (decimal)right.Value;
205+
206+
/// <summary>
207+
/// Multiplies the default values of two fields
208+
/// </summary>
209+
public static decimal operator *(MultiPeriodField left, MultiPeriodField right) => (decimal)left.Value * (decimal)right.Value;
210+
211+
/// <summary>
212+
/// Divides the left field's default value by the right field's default value
213+
/// </summary>
214+
public static decimal operator /(MultiPeriodField left, MultiPeriodField right) => (decimal)left.Value / (decimal)right.Value;
215+
216+
/// <summary>
217+
/// Computes the remainder of dividing the left field's default value by the right field's default value
218+
/// </summary>
219+
public static decimal operator %(MultiPeriodField left, MultiPeriodField right) => (decimal)left.Value % (decimal)right.Value;
220+
221+
/// <summary>
222+
/// Adds the default values of a double and a long backed field
223+
/// </summary>
224+
public static decimal operator +(MultiPeriodField left, MultiPeriodFieldLong right) => (decimal)left.Value + right.Value;
225+
226+
/// <summary>
227+
/// Adds the default values of a long and a double backed field
228+
/// </summary>
229+
public static decimal operator +(MultiPeriodFieldLong left, MultiPeriodField right) => left.Value + (decimal)right.Value;
230+
231+
/// <summary>
232+
/// Subtracts the right field's default value from the left field's default value
233+
/// </summary>
234+
public static decimal operator -(MultiPeriodField left, MultiPeriodFieldLong right) => (decimal)left.Value - right.Value;
235+
236+
/// <summary>
237+
/// Subtracts the right field's default value from the left field's default value
238+
/// </summary>
239+
public static decimal operator -(MultiPeriodFieldLong left, MultiPeriodField right) => left.Value - (decimal)right.Value;
240+
241+
/// <summary>
242+
/// Multiplies the default values of a double and a long backed field
243+
/// </summary>
244+
public static decimal operator *(MultiPeriodField left, MultiPeriodFieldLong right) => (decimal)left.Value * right.Value;
245+
246+
/// <summary>
247+
/// Multiplies the default values of a long and a double backed field
248+
/// </summary>
249+
public static decimal operator *(MultiPeriodFieldLong left, MultiPeriodField right) => left.Value * (decimal)right.Value;
250+
251+
/// <summary>
252+
/// Divides the left field's default value by the right field's default value
253+
/// </summary>
254+
public static decimal operator /(MultiPeriodField left, MultiPeriodFieldLong right) => (decimal)left.Value / right.Value;
255+
256+
/// <summary>
257+
/// Divides the left field's default value by the right field's default value
258+
/// </summary>
259+
public static decimal operator /(MultiPeriodFieldLong left, MultiPeriodField right) => left.Value / (decimal)right.Value;
260+
261+
/// <summary>
262+
/// Computes the remainder of dividing the left field's default value by the right field's default value
263+
/// </summary>
264+
public static decimal operator %(MultiPeriodField left, MultiPeriodFieldLong right) => (decimal)left.Value % right.Value;
265+
266+
/// <summary>
267+
/// Computes the remainder of dividing the left field's default value by the right field's default value
268+
/// </summary>
269+
public static decimal operator %(MultiPeriodFieldLong left, MultiPeriodField right) => left.Value % (decimal)right.Value;
270+
#pragma warning restore CA2225 // Operator overloads have named alternates
194271
}
195272

196273
/// <summary>
@@ -221,5 +298,32 @@ public static implicit operator decimal(MultiPeriodFieldLong instance)
221298
{
222299
return (decimal)instance.Value;
223300
}
301+
302+
#pragma warning disable CA2225 // Operator overloads have named alternates
303+
/// <summary>
304+
/// Adds the default values of two fields
305+
/// </summary>
306+
public static decimal operator +(MultiPeriodFieldLong left, MultiPeriodFieldLong right) => (decimal)left.Value + right.Value;
307+
308+
/// <summary>
309+
/// Subtracts the right field's default value from the left field's default value
310+
/// </summary>
311+
public static decimal operator -(MultiPeriodFieldLong left, MultiPeriodFieldLong right) => (decimal)left.Value - right.Value;
312+
313+
/// <summary>
314+
/// Multiplies the default values of two fields
315+
/// </summary>
316+
public static decimal operator *(MultiPeriodFieldLong left, MultiPeriodFieldLong right) => (decimal)left.Value * right.Value;
317+
318+
/// <summary>
319+
/// Divides the left field's default value by the right field's default value
320+
/// </summary>
321+
public static decimal operator /(MultiPeriodFieldLong left, MultiPeriodFieldLong right) => (decimal)left.Value / right.Value;
322+
323+
/// <summary>
324+
/// Computes the remainder of dividing the left field's default value by the right field's default value
325+
/// </summary>
326+
public static decimal operator %(MultiPeriodFieldLong left, MultiPeriodFieldLong right) => (decimal)left.Value % right.Value;
327+
#pragma warning restore CA2225 // Operator overloads have named alternates
224328
}
225329
}

Tests/Common/Data/Fundamental/MultiPeriodFieldTests.cs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,86 @@ public void EmptyStoreToString()
109109
Assert.AreEqual("", field.ToString());
110110
}
111111

112+
[Test]
113+
public void ArithmeticOperatorsBetweenDoubleFields()
114+
{
115+
var left = new TestMultiPeriodField();
116+
left.OneYear = 10;
117+
var right = new TestMultiPeriodField();
118+
right.OneYear = 4;
119+
120+
Assert.AreEqual(14m, left + right);
121+
Assert.AreEqual(6m, left - right);
122+
Assert.AreEqual(40m, left * right);
123+
Assert.AreEqual(2.5m, left / right);
124+
Assert.AreEqual(2m, left % right);
125+
}
126+
127+
[Test]
128+
public void ArithmeticOperatorsBetweenLongFields()
129+
{
130+
var left = new TestMultiPeriodFieldLong();
131+
left.OneYear = 10;
132+
var right = new TestMultiPeriodFieldLong();
133+
right.OneYear = 4;
134+
135+
Assert.AreEqual(14m, left + right);
136+
Assert.AreEqual(6m, left - right);
137+
Assert.AreEqual(40m, left * right);
138+
Assert.AreEqual(2.5m, left / right);
139+
Assert.AreEqual(2m, left % right);
140+
}
141+
142+
[Test]
143+
public void ArithmeticOperatorsBetweenDoubleAndLongFields()
144+
{
145+
var doubleField = new TestMultiPeriodField();
146+
doubleField.OneYear = 10;
147+
var longField = new TestMultiPeriodFieldLong();
148+
longField.OneYear = 4;
149+
150+
// double on the left, long on the right
151+
Assert.AreEqual(14m, doubleField + longField);
152+
Assert.AreEqual(6m, doubleField - longField);
153+
Assert.AreEqual(40m, doubleField * longField);
154+
Assert.AreEqual(2.5m, doubleField / longField);
155+
Assert.AreEqual(2m, doubleField % longField);
156+
157+
// long on the left, double on the right
158+
Assert.AreEqual(14m, longField + doubleField);
159+
Assert.AreEqual(-6m, longField - doubleField);
160+
Assert.AreEqual(40m, longField * doubleField);
161+
Assert.AreEqual(0.4m, longField / doubleField);
162+
Assert.AreEqual(4m, longField % doubleField);
163+
}
164+
165+
[Test]
166+
public void ArithmeticOperatorsWithScalar()
167+
{
168+
var field = new TestMultiPeriodField();
169+
field.OneYear = 10;
170+
171+
// field implicitly converts to decimal, the built-in decimal operators apply
172+
Assert.AreEqual(13m, field + 3m);
173+
Assert.AreEqual(7m, field - 3m);
174+
Assert.AreEqual(30m, field * 3m);
175+
Assert.AreEqual(5m, field / 2m);
176+
Assert.AreEqual(1m, field % 3m);
177+
}
178+
179+
[Test]
180+
public void ArithmeticOperatorsUseDefaultPeriodValue()
181+
{
182+
// No default period value available, falls back to first available period (3M = 1)
183+
var left = new TestMultiPeriodField();
184+
left.ThreeMonths = 1;
185+
var right = new TestMultiPeriodField();
186+
right.ThreeMonths = 1;
187+
188+
Assert.IsFalse(left.HasValue);
189+
Assert.AreEqual(2m, left + right);
190+
}
191+
112192
private class TestMultiPeriodField : MultiPeriodField
113193
{
114194
protected override string DefaultPeriod => "OneYear";
@@ -161,5 +241,52 @@ public override IReadOnlyDictionary<string, double> GetPeriodValues()
161241
return result;
162242
}
163243
}
244+
245+
private class TestMultiPeriodFieldLong : MultiPeriodFieldLong
246+
{
247+
protected override string DefaultPeriod => "OneYear";
248+
249+
public long ThreeMonths { get; set; } = NoValue;
250+
public long OneYear { get; set; } = NoValue;
251+
public override bool HasValue => !BaseFundamentalDataProvider.IsNone(typeof(long), OneYear);
252+
public override long Value
253+
{
254+
get
255+
{
256+
var defaultValue = OneYear;
257+
if (!BaseFundamentalDataProvider.IsNone(typeof(long), defaultValue))
258+
{
259+
return defaultValue;
260+
}
261+
return base.Value;
262+
}
263+
}
264+
265+
public override long GetPeriodValue(string period)
266+
{
267+
switch (period)
268+
{
269+
case QuantConnect.Data.Fundamental.Period.ThreeMonths:
270+
return ThreeMonths;
271+
case QuantConnect.Data.Fundamental.Period.OneYear:
272+
return OneYear;
273+
default:
274+
return NoValue;
275+
}
276+
}
277+
278+
public override IReadOnlyDictionary<string, long> GetPeriodValues()
279+
{
280+
var result = new Dictionary<string, long>();
281+
foreach (var kvp in new[] { new Tuple<string, long>("1Y", OneYear), new Tuple<string, long>("3M", ThreeMonths) })
282+
{
283+
if (!BaseFundamentalDataProvider.IsNone(typeof(long), kvp.Item2))
284+
{
285+
result[kvp.Item1] = kvp.Item2;
286+
}
287+
}
288+
return result;
289+
}
290+
}
164291
}
165292
}

0 commit comments

Comments
 (0)