diff --git a/Algorithm.CSharp/CallbackCommandRegressionAlgorithm.cs b/Algorithm.CSharp/CallbackCommandRegressionAlgorithm.cs
new file mode 100644
index 000000000000..db31b1a8dcdd
--- /dev/null
+++ b/Algorithm.CSharp/CallbackCommandRegressionAlgorithm.cs
@@ -0,0 +1,147 @@
+/*
+ * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+ * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+using System;
+using QuantConnect.Commands;
+using QuantConnect.Interfaces;
+using System.Collections.Generic;
+
+namespace QuantConnect.Algorithm.CSharp
+{
+ ///
+ /// Regression algorithm asserting the behavior of different callback commands call
+ ///
+ public class CallbackCommandRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
+ {
+ ///
+ /// Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.
+ ///
+ public override void Initialize()
+ {
+ SetStartDate(2013, 10, 07);
+ SetEndDate(2013, 10, 11);
+
+ AddEquity("SPY");
+ AddEquity("BAC");
+ AddEquity("IBM");
+ AddCommand();
+ AddCommand();
+ }
+
+ ///
+ /// Handle generic command callback
+ ///
+ public override bool? OnCommand(dynamic data)
+ {
+ Buy(data.Symbol, data.parameters["quantity"]);
+ return true;
+ }
+
+ private class VoidCommand : Command
+ {
+ public DateTime TargetTime { get; set; }
+ public string[] Target { get; set; }
+ public decimal Quantity { get; set; }
+ public Dictionary Parameters { get; set; }
+ public override bool? Run(IAlgorithm algorithm)
+ {
+ if (TargetTime != algorithm.Time)
+ {
+ return null;
+ }
+
+ ((QCAlgorithm)algorithm).Order(Target[0], Quantity, tag: Parameters["tag"]);
+ return null;
+ }
+ }
+ private class BoolCommand : Command
+ {
+ public bool? Result { get; set; }
+ public override bool? Run(IAlgorithm algorithm)
+ {
+ var shouldTrade = MyCustomMethod();
+ if (shouldTrade.HasValue && shouldTrade.Value)
+ {
+ ((QCAlgorithm)algorithm).Buy("IBM", 1);
+ }
+ return shouldTrade;
+ }
+
+ private bool? MyCustomMethod()
+ {
+ return Result;
+ }
+ }
+
+ ///
+ /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
+ ///
+ public bool CanRunLocally { get; }
+
+ ///
+ /// This is used by the regression test system to indicate which languages this algorithm is written in.
+ ///
+ public List Languages { get; } = new() { Language.CSharp, Language.Python };
+
+ ///
+ /// Data Points count of all timeslices of algorithm
+ ///
+ public long DataPoints => 3943;
+
+ ///
+ /// Data Points count of the algorithm history
+ ///
+ public int AlgorithmHistoryDataPoints => 0;
+
+ ///
+ /// Final status of the algorithm
+ ///
+ public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
+
+ ///
+ /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
+ ///
+ public Dictionary ExpectedStatistics => new Dictionary
+ {
+ {"Total Orders", "1"},
+ {"Average Win", "0%"},
+ {"Average Loss", "0%"},
+ {"Compounding Annual Return", "271.453%"},
+ {"Drawdown", "2.200%"},
+ {"Expectancy", "0"},
+ {"Start Equity", "100000"},
+ {"End Equity", "101691.92"},
+ {"Net Profit", "1.692%"},
+ {"Sharpe Ratio", "8.854"},
+ {"Sortino Ratio", "0"},
+ {"Probabilistic Sharpe Ratio", "67.609%"},
+ {"Loss Rate", "0%"},
+ {"Win Rate", "0%"},
+ {"Profit-Loss Ratio", "0"},
+ {"Alpha", "-0.005"},
+ {"Beta", "0.996"},
+ {"Annual Standard Deviation", "0.222"},
+ {"Annual Variance", "0.049"},
+ {"Information Ratio", "-14.565"},
+ {"Tracking Error", "0.001"},
+ {"Treynor Ratio", "1.97"},
+ {"Total Fees", "$3.44"},
+ {"Estimated Strategy Capacity", "$56000000.00"},
+ {"Lowest Capacity Asset", "SPY R735QTJ8XC9X"},
+ {"Portfolio Turnover", "19.93%"},
+ {"OrderListHash", "3da9fa60bf95b9ed148b95e02e0cfc9e"}
+ };
+ }
+}
diff --git a/Algorithm.Python/CallbackCommandRegressionAlgorithm.py b/Algorithm.Python/CallbackCommandRegressionAlgorithm.py
new file mode 100644
index 000000000000..2d495bb38b3f
--- /dev/null
+++ b/Algorithm.Python/CallbackCommandRegressionAlgorithm.py
@@ -0,0 +1,73 @@
+# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from AlgorithmImports import *
+
+class InvalidCommand():
+ variable = 10
+
+class VoidCommand():
+ quantity = 0
+ target = []
+ parameters = {}
+ targettime = None
+
+ def run(self, algo: QCAlgorithm) -> bool | None:
+ if not self.targettime or self.targettime != algo.time:
+ return
+ tag = self.parameters["tag"]
+ algo.order(self.target[0], self.get_quantity(), tag=tag)
+
+ def get_quantity(self):
+ return self.quantity
+
+class BoolCommand(Command):
+ result = False
+
+ def run(self, algo: QCAlgorithm) -> bool | None:
+ trade_ibm = self.my_custom_method()
+ if trade_ibm:
+ algo.buy("IBM", 1)
+ return trade_ibm
+
+ def my_custom_method(self):
+ return self.result
+
+###
+### Regression algorithm asserting the behavior of different callback commands call
+###
+class CallbackCommandRegressionAlgorithm(QCAlgorithm):
+ def initialize(self):
+ '''Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.'''
+
+ self.set_start_date(2013, 10, 7)
+ self.set_end_date(2013, 10, 11)
+
+ self.add_equity("SPY")
+ self.add_equity("IBM")
+ self.add_equity("BAC")
+
+ self.add_command(VoidCommand)
+ self.add_command(BoolCommand)
+
+ threw_exception = False
+ try:
+ self.add_command(InvalidCommand)
+ except:
+ threw_exception = True
+ if not threw_exception:
+ raise ValueError('InvalidCommand did not throw!')
+
+ def on_command(self, data):
+ self.buy(data.symbol, data.parameters["quantity"])
+ return True # False, None
diff --git a/Algorithm/QCAlgorithm.Python.cs b/Algorithm/QCAlgorithm.Python.cs
index 0a22a1c9e823..dbca952c68b3 100644
--- a/Algorithm/QCAlgorithm.Python.cs
+++ b/Algorithm/QCAlgorithm.Python.cs
@@ -31,6 +31,7 @@
using QuantConnect.Util;
using QuantConnect.Interfaces;
using QuantConnect.Orders;
+using QuantConnect.Commands;
namespace QuantConnect.Algorithm
{
@@ -1620,6 +1621,23 @@ public List Liquidate(PyObject symbols, bool asynchronous = false,
return Liquidate(symbols.ConvertToSymbolEnumerable(), asynchronous, tag, orderProperties);
}
+ ///
+ /// Register a command type to be used
+ ///
+ /// The command type
+ public void AddCommand(PyObject type)
+ {
+ // create a test instance to validate interface is implemented accurate
+ var testInstance = new CommandPythonWrapper(type);
+
+ var wrappedType = Extensions.CreateType(type);
+ _registeredCommands[wrappedType.Name] = (CallbackCommand command) =>
+ {
+ var commandWrapper = new CommandPythonWrapper(type, command.Payload);
+ return commandWrapper.Run(this);
+ };
+ }
+
///
/// Gets indicator base type
///
diff --git a/Algorithm/QCAlgorithm.cs b/Algorithm/QCAlgorithm.cs
index 56549a44c0d7..c1deaf6f1b24 100644
--- a/Algorithm/QCAlgorithm.cs
+++ b/Algorithm/QCAlgorithm.cs
@@ -54,6 +54,8 @@
using QuantConnect.Algorithm.Framework.Alphas.Analysis;
using QuantConnect.Algorithm.Framework.Portfolio.SignalExports;
using Python.Runtime;
+using QuantConnect.Commands;
+using Newtonsoft.Json;
namespace QuantConnect.Algorithm
{
@@ -115,6 +117,9 @@ public partial class QCAlgorithm : MarshalByRefObject, IAlgorithm
private IStatisticsService _statisticsService;
private IBrokerageModel _brokerageModel;
+ private readonly HashSet _oneTimeCommandErrors = new();
+ private readonly Dictionary> _registeredCommands = new(StringComparer.InvariantCultureIgnoreCase);
+
//Error tracking to avoid message flooding:
private string _previousDebugMessage = "";
private string _previousErrorMessage = "";
@@ -3385,6 +3390,62 @@ public DataHistory OptionChain(Symbol symbol)
return new DataHistory(optionChain, new Lazy(() => PandasConverter.GetDataFrame(optionChain)));
}
+ ///
+ /// Register a command type to be used
+ ///
+ /// The command type
+ public void AddCommand() where T : Command
+ {
+ _registeredCommands[typeof(T).Name] = (CallbackCommand command) =>
+ {
+ var commandInstance = JsonConvert.DeserializeObject(command.Payload);
+ return commandInstance.Run(this);
+ };
+ }
+
+ ///
+ /// Run a callback command instance
+ ///
+ /// The callback command instance
+ /// The command result
+ public CommandResultPacket RunCommand(CallbackCommand command)
+ {
+ bool? result = null;
+ if (_registeredCommands.TryGetValue(command.Type, out var target))
+ {
+ try
+ {
+ result = target.Invoke(command);
+ }
+ catch (Exception ex)
+ {
+ QuantConnect.Logging.Log.Error(ex);
+ if (_oneTimeCommandErrors.Add(command.Type))
+ {
+ Log($"Unexpected error running command '{command.Type}' error: '{ex.Message}'");
+ }
+ }
+ }
+ else
+ {
+ if (_oneTimeCommandErrors.Add(command.Type))
+ {
+ Log($"Detected unregistered command type '{command.Type}', will be ignored");
+ }
+ }
+ return new CommandResultPacket(command, result) { CommandName = command.Type };
+ }
+
+ ///
+ /// Generic untyped command call handler
+ ///
+ /// The associated data
+ /// True if success, false otherwise. Returning null will disable command feedback
+ public virtual bool? OnCommand(dynamic data)
+ {
+ return true;
+ }
+
private static Symbol GetCanonicalOptionSymbol(Symbol symbol)
{
// We got the underlying
diff --git a/AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs b/AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs
index 68681ac269f7..0ab8d255923f 100644
--- a/AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs
+++ b/AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs
@@ -37,6 +37,7 @@
using QuantConnect.Statistics;
using QuantConnect.Data.Market;
using QuantConnect.Algorithm.Framework.Alphas.Analysis;
+using QuantConnect.Commands;
namespace QuantConnect.AlgorithmFactory.Python.Wrappers
{
@@ -62,6 +63,7 @@ public class AlgorithmPythonWrapper : BasePythonWrapper, IAlgorithm
private dynamic _onEndOfDay;
private dynamic _onMarginCallWarning;
private dynamic _onOrderEvent;
+ private dynamic _onCommand;
private dynamic _onAssignmentOrderEvent;
private dynamic _onSecuritiesChanged;
private dynamic _onFrameworkSecuritiesChanged;
@@ -153,6 +155,7 @@ public AlgorithmPythonWrapper(string moduleName)
_onDelistings = _algorithm.GetMethod("OnDelistings");
_onSymbolChangedEvents = _algorithm.GetMethod("OnSymbolChangedEvents");
_onEndOfDay = _algorithm.GetMethod("OnEndOfDay");
+ _onCommand = _algorithm.GetMethod("OnCommand");
_onMarginCallWarning = _algorithm.GetMethod("OnMarginCallWarning");
_onOrderEvent = _algorithm.GetMethod("OnOrderEvent");
_onAssignmentOrderEvent = _algorithm.GetMethod("OnAssignmentOrderEvent");
@@ -921,6 +924,16 @@ public void OnOrderEvent(OrderEvent newEvent)
_onOrderEvent(newEvent);
}
+ ///
+ /// Generic untyped command call handler
+ ///
+ /// The associated data
+ /// True if success, false otherwise. Returning null will disable command feedback
+ public bool? OnCommand(dynamic data)
+ {
+ return _onCommand(data);
+ }
+
///
/// Will submit an order request to the algorithm
///
@@ -1242,5 +1255,13 @@ public void SetTags(HashSet tags)
{
_baseAlgorithm.SetTags(tags);
}
+
+ ///
+ /// Run a callback command instance
+ ///
+ /// The callback command instance
+ /// The command result
+ public CommandResultPacket RunCommand(CallbackCommand command) => _baseAlgorithm.RunCommand(command);
+
}
}
diff --git a/Api/Api.cs b/Api/Api.cs
index 4b7eed02bae6..88f84aa87336 100644
--- a/Api/Api.cs
+++ b/Api/Api.cs
@@ -1023,7 +1023,6 @@ public RestResponse LiquidateLiveAlgorithm(int projectId)
///
/// Project for the live instance we want to stop
///
-
public RestResponse StopLiveAlgorithm(int projectId)
{
var request = new RestRequest("live/update/stop", Method.POST)
@@ -1040,6 +1039,29 @@ public RestResponse StopLiveAlgorithm(int projectId)
return result;
}
+ ///
+ /// Create a live command
+ ///
+ /// Project for the live instance we want to run the command against
+ /// The command to run
+ ///
+ public RestResponse CreateLiveCommand(int projectId, object command)
+ {
+ var request = new RestRequest("live/commands/create", Method.POST)
+ {
+ RequestFormat = DataFormat.Json
+ };
+
+ request.AddParameter("application/json", JsonConvert.SerializeObject(new
+ {
+ projectId,
+ command
+ }), ParameterType.RequestBody);
+
+ ApiConnection.TryRequest(request, out RestResponse result);
+ return result;
+ }
+
///
/// Gets the logs of a specific live algorithm
///
diff --git a/Common/AlgorithmImports.py b/Common/AlgorithmImports.py
index ede61a93b7a0..198cd457521e 100644
--- a/Common/AlgorithmImports.py
+++ b/Common/AlgorithmImports.py
@@ -39,6 +39,7 @@
from QuantConnect.Python import *
from QuantConnect.Storage import *
from QuantConnect.Research import *
+from QuantConnect.Commands import *
from QuantConnect.Algorithm import *
from QuantConnect.Statistics import *
from QuantConnect.Parameters import *
diff --git a/Common/Commands/BaseCommandHandler.cs b/Common/Commands/BaseCommandHandler.cs
index a06a391f22c1..20daceec3d19 100644
--- a/Common/Commands/BaseCommandHandler.cs
+++ b/Common/Commands/BaseCommandHandler.cs
@@ -15,8 +15,10 @@
using System;
using System.Linq;
+using Newtonsoft.Json;
using QuantConnect.Logging;
using QuantConnect.Packets;
+using Newtonsoft.Json.Linq;
using QuantConnect.Interfaces;
using System.Collections.Generic;
@@ -27,6 +29,8 @@ namespace QuantConnect.Commands
///
public abstract class BaseCommandHandler : ICommandHandler
{
+ protected static readonly JsonSerializerSettings Settings = new() { TypeNameHandling = TypeNameHandling.All };
+
///
/// The algorithm instance
///
@@ -104,5 +108,41 @@ public virtual void Dispose()
{
// nop
}
+
+ ///
+ /// Helper method to create a callback command
+ ///
+ protected ICommand TryGetCallbackCommand(string payload)
+ {
+ Dictionary deserialized = new(StringComparer.InvariantCultureIgnoreCase);
+ try
+ {
+ if (!string.IsNullOrEmpty(payload))
+ {
+ var jobject = JObject.Parse(payload);
+ foreach (var kv in jobject)
+ {
+ deserialized[kv.Key] = kv.Value;
+ }
+ }
+ }
+ catch (Exception err)
+ {
+ Log.Error(err, $"Payload: '{payload}'");
+ return null;
+ }
+
+ if (!deserialized.TryGetValue("id", out var id) || id == null)
+ {
+ id = string.Empty;
+ }
+
+ if (!deserialized.TryGetValue("$type", out var type) || type == null)
+ {
+ type = string.Empty;
+ }
+
+ return new CallbackCommand { Id = id.ToString(), Type = type.ToString(), Payload = payload };
+ }
}
}
diff --git a/Common/Commands/CallbackCommand.cs b/Common/Commands/CallbackCommand.cs
new file mode 100644
index 000000000000..29d59ed36001
--- /dev/null
+++ b/Common/Commands/CallbackCommand.cs
@@ -0,0 +1,63 @@
+/*
+ * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+ * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+using Newtonsoft.Json;
+using QuantConnect.Interfaces;
+
+namespace QuantConnect.Commands
+{
+ ///
+ /// Algorithm callback command type
+ ///
+ public class CallbackCommand : BaseCommand
+ {
+ ///
+ /// The target command type to run, if empty or null will be the generic untyped command handler
+ ///
+ public string Type { get; set; }
+
+ ///
+ /// The command payload
+ ///
+ public string Payload { get; set; }
+
+ ///
+ /// Runs this command against the specified algorithm instance
+ ///
+ /// The algorithm to run this command against
+ public override CommandResultPacket Run(IAlgorithm algorithm)
+ {
+ if (string.IsNullOrEmpty(Type))
+ {
+ // target is the untyped algorithm handler
+ var result = algorithm.OnCommand(string.IsNullOrEmpty(Payload) ? null : JsonConvert.DeserializeObject(Payload));
+ return new CommandResultPacket(this, result);
+ }
+ return algorithm.RunCommand(this);
+ }
+
+ ///
+ /// The command string representation
+ ///
+ public override string ToString()
+ {
+ if (!string.IsNullOrEmpty(Type))
+ {
+ return Type;
+ }
+ return "OnCommand";
+ }
+ }
+}
diff --git a/Common/Commands/Command.cs b/Common/Commands/Command.cs
new file mode 100644
index 000000000000..d5e862058e08
--- /dev/null
+++ b/Common/Commands/Command.cs
@@ -0,0 +1,91 @@
+/*
+ * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+ * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+using System;
+using System.Dynamic;
+using QuantConnect.Data;
+using System.Reflection;
+using System.Linq.Expressions;
+using QuantConnect.Interfaces;
+using System.Collections.Generic;
+using Newtonsoft.Json.Linq;
+
+namespace QuantConnect.Commands
+{
+ ///
+ /// Base generic dynamic command class
+ ///
+ public class Command : DynamicObject
+ {
+ private static readonly MethodInfo SetPropertyMethodInfo = typeof(Command).GetMethod("SetProperty");
+ private static readonly MethodInfo GetPropertyMethodInfo = typeof(Command).GetMethod("GetProperty");
+
+ private readonly Dictionary _storage = new(StringComparer.InvariantCultureIgnoreCase);
+
+ ///
+ /// Get the metaObject required for Dynamism.
+ ///
+ public sealed override DynamicMetaObject GetMetaObject(Expression parameter)
+ {
+ return new GetSetPropertyDynamicMetaObject(parameter, this, SetPropertyMethodInfo, GetPropertyMethodInfo);
+ }
+
+ ///
+ /// Sets the property with the specified name to the value. This is a case-insensitve search.
+ ///
+ /// The property name to set
+ /// The new property value
+ /// Returns the input value back to the caller
+ public object SetProperty(string name, object value)
+ {
+ if (value is JArray jArray)
+ {
+ return _storage[name] = jArray.ToObject>();
+ }
+ else if (value is JObject jobject)
+ {
+ return _storage[name] = jobject.ToObject>();
+ }
+ else
+ {
+ return _storage[name] = value;
+ }
+ }
+
+ ///
+ /// Gets the property's value with the specified name. This is a case-insensitve search.
+ ///
+ /// The property name to access
+ /// object value of BaseData
+ public object GetProperty(string name)
+ {
+ if (!_storage.TryGetValue(name, out var value))
+ {
+ throw new KeyNotFoundException($"Property with name \'{name}\' does not exist. Properties: {string.Join(", ", _storage.Keys)}");
+ }
+ return value;
+ }
+
+ ///
+ /// Run this command using the target algorithm
+ ///
+ /// The algorithm instance
+ /// True if success, false otherwise. Returning null will disable command feedback
+ public virtual bool? Run(IAlgorithm algorithm)
+ {
+ throw new NotImplementedException($"Please implement the 'def run(algorithm) -> bool | None:' method");
+ }
+ }
+}
diff --git a/Common/Commands/CommandResultsPacket.cs b/Common/Commands/CommandResultsPacket.cs
index 9c260eceab69..07a8a3859225 100644
--- a/Common/Commands/CommandResultsPacket.cs
+++ b/Common/Commands/CommandResultsPacket.cs
@@ -31,12 +31,12 @@ public class CommandResultPacket : Packet
///
/// Gets or sets whether or not the
///
- public bool Success { get; set; }
+ public bool? Success { get; set; }
///
/// Initializes a new instance of the class
///
- public CommandResultPacket(ICommand command, bool success)
+ public CommandResultPacket(ICommand command, bool? success)
: base(PacketType.CommandResult)
{
Success = success;
diff --git a/Common/Commands/FileCommandHandler.cs b/Common/Commands/FileCommandHandler.cs
index c01549612f11..31be9608efb5 100644
--- a/Common/Commands/FileCommandHandler.cs
+++ b/Common/Commands/FileCommandHandler.cs
@@ -19,6 +19,7 @@
using QuantConnect.Logging;
using System.Collections.Generic;
using System.Linq;
+using Newtonsoft.Json.Linq;
namespace QuantConnect.Commands
{
@@ -90,7 +91,9 @@ protected override void Acknowledge(ICommand command, CommandResultPacket comman
private void ReadCommandFile(string commandFilePath)
{
Log.Trace($"FileCommandHandler.ReadCommandFile(): {Messages.FileCommandHandler.ReadingCommandFile(commandFilePath)}");
- object deserialized;
+ string contents = null;
+ Exception exception = null;
+ object deserialized = null;
try
{
if (!File.Exists(commandFilePath))
@@ -98,13 +101,12 @@ private void ReadCommandFile(string commandFilePath)
Log.Error($"FileCommandHandler.ReadCommandFile(): {Messages.FileCommandHandler.CommandFileDoesNotExist(commandFilePath)}");
return;
}
- var contents = File.ReadAllText(commandFilePath);
- deserialized = JsonConvert.DeserializeObject(contents, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All });
+ contents = File.ReadAllText(commandFilePath);
+ deserialized = JsonConvert.DeserializeObject(contents, Settings);
}
catch (Exception err)
{
- Log.Error(err);
- deserialized = null;
+ exception = err;
}
// remove the file when we're done reading it
@@ -126,6 +128,20 @@ private void ReadCommandFile(string commandFilePath)
if (item != null)
{
_commands.Enqueue(item);
+ return;
+ }
+
+ var callbackCommand = TryGetCallbackCommand(contents);
+ if (callbackCommand != null)
+ {
+ _commands.Enqueue(callbackCommand);
+ return;
+ }
+
+ if (exception != null)
+ {
+ // if we are here we failed
+ Log.Error(exception);
}
}
}
diff --git a/Common/Interfaces/IAlgorithm.cs b/Common/Interfaces/IAlgorithm.cs
index b1d37fe867ca..fb4f2c8b6ca2 100644
--- a/Common/Interfaces/IAlgorithm.cs
+++ b/Common/Interfaces/IAlgorithm.cs
@@ -32,6 +32,7 @@
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Alphas.Analysis;
+using QuantConnect.Commands;
namespace QuantConnect.Interfaces
{
@@ -608,6 +609,13 @@ InsightManager Insights
/// Event information
void OnOrderEvent(OrderEvent newEvent);
+ ///
+ /// Generic untyped command call handler
+ ///
+ /// The associated data
+ /// True if success, false otherwise. Returning null will disable command feedback
+ bool? OnCommand(dynamic data);
+
///
/// Will submit an order request to the algorithm
///
@@ -919,5 +927,12 @@ Security AddSecurity(Symbol symbol, Resolution? resolution = null, bool fillForw
///
/// The tags
void SetTags(HashSet tags);
+
+ ///
+ /// Run a callback command instance
+ ///
+ /// The callback command instance
+ /// The command result
+ CommandResultPacket RunCommand(CallbackCommand command);
}
}
diff --git a/Common/Python/CommandPythonWrapper.cs b/Common/Python/CommandPythonWrapper.cs
new file mode 100644
index 000000000000..4554725459ed
--- /dev/null
+++ b/Common/Python/CommandPythonWrapper.cs
@@ -0,0 +1,74 @@
+/*
+ * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+ * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+using Python.Runtime;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using QuantConnect.Commands;
+using QuantConnect.Interfaces;
+using System.Collections.Generic;
+
+namespace QuantConnect.Python
+{
+ ///
+ /// Python wrapper for a python defined command type
+ ///
+ public class CommandPythonWrapper : BasePythonWrapper
+ {
+ ///
+ /// Constructor for initialising the class with wrapped object
+ ///
+ /// Python command type
+ /// Command data
+ public CommandPythonWrapper(PyObject type, string data = null)
+ : base()
+ {
+ using var _ = Py.GIL();
+
+ var instance = type.Invoke();
+
+ SetPythonInstance(instance);
+ if (data != null)
+ {
+ foreach (var kvp in JsonConvert.DeserializeObject>(data))
+ {
+ if (kvp.Value is JArray jArray)
+ {
+ SetProperty(kvp.Key, jArray.ToObject>());
+ }
+ else if (kvp.Value is JObject jobject)
+ {
+ SetProperty(kvp.Key, jobject.ToObject>());
+ }
+ else
+ {
+ SetProperty(kvp.Key, kvp.Value);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Run this command using the target algorithm
+ ///
+ /// The algorithm instance
+ /// True if success, false otherwise. Returning null will disable command feedback
+ public bool? Run(IAlgorithm algorithm)
+ {
+ var result = InvokeMethod(nameof(Run), algorithm);
+ return result.GetAndDispose();
+ }
+ }
+}
diff --git a/Common/Python/PythonWrapper.cs b/Common/Python/PythonWrapper.cs
index 7f878977f5b3..fc20aac09af5 100644
--- a/Common/Python/PythonWrapper.cs
+++ b/Common/Python/PythonWrapper.cs
@@ -34,21 +34,28 @@ public static class PythonWrapper
/// The model implementing the interface type
public static PyObject ValidateImplementationOf(this PyObject model)
{
- if (!typeof(TInterface).IsInterface)
- {
- throw new ArgumentException(
- $"{nameof(PythonWrapper)}.{nameof(ValidateImplementationOf)}(): {Messages.PythonWrapper.ExpectedInterfaceTypeParameter}");
- }
-
+ var notInterface = !typeof(TInterface).IsInterface;
var missingMembers = new List();
var members = typeof(TInterface).GetMembers(BindingFlags.Public | BindingFlags.Instance);
using (Py.GIL())
{
foreach (var member in members)
{
- if ((member is not MethodInfo method || !method.IsSpecialName) &&
+ var method = member as MethodInfo;
+ if ((method == null || !method.IsSpecialName) &&
!model.HasAttr(member.Name) && !model.HasAttr(member.Name.ToSnakeCase()))
{
+ if (notInterface)
+ {
+ if (method != null && !method.IsAbstract && (method.IsFinal || !method.IsVirtual || method.DeclaringType != typeof(TInterface)))
+ {
+ continue;
+ }
+ else if (member is ConstructorInfo)
+ {
+ continue;
+ }
+ }
missingMembers.Add(member.Name);
}
}
diff --git a/Engine/Server/LocalLeanManager.cs b/Engine/Server/LocalLeanManager.cs
index c771176ea782..9a7193910dbd 100644
--- a/Engine/Server/LocalLeanManager.cs
+++ b/Engine/Server/LocalLeanManager.cs
@@ -91,8 +91,7 @@ public virtual void OnAlgorithmStart()
{
if (Algorithm.LiveMode)
{
- _commandHandler = new FileCommandHandler();
- _commandHandler.Initialize(_job, Algorithm);
+ SetCommandHandler();
}
}
@@ -119,5 +118,14 @@ public virtual void Dispose()
{
_commandHandler.DisposeSafely();
}
+
+ ///
+ /// Set the command handler to use, protected for testing purposes
+ ///
+ protected virtual void SetCommandHandler()
+ {
+ _commandHandler = new FileCommandHandler();
+ _commandHandler.Initialize(_job, Algorithm);
+ }
}
}
diff --git a/Tests/Api/ApiTestBase.cs b/Tests/Api/ApiTestBase.cs
index 5a337cf5dd32..da581619227e 100644
--- a/Tests/Api/ApiTestBase.cs
+++ b/Tests/Api/ApiTestBase.cs
@@ -86,7 +86,7 @@ private void CreateTestProjectAndBacktest()
return;
}
Log.Debug("ApiTestBase.Setup(): Waiting for test compile to complete");
- compile = WaitForCompilerResponse(TestProject.ProjectId, compile.CompileId);
+ compile = WaitForCompilerResponse(ApiClient, TestProject.ProjectId, compile.CompileId);
if (!compile.Success)
{
Assert.Warn("Could not create compile for the test project, tests using it will fail.");
@@ -134,14 +134,14 @@ private void DeleteTestProjectAndBacktest()
/// Id of the project
/// Id of the compilation of the project
///
- protected Compile WaitForCompilerResponse(int projectId, string compileId)
+ protected static Compile WaitForCompilerResponse(Api.Api apiClient, int projectId, string compileId, int seconds = 60)
{
- Compile compile;
- var finish = DateTime.UtcNow.AddSeconds(60);
+ var compile = new Compile();
+ var finish = DateTime.UtcNow.AddSeconds(seconds);
do
{
- Thread.Sleep(1000);
- compile = ApiClient.ReadCompile(projectId, compileId);
+ Thread.Sleep(100);
+ compile = apiClient.ReadCompile(projectId, compileId);
} while (compile.State != CompileState.BuildSuccess && DateTime.UtcNow < finish);
return compile;
@@ -169,7 +169,7 @@ protected Backtest WaitForBacktestCompletion(int projectId, string backtestId)
///
/// Reload configuration, making sure environment variables are loaded into the config
///
- private static void ReloadConfiguration()
+ internal static void ReloadConfiguration()
{
// nunit 3 sets the current folder to a temp folder we need it to be the test bin output folder
var dir = TestContext.CurrentContext.TestDirectory;
diff --git a/Tests/Api/CommandTests.cs b/Tests/Api/CommandTests.cs
new file mode 100644
index 000000000000..08616924837a
--- /dev/null
+++ b/Tests/Api/CommandTests.cs
@@ -0,0 +1,120 @@
+/*
+ * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+ * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+using System;
+using NUnit.Framework;
+using QuantConnect.Api;
+using System.Threading;
+using System.Collections.Generic;
+
+namespace QuantConnect.Tests.API
+{
+ [TestFixture, Explicit("Requires configured api access, a live node to run on, and brokerage configurations.")]
+ public class CommandTests
+ {
+ private Api.Api _apiClient;
+
+ [OneTimeSetUp]
+ public void Setup()
+ {
+ ApiTestBase.ReloadConfiguration();
+
+ _apiClient = new Api.Api();
+ _apiClient.Initialize(Globals.UserId, Globals.UserToken, Globals.DataFolder);
+ }
+
+ [TestCase("MyCommand")]
+ [TestCase("MyCommand2")]
+ [TestCase("MyCommand3")]
+ [TestCase("")]
+ public void LiveCommand(string commandType)
+ {
+ var command = new Dictionary
+ {
+ { "quantity", 0.1 },
+ { "target", "BTCUSD" },
+ { "$type", commandType }
+ };
+
+ var projectId = RunLiveAlgorithm();
+ try
+ {
+ // allow algo to be deployed and prices to be set so we can trade
+ Thread.Sleep(TimeSpan.FromSeconds(10));
+ var result = _apiClient.CreateLiveCommand(projectId, command);
+ Assert.IsTrue(result.Success);
+ }
+ finally
+ {
+ _apiClient.StopLiveAlgorithm(projectId);
+ _apiClient.DeleteProject(projectId);
+ }
+ }
+
+ private int RunLiveAlgorithm()
+ {
+ var settings = new Dictionary()
+ {
+ { "id", "QuantConnectBrokerage" },
+ { "environment", "paper" },
+ { "user", "" },
+ { "password", "" },
+ { "account", "" }
+ };
+
+ var file = new ProjectFile
+ {
+ Name = "Main.cs",
+ Code = @"from AlgorithmImports import *
+
+class MyCommand():
+ quantity = 0
+ target = ''
+ def run(self, algo: QCAlgorithm) -> bool | None:
+ self.execute_order(algo)
+ def execute_order(self, algo):
+ algo.order(self.target, self.quantity)
+
+class MyCommand2():
+ quantity = 0
+ target = ''
+ def run(self, algo: QCAlgorithm) -> bool | None:
+ algo.order(self.target, self.quantity)
+ return True
+
+class MyCommand3():
+ quantity = 0
+ target = ''
+ def run(self, algo: QCAlgorithm) -> bool | None:
+ algo.order(self.target, self.quantity)
+ return False
+
+class DeterminedSkyBlueGorilla(QCAlgorithm):
+ def initialize(self):
+ self.set_start_date(2023, 3, 17)
+ self.add_crypto(""BTCUSD"", Resolution.SECOND)
+ self.add_command(MyCommand)
+ self.add_command(MyCommand2)
+ self.add_command(MyCommand3)
+
+ def on_command(self, data):
+ self.order(data.target, data.quantity)"
+ };
+
+ // Run the live algorithm
+ return LiveTradingTests.RunLiveAlgorithm(_apiClient, settings, file, stopLiveAlgos: false, language: Language.Python);
+ }
+ }
+}
diff --git a/Tests/Api/LiveTradingTests.cs b/Tests/Api/LiveTradingTests.cs
index b6da89a6ee9c..12dbdd7249d4 100644
--- a/Tests/Api/LiveTradingTests.cs
+++ b/Tests/Api/LiveTradingTests.cs
@@ -721,42 +721,48 @@ public void LiveAlgorithmsAndLiveLogs_CanBeRead_Successfully()
/// Dictionary with the data providers and their corresponding credentials
/// The id of the project created with the algorithm in
private int RunLiveAlgorithm(Dictionary settings, ProjectFile file, bool stopLiveAlgos, Dictionary dataProviders = null)
+ {
+ return RunLiveAlgorithm(ApiClient, settings, file, stopLiveAlgos, dataProviders);
+ }
+
+ internal static int RunLiveAlgorithm(Api.Api apiClient, Dictionary settings, ProjectFile file, bool stopLiveAlgos,
+ Dictionary dataProviders = null, Language language = Language.CSharp)
{
// Create a new project
- var project = ApiClient.CreateProject($"Test project - {DateTime.Now.ToStringInvariant()}", Language.CSharp, TestOrganization);
+ var project = apiClient.CreateProject($"Test project - {DateTime.Now.ToStringInvariant()}", language, Globals.OrganizationID);
var projectId = project.Projects.First().ProjectId;
// Update Project Files
- var updateProjectFileContent = ApiClient.UpdateProjectFileContent(projectId, "Main.cs", file.Code);
+ var updateProjectFileContent = apiClient.UpdateProjectFileContent(projectId, language == Language.CSharp ? "Main.cs" : "main.py", file.Code);
Assert.IsTrue(updateProjectFileContent.Success);
// Create compile
- var compile = ApiClient.CreateCompile(projectId);
+ var compile = apiClient.CreateCompile(projectId);
Assert.IsTrue(compile.Success);
// Wait at max 30 seconds for project to compile
- var compileCheck = WaitForCompilerResponse(projectId, compile.CompileId, 30);
+ var compileCheck = WaitForCompilerResponse(apiClient, projectId, compile.CompileId, 30);
Assert.IsTrue(compileCheck.Success);
Assert.IsTrue(compileCheck.State == CompileState.BuildSuccess);
// Get a live node to launch the algorithm on
- var nodesResponse = ApiClient.ReadProjectNodes(projectId);
+ var nodesResponse = apiClient.ReadProjectNodes(projectId);
Assert.IsTrue(nodesResponse.Success);
var freeNode = nodesResponse.Nodes.LiveNodes.Where(x => x.Busy == false);
Assert.IsNotEmpty(freeNode, "No free Live Nodes found");
// Create live default algorithm
- var createLiveAlgorithm = ApiClient.CreateLiveAlgorithm(projectId, compile.CompileId, freeNode.FirstOrDefault().Id, settings, dataProviders: dataProviders);
+ var createLiveAlgorithm = apiClient.CreateLiveAlgorithm(projectId, compile.CompileId, freeNode.FirstOrDefault().Id, settings, dataProviders: dataProviders);
Assert.IsTrue(createLiveAlgorithm.Success);
if (stopLiveAlgos)
{
// Liquidate live algorithm; will also stop algorithm
- var liquidateLive = ApiClient.LiquidateLiveAlgorithm(projectId);
+ var liquidateLive = apiClient.LiquidateLiveAlgorithm(projectId);
Assert.IsTrue(liquidateLive.Success);
// Delete the project
- var deleteProject = ApiClient.DeleteProject(projectId);
+ var deleteProject = apiClient.DeleteProject(projectId);
Assert.IsTrue(deleteProject.Success);
}
@@ -820,7 +826,7 @@ public void RunLiveAlgorithmsFromPython()
Assert.IsTrue(compile.Success);
// Wait at max 30 seconds for project to compile
- var compileCheck = WaitForCompilerResponse(projectId, compile.CompileId, 30);
+ var compileCheck = WaitForCompilerResponse(ApiClient, projectId, compile.CompileId, 30);
Assert.IsTrue(compileCheck.Success);
Assert.IsTrue(compileCheck.State == CompileState.BuildSuccess);
@@ -863,26 +869,6 @@ def CreateLiveAlgorithmFromPython(apiClient, projectId, compileId, nodeId):
}
}
- ///
- /// Wait for the compiler to respond to a specified compile request
- ///
- /// Id of the project
- /// Id of the compilation of the project
- /// Seconds to allow for compile time
- ///
- private Compile WaitForCompilerResponse(int projectId, string compileId, int seconds)
- {
- var compile = new Compile();
- var finish = DateTime.Now.AddSeconds(seconds);
- while (DateTime.Now < finish)
- {
- Thread.Sleep(1000);
- compile = ApiClient.ReadCompile(projectId, compileId);
- if (compile.State == CompileState.BuildSuccess) break;
- }
- return compile;
- }
-
///
/// Wait to receive at least one order
///
diff --git a/Tests/Api/OptimizationTests.cs b/Tests/Api/OptimizationTests.cs
index 6f68e1fde999..8be4995fff55 100644
--- a/Tests/Api/OptimizationTests.cs
+++ b/Tests/Api/OptimizationTests.cs
@@ -220,7 +220,7 @@ private int GetProjectCompiledAndWithBacktest(out Compile compile)
Assert.IsTrue(compile.Success);
// Wait at max 30 seconds for project to compile
- var compileCheck = WaitForCompilerResponse(projectId, compile.CompileId);
+ var compileCheck = WaitForCompilerResponse(ApiClient, projectId, compile.CompileId);
Assert.IsTrue(compileCheck.Success);
Assert.IsTrue(compileCheck.State == CompileState.BuildSuccess);
diff --git a/Tests/Api/ProjectTests.cs b/Tests/Api/ProjectTests.cs
index bd3e42f5ed90..d8c46b18594c 100644
--- a/Tests/Api/ProjectTests.cs
+++ b/Tests/Api/ProjectTests.cs
@@ -257,7 +257,7 @@ private void Perform_CreateCompileBackTest_Tests(string projectName, Language la
Assert.AreEqual(CompileState.InQueue, compileCreate.State);
// Read out the compile
- var compileSuccess = WaitForCompilerResponse(project.Projects.First().ProjectId, compileCreate.CompileId);
+ var compileSuccess = WaitForCompilerResponse(ApiClient, project.Projects.First().ProjectId, compileCreate.CompileId);
Assert.IsTrue(compileSuccess.Success);
Assert.AreEqual(CompileState.BuildSuccess, compileSuccess.State);
@@ -265,7 +265,7 @@ private void Perform_CreateCompileBackTest_Tests(string projectName, Language la
file.Code += "[Jibberish at end of the file to cause a build error]";
ApiClient.UpdateProjectFileContent(project.Projects.First().ProjectId, file.Name, file.Code);
var compileError = ApiClient.CreateCompile(project.Projects.First().ProjectId);
- compileError = WaitForCompilerResponse(project.Projects.First().ProjectId, compileError.CompileId);
+ compileError = WaitForCompilerResponse(ApiClient, project.Projects.First().ProjectId, compileError.CompileId);
Assert.IsTrue(compileError.Success); // Successfully processed rest request.
Assert.AreEqual(CompileState.BuildError, compileError.State); //Resulting in build fail.
@@ -336,7 +336,7 @@ public void ReadBacktestOrdersReportAndChart()
$"Error updating project file:\n {string.Join("\n ", updateProjectFileContent.Errors)}");
var compileCreate = ApiClient.CreateCompile(project.ProjectId);
- var compileSuccess = WaitForCompilerResponse(project.ProjectId, compileCreate.CompileId);
+ var compileSuccess = WaitForCompilerResponse(ApiClient, project.ProjectId, compileCreate.CompileId);
Assert.IsTrue(compileSuccess.Success, $"Error compiling project:\n {string.Join("\n ", compileSuccess.Errors)}");
var backtestName = $"ReadBacktestOrders Backtest {GetTimestamp()}";
@@ -559,7 +559,7 @@ public void CreatesLiveAlgorithm()
Assert.IsTrue(compile.Success);
// Wait at max 30 seconds for project to compile
- var compileCheck = WaitForCompilerResponse(projectId, compile.CompileId);
+ var compileCheck = WaitForCompilerResponse(ApiClient, projectId, compile.CompileId);
Assert.IsTrue(compileCheck.Success);
Assert.IsTrue(compileCheck.State == CompileState.BuildSuccess);
@@ -642,7 +642,7 @@ public void CreatesOptimization()
Assert.IsTrue(compile.Success);
// Wait at max 30 seconds for project to compile
- var compileCheck = WaitForCompilerResponse(projectId, compile.CompileId);
+ var compileCheck = WaitForCompilerResponse(ApiClient, projectId, compile.CompileId);
Assert.IsTrue(compileCheck.Success);
Assert.IsTrue(compileCheck.State == CompileState.BuildSuccess);
@@ -726,7 +726,7 @@ private void GetProjectAndCompileIdToReadInsights(out int projectId, out string
compileId = compile.CompileId;
// Wait at max 30 seconds for project to compile
- var compileCheck = WaitForCompilerResponse(projectId, compile.CompileId);
+ var compileCheck = WaitForCompilerResponse(ApiClient, projectId, compile.CompileId);
Assert.IsTrue(compileCheck.Success);
Assert.IsTrue(compileCheck.State == CompileState.BuildSuccess);
}
diff --git a/Tests/Common/Commands/CallbackCommandTests.cs b/Tests/Common/Commands/CallbackCommandTests.cs
new file mode 100644
index 000000000000..319109748574
--- /dev/null
+++ b/Tests/Common/Commands/CallbackCommandTests.cs
@@ -0,0 +1,129 @@
+/*
+ * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+ * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+using System.IO;
+using NUnit.Framework;
+using Newtonsoft.Json;
+using QuantConnect.Statistics;
+using QuantConnect.Configuration;
+using System.Collections.Generic;
+using QuantConnect.Algorithm.CSharp;
+using QuantConnect.Lean.Engine.Server;
+using System;
+
+namespace QuantConnect.Tests.Common.Commands
+{
+ [TestFixture]
+ public class CallbackCommandTests
+ {
+ [TestCase(Language.CSharp)]
+ [TestCase(Language.Python)]
+ public void CommanCallback(Language language)
+ {
+ var parameter = new RegressionTests.AlgorithmStatisticsTestParameters(typeof(CallbackCommandRegressionAlgorithm).Name,
+ new Dictionary {
+ {PerformanceMetrics.TotalOrders, "3"},
+ {"Average Win", "0%"},
+ {"Average Loss", "0%"},
+ {"Compounding Annual Return", "0.212%"},
+ {"Drawdown", "0.000%"},
+ {"Expectancy", "0"},
+ {"Net Profit", "0.003%"},
+ {"Sharpe Ratio", "-5.552"},
+ {"Sortino Ratio", "0"},
+ {"Probabilistic Sharpe Ratio", "66.765%"},
+ {"Loss Rate", "0%"},
+ {"Win Rate", "0%"},
+ {"Profit-Loss Ratio", "0"},
+ {"Alpha", "-0.01"},
+ {"Beta", "0.003"},
+ {"Annual Standard Deviation", "0.001"},
+ {"Annual Variance", "0"},
+ {"Information Ratio", "-8.919"},
+ {"Tracking Error", "0.222"},
+ {"Treynor Ratio", "-1.292"},
+ {"Total Fees", "$3.00"},
+ {"Estimated Strategy Capacity", "$670000000.00"},
+ {"Lowest Capacity Asset", "IBM R735QTJ8XC9X"},
+ {"Portfolio Turnover", "0.06%"}
+ },
+ language,
+ AlgorithmStatus.Completed);
+
+ Config.Set("lean-manager-type", typeof(TestLocalLeanManager).Name);
+
+ var result = AlgorithmRunner.RunLocalBacktest(parameter.Algorithm,
+ parameter.Statistics,
+ parameter.Language,
+ parameter.ExpectedFinalStatus);
+ }
+
+ internal class TestLocalLeanManager : LocalLeanManager
+ {
+ private bool _sentCommands;
+ public override void Update()
+ {
+ if (!_sentCommands && Algorithm.Time.TimeOfDay > TimeSpan.FromHours(9.50))
+ {
+ _sentCommands = true;
+ var commands = new List>
+ {
+ new()
+ {
+ { "$type", "" },
+ { "id", 1 },
+ { "Symbol", "SPY" },
+ { "Parameters", new Dictionary { { "quantity", 1 } } },
+ { "unused", 99 }
+ },
+ new()
+ {
+ { "$type", "VoidCommand" },
+ { "id", null },
+ { "Quantity", 1 },
+ { "targettime", Algorithm.Time },
+ { "target", new [] { "BAC" } },
+ { "Parameters", new Dictionary { { "tag", "a tag" }, { "something", "else" } } },
+ },
+ new()
+ {
+ { "id", "2" },
+ { "$type", "BoolCommand" },
+ { "Result", true },
+ { "unused", new [] { 99 } }
+ },
+ new()
+ {
+ { "$type", "BoolCommand" },
+ { "Result", null },
+ }
+ };
+
+ for (var i = 1; i <= commands.Count; i++)
+ {
+ var command = commands[i - 1];
+ command["id"] = i;
+ File.WriteAllText($"command-{i}.json", JsonConvert.SerializeObject(command));
+ }
+ base.Update();
+ }
+ }
+ public override void OnAlgorithmStart()
+ {
+ SetCommandHandler();
+ }
+ }
+ }
+}