Skip to content

Commit

Permalink
Enhance command support
Browse files Browse the repository at this point in the history
- Enhance command support, adding link helper methods. Adding and
  expanding new tests
- Minor improvement for command str representation
  • Loading branch information
Martin-Molinero committed Sep 30, 2024
1 parent 809faa2 commit 3917e87
Show file tree
Hide file tree
Showing 12 changed files with 413 additions and 4 deletions.
12 changes: 12 additions & 0 deletions Algorithm.CSharp/CallbackCommandRegressionAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ public override void Initialize()
AddEquity("IBM");
AddCommand<BoolCommand>();
AddCommand<VoidCommand>();

var potentialCommand = new VoidCommand
{
Target = new[] { "BAC" },
Quantity = 10,
Parameters = new() { { "tag", "Signal X" } }
};
var commandLink = Link(potentialCommand);
Notify.Email("email@address", "Trade Command Event", $"Signal X trade\nFollow link to trigger: {commandLink}");

var commandLink2 = Link(new { Symbol = "SPY", Parameters = new Dictionary<string, int>() { { "Quantity", 10 } } });
Notify.Email("email@address", "Untyped Command Event", $"Signal Y trade\nFollow link to trigger: {commandLink2}");
}

/// <summary>
Expand Down
13 changes: 13 additions & 0 deletions Algorithm.Python/CallbackCommandRegressionAlgorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class BoolCommand(Command):
def run(self, algo: QCAlgorithm) -> bool | None:
trade_ibm = self.my_custom_method()
if trade_ibm:
algo.debug(f"BoolCommand.run: {str(self)}")
algo.buy("IBM", 1)
return trade_ibm

Expand Down Expand Up @@ -68,6 +69,18 @@ def initialize(self):
if not threw_exception:
raise ValueError('InvalidCommand did not throw!')

potential_command = VoidCommand()
potential_command.target = [ "BAC" ]
potential_command.quantity = 10
potential_command.parameters = { "tag": "Signal X" }

command_link = self.link(potential_command)
self.notify.email("email@address", "Trade Command Event", f"Signal X trade\nFollow link to trigger: {command_link}")

untyped_command_link = self.link({ "symbol": "SPY", "parameters": { "quantity": 10 } })
self.notify.email("email@address", "Untyped Command Event", f"Signal Y trade\nFollow link to trigger: {untyped_command_link}")

def on_command(self, data):
self.debug(f"on_command: {str(data)}")
self.buy(data.symbol, data.parameters["quantity"])
return True # False, None
18 changes: 18 additions & 0 deletions Algorithm/QCAlgorithm.Python.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Data.Fundamental;
using System.Linq;
using Newtonsoft.Json;
using QuantConnect.Brokerages;
using QuantConnect.Scheduling;
using QuantConnect.Util;
Expand Down Expand Up @@ -1638,6 +1639,23 @@ public void AddCommand(PyObject type)
};
}

/// <summary>
/// Get an authenticated link to execute the given command instance
/// </summary>
/// <param name="command">The target command</param>
/// <returns>The authenticated link</returns>
public string Link(PyObject command)
{
using var _ = Py.GIL();

var strResult = CommandPythonWrapper.Serialize(command);
using var pyType = command.GetPythonType();
var wrappedType = Extensions.CreateType(pyType);

var payload = JsonConvert.DeserializeObject<Dictionary<string, object>>(strResult);
return CommandLink(wrappedType.Name, payload);
}

/// <summary>
/// Gets indicator base type
/// </summary>
Expand Down
25 changes: 25 additions & 0 deletions Algorithm/QCAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3390,6 +3390,21 @@ public DataHistory<OptionUniverse> OptionChain(Symbol symbol)
return new DataHistory<OptionUniverse>(optionChain, new Lazy<PyObject>(() => PandasConverter.GetDataFrame(optionChain)));
}

