diff --git a/Common/Orders/BloombergFixOrderProperties.cs b/Common/Orders/BloombergFixOrderProperties.cs
index 7a77f2d7423c..6565bf2259dd 100644
--- a/Common/Orders/BloombergFixOrderProperties.cs
+++ b/Common/Orders/BloombergFixOrderProperties.cs
@@ -13,8 +13,6 @@
* limitations under the License.
*/
-using System.Collections.Generic;
-
namespace QuantConnect.Orders
{
///
@@ -28,7 +26,7 @@ public class BloombergFixOrderProperties : FixOrderProperties
///
public string LocateBroker
{
- get { return AdditionalProperties?.GetValueOrDefault("5700"); }
+ get { return GetTag("5700"); }
set { SetTag("5700", value); }
}
@@ -38,10 +36,15 @@ public string LocateBroker
///
public string LocateReqd
{
- get { return AdditionalProperties?.GetValueOrDefault("114"); }
+ get { return GetTag("114"); }
set { SetTag("114", value); }
}
+ private string GetTag(string tag)
+ {
+ return AdditionalProperties != null && AdditionalProperties.TryGetValue(tag, out var value) ? value : null;
+ }
+
private void SetTag(string tag, string value)
{
if (value == null)
diff --git a/Common/Orders/FixOrderProperties.cs b/Common/Orders/FixOrderProperties.cs
index 077ca5a75c20..1e9cd4770d00 100644
--- a/Common/Orders/FixOrderProperties.cs
+++ b/Common/Orders/FixOrderProperties.cs
@@ -13,7 +13,7 @@
* limitations under the License.
*/
-using System.Collections.Generic;
+using Common.Util;
using QuantConnect.Interfaces;
namespace QuantConnect.Orders
@@ -27,7 +27,10 @@ public class FixOrderProperties : OrderProperties
/// Custom FIX tags to send with the order. The key is the FIX tag number
/// and the value is the tag value, e.g. AdditionalProperties["9301"] = "1"
///
- public Dictionary AdditionalProperties { get; set; } = [];
+ /// Starts empty. Python cannot assign a plain dict to it, since pythonnet has no
+ /// conversion for it; add the entries one by one, bulk load them from a dict with update(),
+ /// and reset with clear()
+ public BaseExtendedDictionary AdditionalProperties { get; set; } = [];
///
/// Instruction for order handling on Broker floor
@@ -60,7 +63,7 @@ public class FixOrderProperties : OrderProperties
public override IOrderProperties Clone()
{
var clone = (FixOrderProperties)MemberwiseClone();
- clone.AdditionalProperties = new Dictionary(AdditionalProperties);
+ clone.AdditionalProperties = new BaseExtendedDictionary(AdditionalProperties);
return clone;
}
}
diff --git a/Common/Orders/TerminalLinkOrderProperties.cs b/Common/Orders/TerminalLinkOrderProperties.cs
index 333133dc0fa0..db0a52dffb1b 100644
--- a/Common/Orders/TerminalLinkOrderProperties.cs
+++ b/Common/Orders/TerminalLinkOrderProperties.cs
@@ -15,6 +15,8 @@
*/
using System.Collections.Generic;
+using Common.Util;
+using QuantConnect.Interfaces;
namespace QuantConnect.Orders
{
@@ -23,6 +25,15 @@ namespace QuantConnect.Orders
///
public class TerminalLinkOrderProperties : OrderProperties
{
+ ///
+ /// Custom EMSX fields to send with the order. The key is the EMSX element name
+ /// and the value is the element value, e.g. AdditionalProperties["EMSX_CFD_FLAG"] = "1"
+ ///
+ /// Starts empty. Python cannot assign a plain dict to it, since pythonnet has no
+ /// conversion for it; add the entries one by one, bulk load them from a dict with update(),
+ /// and reset with clear()
+ public BaseExtendedDictionary AdditionalProperties { get; set; } = [];
+
///
/// The EMSX Instructions is the free form instructions that may be sent to the broker
///
@@ -88,6 +99,17 @@ public class TerminalLinkOrderProperties : OrderProperties
///
public string LocateId { get; set; }
+ ///
+ /// Indicates if the order is a contract for differences (CFD) trade (EMSX_CFD_FLAG).
+ /// This field is applicable to trades on an order level, and does not populate on a per
+ /// security basis.
+ ///
+ public bool IsCfdTrade
+ {
+ get { return AdditionalProperties != null && AdditionalProperties.TryGetValue("EMSX_CFD_FLAG", out var flag) && flag == "1"; }
+ set { SetTag("EMSX_CFD_FLAG", value ? "1" : null); }
+ }
+
///
/// The EMSX order strategy details.
/// Strategy parameters must be appended in the correct order as expected by EMSX.
@@ -105,6 +127,31 @@ public class TerminalLinkOrderProperties : OrderProperties
/// Has precedence over
public OrderPosition? PositionSide { get; set; }
+ ///
+ /// Returns a new instance clone of this object
+ ///
+ /// Deep copies so edits on the clone, e.g. the
+ /// locate cleanup in BrokerageExtensions.RemoveLocateFromNonShortOrder, never reach the
+ /// instance the algorithm holds on to
+ public override IOrderProperties Clone()
+ {
+ var clone = (TerminalLinkOrderProperties)MemberwiseClone();
+ clone.AdditionalProperties = new BaseExtendedDictionary(AdditionalProperties);
+ return clone;
+ }
+
+ private void SetTag(string tag, string value)
+ {
+ if (value == null)
+ {
+ AdditionalProperties?.Remove(tag);
+ }
+ else
+ {
+ AdditionalProperties[tag] = value;
+ }
+ }
+
///
/// Models an EMSX order strategy parameter
///
diff --git a/Tests/Common/Orders/FixOrderPropertiesTests.cs b/Tests/Common/Orders/FixOrderPropertiesTests.cs
index 9105fe89db3d..6991a5844c40 100644
--- a/Tests/Common/Orders/FixOrderPropertiesTests.cs
+++ b/Tests/Common/Orders/FixOrderPropertiesTests.cs
@@ -14,6 +14,7 @@
*/
using NUnit.Framework;
+using Python.Runtime;
using QuantConnect.Interfaces;
using QuantConnect.Orders;
@@ -37,5 +38,48 @@ public void BloombergFixOrderPropertiesSupportsAdditionalPropertiesAndClone()
properties.AdditionalProperties["9301"] = "2";
Assert.AreEqual("1", clone.AdditionalProperties["9301"]);
}
+
+ [Test]
+ public void LocateTagPassthroughsSurviveTheDictionarySwap()
+ {
+ // The tags are the store behind the properties, so writing either way must be visible
+ // from the other.
+ var properties = new BloombergFixOrderProperties { LocateBroker = "MLCO" };
+ Assert.AreEqual("MLCO", properties.AdditionalProperties["5700"]);
+
+ properties.AdditionalProperties["114"] = "Y";
+ Assert.AreEqual("Y", properties.LocateReqd);
+
+ properties.LocateBroker = null;
+ Assert.IsFalse(properties.AdditionalProperties.ContainsKey("5700"));
+ Assert.IsNull(properties.LocateBroker);
+ }
+
+ [Test]
+ public void UpdatesAdditionalPropertiesFromPlainPythonDictionary()
+ {
+ using (Py.GIL())
+ {
+ // pythonnet cannot convert a plain dict for assignment, so update() is what lets a
+ // Python algorithm bulk load its custom tags.
+ var module = PyModule.FromString("fixAdditionalPropertiesModule",
+ @"
+from AlgorithmImports import *
+
+def getOrderProperties() -> BloombergFixOrderProperties:
+ properties = BloombergFixOrderProperties()
+ properties.additional_properties.update({ ""5700"": ""MLCO"", ""9301"": ""1"" })
+ return properties
+");
+
+ dynamic getOrderProperties = module.GetAttr("getOrderProperties");
+ var properties = (BloombergFixOrderProperties)getOrderProperties();
+
+ Assert.IsNotNull(properties);
+ Assert.AreEqual(2, properties.AdditionalProperties.Count);
+ Assert.AreEqual("1", properties.AdditionalProperties["9301"]);
+ Assert.AreEqual("MLCO", properties.LocateBroker);
+ }
+ }
}
}
diff --git a/Tests/Common/Orders/TerminalLinkOrderPropertiesTests.cs b/Tests/Common/Orders/TerminalLinkOrderPropertiesTests.cs
index df86a02882d1..e3ea4e325d50 100644
--- a/Tests/Common/Orders/TerminalLinkOrderPropertiesTests.cs
+++ b/Tests/Common/Orders/TerminalLinkOrderPropertiesTests.cs
@@ -13,6 +13,7 @@
* limitations under the License.
*/
+using System;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Orders;
@@ -100,5 +101,181 @@ def getOrderProperties() -> TerminalLinkOrderProperties:
Assert.AreEqual("LOC-123", properties.LocateId);
}
}
+
+ [Test]
+ public void IsCfdTradeDefaultsToFalse()
+ {
+ // A regular trade is the EMSX default, and the value for which the brokerage sends no
+ // EMSX_CFD_FLAG at all, so it must be what an untouched instance reports.
+ var properties = new TerminalLinkOrderProperties();
+ Assert.IsFalse(properties.IsCfdTrade);
+ }
+
+ [Test]
+ public void CloneDoesNotShareAdditionalProperties()
+ {
+ // Order properties are reused across orders and cloned before being edited, e.g. by
+ // BrokerageExtensions.RemoveLocateFromNonShortOrder; a shared dictionary would let an
+ // edit on the copy leak back into the caller's instance.
+ var properties = new TerminalLinkOrderProperties { IsCfdTrade = true };
+
+ var clone = (TerminalLinkOrderProperties)properties.Clone();
+ clone.IsCfdTrade = false;
+
+ Assert.IsTrue(properties.IsCfdTrade);
+ }
+
+ [Test]
+ public void SetsIsCfdTradeFromPython()
+ {
+ using (Py.GIL())
+ {
+ var module = PyModule.FromString("cfdTradeModule",
+ @"
+from AlgorithmImports import *
+
+def getOrderProperties() -> TerminalLinkOrderProperties:
+ properties = TerminalLinkOrderProperties()
+ properties.is_cfd_trade = True
+ return properties
+");
+
+ dynamic getOrderProperties = module.GetAttr("getOrderProperties");
+ var properties = (TerminalLinkOrderProperties)getOrderProperties();
+
+ Assert.IsNotNull(properties);
+ Assert.IsTrue(properties.IsCfdTrade);
+ }
+ }
+
+ [Test]
+ public void SetsAdditionalPropertiesEntryFromPython()
+ {
+ using (Py.GIL())
+ {
+ var module = PyModule.FromString("additionalPropertiesEntryModule",
+ @"
+from AlgorithmImports import *
+
+def getOrderProperties() -> TerminalLinkOrderProperties:
+ properties = TerminalLinkOrderProperties()
+ properties.additional_properties[""EMSX_CFD_FLAG""] = ""1""
+ properties.additional_properties[""EMSX_ODD_LOT""] = ""0""
+ return properties
+");
+
+ dynamic getOrderProperties = module.GetAttr("getOrderProperties");
+ var properties = (TerminalLinkOrderProperties)getOrderProperties();
+
+ Assert.IsNotNull(properties);
+ Assert.AreEqual(2, properties.AdditionalProperties.Count);
+ Assert.AreEqual("0", properties.AdditionalProperties["EMSX_ODD_LOT"]);
+ // an entry written through the dictionary is visible on the typed property
+ Assert.IsTrue(properties.IsCfdTrade);
+ }
+ }
+
+ [Test]
+ public void ClearsAdditionalPropertiesFromPython()
+ {
+ using (Py.GIL())
+ {
+ // clear() resets the dictionary, taking the typed properties reading from it back to
+ // their defaults.
+ var module = PyModule.FromString("additionalPropertiesClearModule",
+ @"
+from AlgorithmImports import *
+
+def getOrderProperties() -> TerminalLinkOrderProperties:
+ properties = TerminalLinkOrderProperties()
+ properties.is_cfd_trade = True
+ properties.additional_properties[""EMSX_ODD_LOT""] = ""0""
+ properties.additional_properties.clear()
+ return properties
+");
+
+ dynamic getOrderProperties = module.GetAttr("getOrderProperties");
+ var properties = (TerminalLinkOrderProperties)getOrderProperties();
+
+ Assert.IsNotNull(properties);
+ Assert.IsEmpty(properties.AdditionalProperties);
+ Assert.IsFalse(properties.IsCfdTrade);
+ }
+ }
+
+ [Test]
+ public void UpdatesAdditionalPropertiesFromPlainPythonDictionary()
+ {
+ using (Py.GIL())
+ {
+ // update() is the way to bulk load from a plain Python dict; it takes a PyObject, so
+ // it sidesteps the conversion that plain assignment cannot do.
+ var module = PyModule.FromString("additionalPropertiesUpdateModule",
+ @"
+from AlgorithmImports import *
+
+def getOrderProperties() -> TerminalLinkOrderProperties:
+ properties = TerminalLinkOrderProperties()
+ properties.additional_properties.update({ ""EMSX_CFD_FLAG"": ""1"", ""EMSX_ODD_LOT"": ""0"" })
+ return properties
+");
+
+ dynamic getOrderProperties = module.GetAttr("getOrderProperties");
+ var properties = (TerminalLinkOrderProperties)getOrderProperties();
+
+ Assert.IsNotNull(properties);
+ Assert.AreEqual(2, properties.AdditionalProperties.Count);
+ Assert.AreEqual("0", properties.AdditionalProperties["EMSX_ODD_LOT"]);
+ Assert.IsTrue(properties.IsCfdTrade);
+ }
+ }
+
+ [Test]
+ public void AssigningPlainPythonDictionaryToAdditionalPropertiesThrows()
+ {
+ using (Py.GIL())
+ {
+ // pythonnet has no conversion from a Python dict to Dictionary, so
+ // the natural looking assignment fails at runtime; the entries have to be added to
+ // the dictionary the properties already own.
+ var module = PyModule.FromString("additionalPropertiesReplacementModule",
+ @"
+from AlgorithmImports import *
+
+def getOrderProperties() -> TerminalLinkOrderProperties:
+ properties = TerminalLinkOrderProperties()
+ properties.additional_properties = { ""EMSX_CFD_FLAG"": ""1"" }
+ return properties
+");
+
+ dynamic getOrderProperties = module.GetAttr("getOrderProperties");
+
+ var exception = Assert.Throws(() => getOrderProperties());
+ Assert.IsTrue(exception.Message.Contains("cannot be converted", StringComparison.InvariantCulture),
+ $"Expected a conversion failure, got: {exception.Message}");
+ }
+ }
+
+ [Test]
+ public void ReadsAdditionalPropertiesEntryWrittenByTypedPropertyFromPython()
+ {
+ using (Py.GIL())
+ {
+ var module = PyModule.FromString("additionalPropertiesReadModule",
+ @"
+from AlgorithmImports import *
+
+def getCfdFlag() -> str:
+ properties = TerminalLinkOrderProperties()
+ properties.is_cfd_trade = True
+ return properties.additional_properties[""EMSX_CFD_FLAG""]
+");
+
+ dynamic getCfdFlag = module.GetAttr("getCfdFlag");
+ var flag = (string)getCfdFlag();
+
+ Assert.AreEqual("1", flag);
+ }
+ }
}
}
diff --git a/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs b/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs
index 35cf8275e617..a7c22d903ec1 100644
--- a/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs
+++ b/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs
@@ -396,11 +396,17 @@ private static (string LocateBroker, string LocateId, string LocateBrokerTag, st
{
TerminalLinkOrderProperties terminalLink => (terminalLink.LocateBroker, terminalLink.LocateId, null, null),
WolverineOrderProperties wolverine => (wolverine.LocateBroker, null, null, null),
- FixOrderProperties fix => (null, null, fix.AdditionalProperties.GetValueOrDefault("5700"), fix.AdditionalProperties.GetValueOrDefault("114")),
+ FixOrderProperties fix => (null, null, GetTag(fix, "5700"), GetTag(fix, "114")),
_ => default
};
}
+ private static string GetTag(FixOrderProperties properties, string tag)
+ {
+ properties.AdditionalProperties.TryGetValue(tag, out var value);
+ return value;
+ }
+
private static BloombergFixOrderProperties CreateLocateBrokerTagProperties()
{
// the property is a passthrough of the 5700 tag in AdditionalProperties