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
11 changes: 7 additions & 4 deletions Common/Orders/BloombergFixOrderProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
* limitations under the License.
*/

using System.Collections.Generic;

namespace QuantConnect.Orders
{
/// <summary>
Expand All @@ -28,7 +26,7 @@ public class BloombergFixOrderProperties : FixOrderProperties
/// </summary>
public string LocateBroker
{
get { return AdditionalProperties?.GetValueOrDefault("5700"); }
get { return GetTag("5700"); }
set { SetTag("5700", value); }
}

Expand All @@ -38,10 +36,15 @@ public string LocateBroker
/// </summary>
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)
Expand Down
9 changes: 6 additions & 3 deletions Common/Orders/FixOrderProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* limitations under the License.
*/

using System.Collections.Generic;
using Common.Util;
using QuantConnect.Interfaces;

namespace QuantConnect.Orders
Expand All @@ -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"
/// </summary>
public Dictionary<string, string> AdditionalProperties { get; set; } = [];
/// <remarks>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()</remarks>
public BaseExtendedDictionary<string, string> AdditionalProperties { get; set; } = [];

/// <summary>
/// Instruction for order handling on Broker floor
Expand Down Expand Up @@ -60,7 +63,7 @@ public class FixOrderProperties : OrderProperties
public override IOrderProperties Clone()
{
var clone = (FixOrderProperties)MemberwiseClone();
clone.AdditionalProperties = new Dictionary<string, string>(AdditionalProperties);
clone.AdditionalProperties = new BaseExtendedDictionary<string, string>(AdditionalProperties);
return clone;
}
}
Expand Down
47 changes: 47 additions & 0 deletions Common/Orders/TerminalLinkOrderProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/

using System.Collections.Generic;
using Common.Util;
using QuantConnect.Interfaces;

namespace QuantConnect.Orders
{
Expand All @@ -23,6 +25,15 @@ namespace QuantConnect.Orders
/// </summary>
public class TerminalLinkOrderProperties : OrderProperties
{
/// <summary>
/// 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"
/// </summary>
/// <remarks>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()</remarks>
public BaseExtendedDictionary<string, string> AdditionalProperties { get; set; } = [];

/// <summary>
/// The EMSX Instructions is the free form instructions that may be sent to the broker
/// </summary>
Expand Down Expand Up @@ -88,6 +99,17 @@ public class TerminalLinkOrderProperties : OrderProperties
/// </summary>
public string LocateId { get; set; }

/// <summary>
/// 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.
/// </summary>
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); }
}

/// <summary>
/// The EMSX order strategy details.
/// Strategy parameters must be appended in the correct order as expected by EMSX.
Expand All @@ -105,6 +127,31 @@ public class TerminalLinkOrderProperties : OrderProperties
/// <remarks>Has precedence over <see cref="AutomaticPositionSides"/></remarks>
public OrderPosition? PositionSide { get; set; }

/// <summary>
/// Returns a new instance clone of this object
/// </summary>
/// <remarks>Deep copies <see cref="AdditionalProperties"/> so edits on the clone, e.g. the
/// locate cleanup in BrokerageExtensions.RemoveLocateFromNonShortOrder, never reach the
/// instance the algorithm holds on to</remarks>
public override IOrderProperties Clone()
{
var clone = (TerminalLinkOrderProperties)MemberwiseClone();
clone.AdditionalProperties = new BaseExtendedDictionary<string, string>(AdditionalProperties);
return clone;
}

private void SetTag(string tag, string value)
{
if (value == null)
{
AdditionalProperties?.Remove(tag);
}
else
{
AdditionalProperties[tag] = value;
}
}

/// <summary>
/// Models an EMSX order strategy parameter
/// </summary>
Expand Down
44 changes: 44 additions & 0 deletions Tests/Common/Orders/FixOrderPropertiesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Interfaces;
using QuantConnect.Orders;

Expand All @@ -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);
}
}
}
}
177 changes: 177 additions & 0 deletions Tests/Common/Orders/TerminalLinkOrderPropertiesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* limitations under the License.
*/

using System;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Orders;
Expand Down Expand Up @@ -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<string, string>, 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<PythonException>(() => 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);
}
}
}
}
Loading
Loading