/// <summary>
/// Get an authenticated link to execute the given command instance
/// </summary>
/// <param name="command">The target command</param>
/// <returns>The authenticated link</returns>
public string Link(object command)
{
var typeName = command.GetType().Name;
if (command is Command || typeName.Contains("AnonymousType", StringComparison.InvariantCultureIgnoreCase))
{
return CommandLink(typeName, command);
}
return string.Empty;
}

/// <summary>
/// Register a command type to be used
/// </summary>
Expand Down Expand Up @@ -3446,6 +3461,16 @@ public CommandResultPacket RunCommand(CallbackCommand command)
return true;
}

private string CommandLink(string typeName, object command)
{
var payload = new Dictionary<string, dynamic> { { "projectId", ProjectId }, { "command", command } };
if (_registeredCommands.ContainsKey(typeName))
{
payload["command[$type]"] = typeName;
}
return Api.Authentication.Link("live/commands/create", payload);
}

private static Symbol GetCanonicalOptionSymbol(Symbol symbol)
{
// We got the underlying
Expand Down
126 changes: 126 additions & 0 deletions Common/Api/Authentication.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* 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.Web;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Collections.Specialized;

namespace QuantConnect.Api
{
/// <summary>
/// Helper methods for api authentication and interaction
/// </summary>
public static class Authentication
{
/// <summary>
/// Generate a secure hash for the authorization headers.
/// </summary>
/// <returns>Time based hash of user token and timestamp.</returns>
public static string Hash(int timestamp)
{
return Hash(timestamp, Globals.UserToken);
}

/// <summary>
/// Generate a secure hash for the authorization headers.
/// </summary>
/// <returns>Time based hash of user token and timestamp.</returns>
public static string Hash(int timestamp, string token)
{
// Create a new hash using current UTC timestamp.
// Hash must be generated fresh each time.
var data = $"{token}:{timestamp.ToStringInvariant()}";
return data.ToSHA256();
}

/// <summary>
/// Create an authenticated link for the target endpoint using the optional given payload
/// </summary>
/// <param name="endpoint">The endpoint</param>
/// <param name="payload">The payload</param>
/// <returns>The authenticated link to trigger the request</returns>
public static string Link(string endpoint, IEnumerable<KeyValuePair<string, object>> payload = null)
{
var queryString = HttpUtility.ParseQueryString(string.Empty);

var timestamp = (int)Time.TimeStamp();
queryString.Add("authorization", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Globals.UserId}:{Hash(timestamp)}")));
queryString.Add("timestamp", timestamp.ToStringInvariant());

PopulateQueryString(queryString, payload);

return $"{Globals.Api}{endpoint.RemoveFromStart("/").RemoveFromEnd("/")}?{queryString}";
}

/// <summary>
/// Helper method to populate a query string with the given payload
/// </summary>
/// <remarks>Useful for testing purposes</remarks>
public static void PopulateQueryString(NameValueCollection queryString, IEnumerable<KeyValuePair<string, object>> payload = null)
{
if (payload != null)
{
foreach (var kv in payload)
{
AddToQuery(queryString, kv);
}
}
}

/// <summary>
/// Will add the given key value pairs to the query encoded as xform data
/// </summary>
private static void AddToQuery(NameValueCollection queryString, KeyValuePair<string, object> keyValuePairs)
{
var objectType = keyValuePairs.Value.GetType();
if (objectType.IsValueType || objectType == typeof(string))
{
// straight
queryString.Add(keyValuePairs.Key, keyValuePairs.Value.ToString());
}
else
{
// let's take advantage of json to load the properties we should include
var serialized = JsonConvert.SerializeObject(keyValuePairs.Value);
foreach (var jObject in JObject.Parse(serialized))
{
var subKey = $"{keyValuePairs.Key}[{jObject.Key}]";
if (jObject.Value is JObject)
{
// inception
AddToQuery(queryString, new KeyValuePair<string, object>(subKey, jObject.Value.ToObject<object>()));
}
else if(jObject.Value is JArray jArray)
{
var counter = 0;
foreach (var value in jArray.ToObject<List<object>>())
{
queryString.Add($"{subKey}[{counter++}]", value.ToString());
}
}
else
{
queryString.Add(subKey, jObject.Value.ToString());
}
}
}
}
}
}
3 changes: 3 additions & 0 deletions Common/Commands/BaseCommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ namespace QuantConnect.Commands
/// </summary>
public abstract class BaseCommandHandler : ICommandHandler
{
/// <summary>
/// Command json settings
/// </summary>
protected static readonly JsonSerializerSettings Settings = new() { TypeNameHandling = TypeNameHandling.All };

/// <summary>
Expand Down
56 changes: 54 additions & 2 deletions Common/Commands/Command.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
*/

using System;
using System.Linq;
using System.Dynamic;
using Newtonsoft.Json;
using QuantConnect.Data;
using System.Reflection;
using Newtonsoft.Json.Linq;
using System.Linq.Expressions;
using QuantConnect.Interfaces;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;

namespace QuantConnect.Commands
{
Expand All @@ -34,12 +36,17 @@ public class Command : DynamicObject

private readonly Dictionary<string, object> _storage = new(StringComparer.InvariantCultureIgnoreCase);

/// <summary>
/// Useful to string representation in python
/// </summary>
protected string PayloadData { get; set; }

/// <summary>
/// Get the metaObject required for Dynamism.
/// </summary>
public sealed override DynamicMetaObject GetMetaObject(Expression parameter)
{
return new GetSetPropertyDynamicMetaObject(parameter, this, SetPropertyMethodInfo, GetPropertyMethodInfo);
return new SerializableDynamicMetaObject(parameter, this, SetPropertyMethodInfo, GetPropertyMethodInfo);
}

/// <summary>
Expand Down Expand Up @@ -73,6 +80,20 @@ public object GetProperty(string name)
{
if (!_storage.TryGetValue(name, out var value))
{
var type = GetType();
if (type != typeof(Command))
{
var propertyInfo = type.GetProperty(name, BindingFlags.Public | BindingFlags.Instance);
if (propertyInfo != null)
{
return propertyInfo.GetValue(this, null);
}
var fieldInfo = type.GetField(name, BindingFlags.Public | BindingFlags.Instance);
if (fieldInfo != null)
{
return fieldInfo.GetValue(this);
}
}
throw new KeyNotFoundException($"Property with name \'{name}\' does not exist. Properties: {string.Join(", ", _storage.Keys)}");
}
return value;
Expand All @@ -87,5 +108,36 @@ public object GetProperty(string name)
{
throw new NotImplementedException($"Please implement the 'def run(algorithm) -> bool | None:' method");
}

/// <summary>
/// The string representation of this command
/// </summary>
public override string ToString()
{
if (!string.IsNullOrEmpty(PayloadData))
{
return PayloadData;
}
return JsonConvert.SerializeObject(this);
}

/// <summary>
/// Helper class so we can serialize a command
/// </summary>
private class SerializableDynamicMetaObject : GetSetPropertyDynamicMetaObject
{
private readonly Command _object;
public SerializableDynamicMetaObject(Expression expression, object value, MethodInfo setPropertyMethodInfo, MethodInfo getPropertyMethodInfo)
: base(expression, value, setPropertyMethodInfo, getPropertyMethodInfo)
{
_object = (Command)value;
}
public override IEnumerable<string> GetDynamicMemberNames()
{
return _object._storage.Keys.Concat(_object.GetType()
.GetMembers(BindingFlags.Public | BindingFlags.Instance)
.Where(x => x.MemberType == MemberTypes.Field || x.MemberType == MemberTypes.Property).Select(x => x.Name));
}
}
}
}
5 changes: 5 additions & 0 deletions Common/Globals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ static Globals()
Reset();
}

/// <summary>
/// The base api url address to use
/// </summary>
public static string Api { get; } = "https://www.quantconnect.com/api/v2/";

/// <summary>
/// The user Id
/// </summary>
Expand Down
Loading

0 comments on commit 3917e87

Please sign in to comment.