Skip to content

Commit

Permalink
Minor improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
Martin-Molinero committed Sep 18, 2024
1 parent 9d38a71 commit 2045afc
Show file tree
Hide file tree
Showing 14 changed files with 234 additions and 72 deletions.
2 changes: 1 addition & 1 deletion Algorithm.CSharp/CallbackCommandRegressionAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public override void Initialize()
/// </summary>
public override bool? OnCommand(dynamic data)
{
Buy(data.Symbol, 1);
Buy(data.Symbol, data.parameters["quantity"]);
return true;
}

Expand Down
2 changes: 1 addition & 1 deletion Algorithm.Python/CallbackCommandRegressionAlgorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,5 @@ def initialize(self):
raise ValueError('InvalidCommand did not throw!')

def on_command(self, data):
self.buy(data.symbol, 1)
self.buy(data.symbol, data.parameters["quantity"])
return True # False, None
4 changes: 2 additions & 2 deletions Algorithm/QCAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3422,15 +3422,15 @@ public CommandResultPacket RunCommand(CallbackCommand command)
QuantConnect.Logging.Log.Error(ex);
if (_oneTimeCommandErrors.Add(command.Type))
{
Log($"Unexpected error running command {command.Type}, {ex.Message}");
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");
Log($"Detected unregistered command type '{command.Type}', will be ignored");
}
}
return new CommandResultPacket(command, result) { CommandName = command.Type };
Expand Down
24 changes: 23 additions & 1 deletion Api/Api.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1023,7 +1023,6 @@ public RestResponse LiquidateLiveAlgorithm(int projectId)
/// </summary>
/// <param name="projectId">Project for the live instance we want to stop</param>
/// <returns><see cref="RestResponse"/></returns>

public RestResponse StopLiveAlgorithm(int projectId)
{
var request = new RestRequest("live/update/stop", Method.POST)
Expand All @@ -1040,6 +1039,29 @@ public RestResponse StopLiveAlgorithm(int projectId)
return result;
}

/// <summary>
/// Create a live command
/// </summary>
/// <param name="projectId">Project for the live instance we want to run the command against</param>
/// <param name="command">The command to run</param>
/// <returns><see cref="RestResponse"/></returns>
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;
}

/// <summary>
/// Gets the logs of a specific live algorithm
/// </summary>
Expand Down
16 changes: 10 additions & 6 deletions Common/Commands/BaseCommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,23 +117,27 @@ protected ICommand TryGetCallbackCommand(string payload)
Dictionary<string, JToken> deserialized = new(StringComparer.InvariantCultureIgnoreCase);
try
{
var jobject = JObject.Parse(payload);
foreach (var kv in jobject)
if (!string.IsNullOrEmpty(payload))
{
deserialized[kv.Key] = kv.Value;
var jobject = JObject.Parse(payload);
foreach (var kv in jobject)
{
deserialized[kv.Key] = kv.Value;
}
}
}
catch
catch (Exception err)
{
Log.Error(err, $"Payload: '{payload}'");
return null;
}

if (!deserialized.TryGetValue("id", out var id))
if (!deserialized.TryGetValue("id", out var id) || id == null)
{
id = string.Empty;
}

if (!deserialized.TryGetValue("$type", out var type))
if (!deserialized.TryGetValue("$type", out var type) || type == null)
{
type = string.Empty;
}
Expand Down
14 changes: 13 additions & 1 deletion Common/Commands/CallbackCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,22 @@ public override CommandResultPacket Run(IAlgorithm algorithm)
if (string.IsNullOrEmpty(Type))
{
// target is the untyped algorithm handler
var result = algorithm.OnCommand(JsonConvert.DeserializeObject<Command>(Payload));
var result = algorithm.OnCommand(string.IsNullOrEmpty(Payload) ? null : JsonConvert.DeserializeObject<Command>(Payload));
return new CommandResultPacket(this, result);
}
return algorithm.RunCommand(this);
}

/// <summary>
/// The command string representation
/// </summary>
public override string ToString()
{
if (!string.IsNullOrEmpty(Type))
{
return Type;
}
return "OnCommand";
}
}
}
14 changes: 13 additions & 1 deletion Common/Commands/Command.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using System.Linq.Expressions;
using QuantConnect.Interfaces;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;

namespace QuantConnect.Commands
{
Expand Down Expand Up @@ -49,7 +50,18 @@ public sealed override DynamicMetaObject GetMetaObject(Expression parameter)
/// <returns>Returns the input value back to the caller</returns>
public object SetProperty(string name, object value)
{
return _storage[name] = value;
if (value is JArray jArray)
{
return _storage[name] = jArray.ToObject<List<object>>();
}
else if (value is JObject jobject)
{
return _storage[name] = jobject.ToObject<Dictionary<string, object>>();
}
else
{
return _storage[name] = value;
}
}

/// <summary>
Expand Down
34 changes: 18 additions & 16 deletions Common/Commands/FileCommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +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))
Expand All @@ -101,24 +102,11 @@ private void ReadCommandFile(string commandFilePath)
return;
}
contents = File.ReadAllText(commandFilePath);
try
{
deserialized = JsonConvert.DeserializeObject(contents, Settings);
}
catch
{
deserialized = TryGetCallbackCommand(contents);
if (deserialized == null)
{
throw;
}
}

deserialized = JsonConvert.DeserializeObject(contents, Settings);
}
catch (Exception err)
{
Log.Error(err);
deserialized = null;
exception = err;
}

// remove the file when we're done reading it
Expand All @@ -140,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);
}
}
}
Expand Down
14 changes: 7 additions & 7 deletions Tests/Api/ApiTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down Expand Up @@ -134,14 +134,14 @@ private void DeleteTestProjectAndBacktest()
/// <param name="projectId">Id of the project</param>
/// <param name="compileId">Id of the compilation of the project</param>
/// <returns></returns>
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;
Expand Down Expand Up @@ -169,7 +169,7 @@ protected Backtest WaitForBacktestCompletion(int projectId, string backtestId)
/// <summary>
/// Reload configuration, making sure environment variables are loaded into the config
/// </summary>
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;
Expand Down
120 changes: 120 additions & 0 deletions Tests/Api/CommandTests.cs
Original file line number Diff line number Diff line change
@@ -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<string, object>
{
{ "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<string, object>()
{
{ "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);
}
}
}
Loading

0 comments on commit 2045afc

Please sign in to comment.