/* * 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.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using RestSharp; using RestSharp.Extensions; using QuantConnect.Interfaces; using QuantConnect.Logging; using QuantConnect.Optimizer.Objectives; using QuantConnect.Optimizer.Parameters; using QuantConnect.Orders; using QuantConnect.Statistics; using QuantConnect.Util; using QuantConnect.Notifications; using Python.Runtime; using System.Threading; using System.Net.Http.Headers; using System.Collections.Concurrent; using System.Text; using Newtonsoft.Json.Serialization; namespace QuantConnect.Api { /// /// QuantConnect.com Interaction Via API. /// public class Api : IApi, IDownloadProvider { private readonly BlockingCollection> _clientPool; private string _dataFolder; /// /// Serializer settings to use /// protected JsonSerializerSettings SerializerSettings { get; set; } = new() { ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy { ProcessDictionaryKeys = false, OverrideSpecifiedNames = true } } }; /// /// Returns the underlying API connection /// protected ApiConnection ApiConnection { get; private set; } /// /// Creates a new instance of /// public Api() { _clientPool = new BlockingCollection>(new ConcurrentQueue>(), 5); for (int i = 0; i < _clientPool.BoundedCapacity; i++) { _clientPool.Add(new Lazy()); } } /// /// Initialize the API with the given variables /// public virtual void Initialize(int userId, string token, string dataFolder) { ApiConnection = new ApiConnection(userId, token); _dataFolder = dataFolder?.Replace("\\", "/", StringComparison.InvariantCulture); //Allow proper decoding of orders from the API. JsonConvert.DefaultSettings = () => new JsonSerializerSettings { Converters = { new OrderJsonConverter() } }; } /// /// Check if Api is successfully connected with correct credentials /// public bool Connected => ApiConnection.Connected; /// /// Create a project with the specified name and language via QuantConnect.com API /// /// Project name /// Programming language to use /// Optional param for specifying organization to create project under. /// If none provided web defaults to preferred. /// Project object from the API. public ProjectResponse CreateProject(string name, Language language, string organizationId = null) { var request = new RestRequest("projects/create", Method.POST) { RequestFormat = DataFormat.Json }; // Only include organization Id if its not null or empty string jsonParams; if (string.IsNullOrEmpty(organizationId)) { jsonParams = JsonConvert.SerializeObject(new { name, language }); } else { jsonParams = JsonConvert.SerializeObject(new { name, language, organizationId }); } request.AddParameter("application/json", jsonParams, ParameterType.RequestBody); ApiConnection.TryRequest(request, out ProjectResponse result); return result; } /// /// Get details about a single project /// /// Id of the project /// that contains information regarding the project public ProjectResponse ReadProject(int projectId) { var request = new RestRequest("projects/read", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out ProjectResponse result); return result; } /// /// List details of all projects /// /// that contains information regarding the project public ProjectResponse ListProjects() { var request = new RestRequest("projects/read", Method.POST) { RequestFormat = DataFormat.Json }; ApiConnection.TryRequest(request, out ProjectResponse result); return result; } /// /// Add a file to a project /// /// The project to which the file should be added /// The name of the new file /// The content of the new file /// that includes information about the newly created file public RestResponse AddProjectFile(int projectId, string name, string content) { var request = new RestRequest("files/create", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId, name, content }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out RestResponse result); return result; } /// /// Update the name of a file /// /// Project id to which the file belongs /// The current name of the file /// The new name for the file /// indicating success public RestResponse UpdateProjectFileName(int projectId, string oldFileName, string newFileName) { var request = new RestRequest("files/update", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId, name = oldFileName, newName = newFileName }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out RestResponse result); return result; } /// /// Update the contents of a file /// /// Project id to which the file belongs /// The name of the file that should be updated /// The new contents of the file /// indicating success public RestResponse UpdateProjectFileContent(int projectId, string fileName, string newFileContents) { var request = new RestRequest("files/update", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId, name = fileName, content = newFileContents }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out RestResponse result); return result; } /// /// Read all files in a project /// /// Project id to which the file belongs /// that includes the information about all files in the project public ProjectFilesResponse ReadProjectFiles(int projectId) { var request = new RestRequest("files/read", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out ProjectFilesResponse result); return result; } /// /// Read all nodes in a project. /// /// Project id to which the nodes refer /// that includes the information about all nodes in the project public ProjectNodesResponse ReadProjectNodes(int projectId) { var request = new RestRequest("projects/nodes/read", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out ProjectNodesResponse result); return result; } /// /// Update the active state of some nodes to true. /// If you don't provide any nodes, all the nodes become inactive and AutoSelectNode is true. /// /// Project id to which the nodes refer /// List of node ids to update /// that includes the information about all nodes in the project public ProjectNodesResponse UpdateProjectNodes(int projectId, string[] nodes) { var request = new RestRequest("projects/nodes/update", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId, nodes }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out ProjectNodesResponse result); return result; } /// /// Read a file in a project /// /// Project id to which the file belongs /// The name of the file /// that includes the file information public ProjectFilesResponse ReadProjectFile(int projectId, string fileName) { var request = new RestRequest("files/read", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId, name = fileName }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out ProjectFilesResponse result); return result; } /// /// Gets a list of LEAN versions with their corresponding basic descriptions /// public VersionsResponse ReadLeanVersions() { var request = new RestRequest("lean/versions/read", Method.POST) { RequestFormat = DataFormat.Json }; ApiConnection.TryRequest(request, out VersionsResponse result); return result; } /// /// Delete a file in a project /// /// Project id to which the file belongs /// The name of the file that should be deleted /// that includes the information about all files in the project public RestResponse DeleteProjectFile(int projectId, string name) { var request = new RestRequest("files/delete", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId, name, }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out RestResponse result); return result; } /// /// Delete a project /// /// Project id we own and wish to delete /// RestResponse indicating success public RestResponse DeleteProject(int projectId) { var request = new RestRequest("projects/delete", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out RestResponse result); return result; } /// /// Create a new compile job request for this project id. /// /// Project id we wish to compile. /// Compile object result public Compile CreateCompile(int projectId) { var request = new RestRequest("compile/create", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out Compile result); return result; } /// /// Read a compile packet job result. /// /// Project id we sent for compile /// Compile id return from the creation request /// public Compile ReadCompile(int projectId, string compileId) { var request = new RestRequest("compile/read", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId, compileId }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out Compile result); return result; } /// /// Sends a notification /// /// The notification to send /// The project id /// containing success response and errors public virtual RestResponse SendNotification(Notification notification, int projectId) { throw new NotImplementedException($"{nameof(Api)} does not support sending notifications"); } /// /// Create a new backtest request and get the id. /// /// Id for the project to backtest /// Compile id for the project /// Name for the new backtest /// t public Backtest CreateBacktest(int projectId, string compileId, string backtestName) { var request = new RestRequest("backtests/create", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId, compileId, backtestName }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out BacktestResponseWrapper result); // Use API Response values for Backtest Values result.Backtest.Success = result.Success; result.Backtest.Errors = result.Errors; // Return only the backtest object return result.Backtest; } /// /// Read out a backtest in the project id specified. /// /// Project id to read /// Specific backtest id to read /// True will return backtest charts /// public Backtest ReadBacktest(int projectId, string backtestId, bool getCharts = true) { var request = new RestRequest("backtests/read", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId, backtestId }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out BacktestResponseWrapper result); if (result == null) { // api call failed return null; } if (!result.Success) { // place an empty place holder so we can return any errors back to the user and not just null result.Backtest = new Backtest { BacktestId = backtestId }; } // Go fetch the charts if the backtest is completed and success else if (getCharts && result.Backtest.Completed) { // For storing our collected charts var updatedCharts = new Dictionary(); // Create backtest requests for each chart that is empty foreach (var chart in result.Backtest.Charts) { if (!chart.Value.Series.IsNullOrEmpty()) { continue; } var chartRequest = new RestRequest("backtests/chart/read", Method.POST) { RequestFormat = DataFormat.Json }; chartRequest.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId, backtestId, name = chart.Key, count = 100 }), ParameterType.RequestBody); // Add this chart to our updated collection if (ApiConnection.TryRequest(chartRequest, out ReadChartResponse chartResponse) && chartResponse.Success) { updatedCharts.Add(chart.Key, chartResponse.Chart); } } // Update our result foreach(var updatedChart in updatedCharts) { result.Backtest.Charts[updatedChart.Key] = updatedChart.Value; } } // Use API Response values for Backtest Values result.Backtest.Success = result.Success; result.Backtest.Errors = result.Errors; // Return only the backtest object return result.Backtest; } /// /// Returns the orders of the specified backtest and project id. /// /// Id of the project from which to read the orders /// Id of the backtest from which to read the orders /// Starting index of the orders to be fetched. Required if end > 100 /// Last index of the orders to be fetched. Note that end - start must be less than 100 /// Will throw an if there are any API errors /// The list of public List ReadBacktestOrders(int projectId, string backtestId, int start = 0, int end = 100) { var request = new RestRequest("backtests/orders/read", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { start, end, projectId, backtestId }), ParameterType.RequestBody); return MakeRequestOrThrow(request, nameof(ReadBacktestOrders)).Orders; } /// /// Returns a requested chart object from a backtest /// /// Project ID of the request /// The requested chart name /// The Utc start seconds timestamp of the request /// The Utc end seconds timestamp of the request /// The number of data points to request /// Associated Backtest ID for this chart request /// The chart public ReadChartResponse ReadBacktestChart(int projectId, string name, int start, int end, uint count, string backtestId) { var request = new RestRequest("backtests/chart/read", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId, name, start, end, count, backtestId, }), ParameterType.RequestBody); ReadChartResponse result; ApiConnection.TryRequest(request, out result); var finish = DateTime.UtcNow.AddMinutes(1); while (DateTime.UtcNow < finish && result.Chart == null) { Thread.Sleep(5000); ApiConnection.TryRequest(request, out result); } return result; } /// /// Update a backtest name /// /// Project for the backtest we want to update /// Backtest id we want to update /// Name we'd like to assign to the backtest /// Note attached to the backtest /// public RestResponse UpdateBacktest(int projectId, string backtestId, string name = "", string note = "") { var request = new RestRequest("backtests/update", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId, backtestId, name, note }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out RestResponse result); return result; } /// /// List all the backtest summaries for a project /// /// Project id we'd like to get a list of backtest for /// True for include statistics in the response, false otherwise /// public BacktestSummaryList ListBacktests(int projectId, bool includeStatistics = true) { var request = new RestRequest("backtests/list", Method.POST) { RequestFormat = DataFormat.Json }; var obj = new Dictionary() { { "projectId", projectId }, { "includeStatistics", includeStatistics } }; request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody); ApiConnection.TryRequest(request, out BacktestSummaryList result); return result; } /// /// Delete a backtest from the specified project and backtestId. /// /// Project for the backtest we want to delete /// Backtest id we want to delete /// public RestResponse DeleteBacktest(int projectId, string backtestId) { var request = new RestRequest("backtests/delete", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId, backtestId }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out RestResponse result); return result; } /// /// Updates the tags collection for a backtest /// /// Project for the backtest we want to update /// Backtest id we want to update /// The new backtest tags /// public RestResponse UpdateBacktestTags(int projectId, string backtestId, IReadOnlyCollection tags) { var request = new RestRequest("backtests/tags/update", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId, backtestId, tags }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out RestResponse result); return result; } /// /// Read out the insights of a backtest /// /// Id of the project from which to read the backtest /// Backtest id from which we want to get the insights /// Starting index of the insights to be fetched /// Last index of the insights to be fetched. Note that end - start must be less than 100 /// /// public InsightResponse ReadBacktestInsights(int projectId, string backtestId, int start = 0, int end = 0) { var request = new RestRequest("backtests/insights/read", Method.POST) { RequestFormat = DataFormat.Json, }; var diff = end - start; if (diff > 100) { throw new ArgumentException($"The difference between the start and end index of the insights must be smaller than 100, but it was {diff}."); } else if (end == 0) { end = start + 100; } JObject obj = new() { { "projectId", projectId }, { "backtestId", backtestId }, { "start", start }, { "end", end }, }; request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody); ApiConnection.TryRequest(request, out InsightResponse result); return result; } /// /// Create a live algorithm. /// /// Id of the project on QuantConnect /// Id of the compilation on QuantConnect /// Id of the node that will run the algorithm /// Dictionary with brokerage specific settings. Each brokerage requires certain specific credentials /// in order to process the given orders. Each key in this dictionary represents a required field/credential /// to provide to the brokerage API and its value represents the value of that field. For example: "brokerageSettings: { /// "id": "Binance", "binance-api-secret": "123ABC", "binance-api-key": "ABC123"}. It is worth saying, /// that this dictionary must always contain an entry whose key is "id" and its value is the name of the brokerage /// (see ) /// The version of the Lean used to run the algorithm. /// -1 is master, however, sometimes this can create problems with live deployments. /// If you experience problems using, try specifying the version of Lean you would like to use. /// Dictionary with data providers credentials. Each data provider requires certain credentials /// in order to retrieve data from their API. Each key in this dictionary describes a data provider name /// and its corresponding value is another dictionary with the required key-value pairs of credential /// names and values. For example: "dataProviders: { "InteractiveBrokersBrokerage" : { "id": 12345, "environment" : "paper", /// "username": "testUsername", "password": "testPassword"}}" /// Information regarding the new algorithm public CreateLiveAlgorithmResponse CreateLiveAlgorithm(int projectId, string compileId, string nodeId, Dictionary brokerageSettings, string versionId = "-1", Dictionary dataProviders = null) { var request = new RestRequest("live/create", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject( new LiveAlgorithmApiSettingsWrapper (projectId, compileId, nodeId, brokerageSettings, versionId, dataProviders ) ), ParameterType.RequestBody); ApiConnection.TryRequest(request, out CreateLiveAlgorithmResponse result); return result; } /// /// Create a live algorithm. /// /// Id of the project on QuantConnect /// Id of the compilation on QuantConnect /// Id of the node that will run the algorithm /// Python Dictionary with brokerage specific settings. Each brokerage requires certain specific credentials /// in order to process the given orders. Each key in this dictionary represents a required field/credential /// to provide to the brokerage API and its value represents the value of that field. For example: "brokerageSettings: { /// "id": "Binance", "binance-api-secret": "123ABC", "binance-api-key": "ABC123"}. It is worth saying, /// that this dictionary must always contain an entry whose key is "id" and its value is the name of the brokerage /// (see ) /// The version of the Lean used to run the algorithm. /// -1 is master, however, sometimes this can create problems with live deployments. /// If you experience problems using, try specifying the version of Lean you would like to use. /// Python Dictionary with data providers credentials. Each data provider requires certain credentials /// in order to retrieve data from their API. Each key in this dictionary describes a data provider name /// and its corresponding value is another dictionary with the required key-value pairs of credential /// names and values. For example: "dataProviders: { "InteractiveBrokersBrokerage" : { "id": 12345, "environment" : "paper", /// "username": "testUsername", "password": "testPassword"}}" /// Information regarding the new algorithm public CreateLiveAlgorithmResponse CreateLiveAlgorithm(int projectId, string compileId, string nodeId, PyObject brokerageSettings, string versionId = "-1", PyObject dataProviders = null) { return CreateLiveAlgorithm(projectId, compileId, nodeId, ConvertToDictionary(brokerageSettings), versionId, dataProviders != null ? ConvertToDictionary(dataProviders) : null); } /// /// Converts a given Python dictionary into a C# /// /// Python dictionary to be converted private static Dictionary ConvertToDictionary(PyObject brokerageSettings) { using (Py.GIL()) { var stringBrokerageSettings = brokerageSettings.ToString(); return JsonConvert.DeserializeObject>(stringBrokerageSettings); } } /// /// Get a list of live running algorithms for user /// /// Filter the statuses of the algorithms returned from the api /// Earliest launched time of the algorithms returned by the Api /// Latest launched time of the algorithms returned by the Api /// public LiveList ListLiveAlgorithms(AlgorithmStatus? status = null, DateTime? startTime = null, DateTime? endTime = null) { // Only the following statuses are supported by the Api if (status.HasValue && status != AlgorithmStatus.Running && status != AlgorithmStatus.RuntimeError && status != AlgorithmStatus.Stopped && status != AlgorithmStatus.Liquidated) { throw new ArgumentException( "The Api only supports Algorithm Statuses of Running, Stopped, RuntimeError and Liquidated"); } var request = new RestRequest("live/list", Method.POST) { RequestFormat = DataFormat.Json }; var epochStartTime = startTime == null ? 0 : Time.DateTimeToUnixTimeStamp(startTime.Value); var epochEndTime = endTime == null ? Time.DateTimeToUnixTimeStamp(DateTime.UtcNow) : Time.DateTimeToUnixTimeStamp(endTime.Value); JObject obj = new JObject { { "start", epochStartTime }, { "end", epochEndTime } }; if (status.HasValue) { obj.Add("status", status.ToString()); } request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody); ApiConnection.TryRequest(request, out LiveList result); return result; } /// /// Read out a live algorithm in the project id specified. /// /// Project id to read /// Specific instance id to read /// public LiveAlgorithmResults ReadLiveAlgorithm(int projectId, string deployId) { var request = new RestRequest("live/read", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId, deployId }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out LiveAlgorithmResults result); return result; } /// /// Read out the portfolio state of a live algorithm /// /// Id of the project from which to read the live algorithm /// public PortfolioResponse ReadLivePortfolio(int projectId) { var request = new RestRequest("live/portfolio/read", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out PortfolioResponse result); return result; } /// /// Returns the orders of the specified project id live algorithm. /// /// Id of the project from which to read the live orders /// Starting index of the orders to be fetched. Required if end > 100 /// Last index of the orders to be fetched. Note that end - start must be less than 100 /// Will throw an if there are any API errors /// The list of public List ReadLiveOrders(int projectId, int start = 0, int end = 100) { var request = new RestRequest("live/orders/read", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { start, end, projectId }), ParameterType.RequestBody); return MakeRequestOrThrow(request, nameof(ReadLiveOrders)).Orders; } /// /// Liquidate a live algorithm from the specified project and deployId. /// /// Project for the live instance we want to stop /// public RestResponse LiquidateLiveAlgorithm(int projectId) { var request = new RestRequest("live/update/liquidate", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out RestResponse result); return result; } /// /// Stop a live algorithm from the specified project and deployId. /// /// Project for the live instance we want to stop /// public RestResponse StopLiveAlgorithm(int projectId) { var request = new RestRequest("live/update/stop", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out RestResponse result); 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; } /// /// Broadcast a live command /// /// Organization ID of the projects we would like to broadcast the command to /// Project for the live instance we want to exclude from the broadcast list /// The command to run /// public RestResponse BroadcastLiveCommand(string organizationId, int? excludeProjectId, object command) { var request = new RestRequest("live/commands/broadcast", Method.POST) { RequestFormat = DataFormat.Json, }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { organizationId, excludeProjectId, command }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out RestResponse result); return result; } /// /// Gets the logs of a specific live algorithm /// /// Project Id of the live running algorithm /// Algorithm Id of the live running algorithm /// Start line of logs to read /// End line of logs to read /// List of strings that represent the logs of the algorithm public LiveLog ReadLiveLogs(int projectId, string algorithmId, int startLine, int endLine) { var logLinesNumber = endLine - startLine; if (logLinesNumber > 250) { throw new ArgumentException($"The maximum number of log lines allowed is 250. But the number of log lines was {logLinesNumber}."); } var request = new RestRequest("live/logs/read", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { format = "json", projectId, algorithmId, startLine, endLine, }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out LiveLog result); return result; } /// /// Returns a chart object from a live algorithm /// /// Project ID of the request /// The requested chart name /// The Utc start seconds timestamp of the request /// The Utc end seconds timestamp of the request /// The number of data points to request /// The chart public ReadChartResponse ReadLiveChart(int projectId, string name, int start, int end, uint count) { var request = new RestRequest("live/chart/read", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId, name, start, end, count }), ParameterType.RequestBody); ReadChartResponse result = default; ApiConnection.TryRequest(request, out result); var finish = DateTime.UtcNow.AddMinutes(1); while(DateTime.UtcNow < finish && result.Chart == null) { Thread.Sleep(5000); ApiConnection.TryRequest(request, out result); } return result; } /// /// Read out the insights of a live algorithm /// /// Id of the project from which to read the live algorithm /// Starting index of the insights to be fetched /// Last index of the insights to be fetched. Note that end - start must be less than 100 /// /// public InsightResponse ReadLiveInsights(int projectId, int start = 0, int end = 0) { var request = new RestRequest("live/insights/read", Method.POST) { RequestFormat = DataFormat.Json, }; var diff = end - start; if (diff > 100) { throw new ArgumentException($"The difference between the start and end index of the insights must be smaller than 100, but it was {diff}."); } else if (end == 0) { end = start + 100; } JObject obj = new JObject { { "projectId", projectId }, { "start", start }, { "end", end }, }; request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody); ApiConnection.TryRequest(request, out InsightResponse result); return result; } /// /// Gets the link to the downloadable data. /// /// File path representing the data requested /// Organization to download from /// to the downloadable data. public DataLink ReadDataLink(string filePath, string organizationId) { if (filePath == null) { throw new ArgumentException("Api.ReadDataLink(): Filepath must not be null"); } // Prepare filePath for request filePath = FormatPathForDataRequest(filePath); var request = new RestRequest("data/read", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { format = "link", filePath, organizationId }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out DataLink result); return result; } /// /// Get valid data entries for a given filepath from data/list /// /// public DataList ReadDataDirectory(string filePath) { if (filePath == null) { throw new ArgumentException("Api.ReadDataDirectory(): Filepath must not be null"); } // Prepare filePath for request filePath = FormatPathForDataRequest(filePath); // Verify the filePath for this request is at least three directory deep // (requirement of endpoint) if (filePath.Count(x => x == '/') < 3) { throw new ArgumentException($"Api.ReadDataDirectory(): Data directory requested must be at least" + $" three directories deep. FilePath: {filePath}"); } var request = new RestRequest("data/list", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { filePath }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out DataList result); return result; } /// /// Gets data prices from data/prices /// public DataPricesList ReadDataPrices(string organizationId) { var request = new RestRequest("data/prices", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { organizationId }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out DataPricesList result); return result; } /// /// Read out the report of a backtest in the project id specified. /// /// Project id to read /// Specific backtest id to read /// public BacktestReport ReadBacktestReport(int projectId, string backtestId) { var request = new RestRequest("backtests/read/report", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { backtestId, projectId }), ParameterType.RequestBody); BacktestReport report = new BacktestReport(); var finish = DateTime.UtcNow.AddMinutes(1); while (DateTime.UtcNow < finish && !report.Success) { Thread.Sleep(10000); ApiConnection.TryRequest(request, out report); } return report; } /// /// Method to purchase and download data from QuantConnect /// /// File path representing the data requested /// Organization to buy the data with /// A indicating whether the data was successfully downloaded or not. public bool DownloadData(string filePath, string organizationId) { // Get a link to the data var dataLink = ReadDataLink(filePath, organizationId); // Make sure the link was successfully retrieved if (!dataLink.Success) { Log.Trace($"Api.DownloadData(): Failed to get link for {filePath}. " + $"Errors: {string.Join(',', dataLink.Errors)}"); return false; } // Make sure the directory exist before writing var directory = Path.GetDirectoryName(filePath); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } var client = BorrowClient(); try { // Download the file var uri = new Uri(dataLink.Link); using var dataStream = client.Value.GetStreamAsync(uri); using var fileStream = new FileStream(FileExtension.ToNormalizedPath(filePath), FileMode.Create); dataStream.Result.CopyTo(fileStream); } catch { Log.Error($"Api.DownloadData(): Failed to download zip for path ({filePath})"); return false; } finally { ReturnClient(client); } return true; } /// /// Get the algorithm status from the user with this algorithm id. /// /// String algorithm id we're searching for. /// Algorithm status enum public virtual AlgorithmControl GetAlgorithmStatus(string algorithmId) { return new AlgorithmControl() { ChartSubscription = "*" }; } /// /// Algorithm passes back its current status to the UX. /// /// Status of the current algorithm /// String algorithm id we're setting. /// Message for the algorithm status event /// Algorithm status enum public virtual void SetAlgorithmStatus(string algorithmId, AlgorithmStatus status, string message = "") { // } /// /// Send the statistics to storage for performance tracking. /// /// Identifier for algorithm /// Unrealized gainloss /// Total fees /// Net profi /// Algorithm holdings /// Total equity /// Net return for the deployment /// Volume traded /// Total trades since inception /// Sharpe ratio since inception public virtual void SendStatistics(string algorithmId, decimal unrealized, decimal fees, decimal netProfit, decimal holdings, decimal equity, decimal netReturn, decimal volume, int trades, double sharpe) { // } /// /// Send an email to the user associated with the specified algorithm id /// /// The algorithm id /// The email subject /// The email message body public virtual void SendUserEmail(string algorithmId, string subject, string body) { // } /// /// Local implementation for downloading data to algorithms /// /// URL to download /// KVP headers /// Username for basic authentication /// Password for basic authentication /// public virtual string Download(string address, IEnumerable> headers, string userName, string password) { return Encoding.UTF8.GetString(DownloadBytes(address, headers, userName, password)); } /// /// Local implementation for downloading data to algorithms /// /// URL to download /// KVP headers /// Username for basic authentication /// Password for basic authentication /// A stream from which the data can be read /// Stream.Close() most be called to avoid running out of resources public virtual byte[] DownloadBytes(string address, IEnumerable> headers, string userName, string password) { var client = BorrowClient(); try { client.Value.DefaultRequestHeaders.Clear(); // Add a user agent header in case the requested URI contains a query. client.Value.DefaultRequestHeaders.TryAddWithoutValidation("user-agent", "QCAlgorithm.Download(): User Agent Header"); if (headers != null) { foreach (var header in headers) { client.Value.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value); } } if (!userName.IsNullOrEmpty() || !password.IsNullOrEmpty()) { var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{userName}:{password}")); client.Value.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials); } return client.Value.GetByteArrayAsync(new Uri(address)).Result; } catch (Exception exception) { var message = $"Api.DownloadBytes(): Failed to download data from {address}"; if (!userName.IsNullOrEmpty() || !password.IsNullOrEmpty()) { message += $" with username: {userName} and password {password}"; } throw new WebException($"{message}. Please verify the source for missing http:// or https://", exception); } finally { client.Value.DefaultRequestHeaders.Clear(); ReturnClient(client); } } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// /// 2 public virtual void Dispose() { // Dispose of the HttpClient pool _clientPool.CompleteAdding(); foreach (var client in _clientPool.GetConsumingEnumerable()) { if (client.IsValueCreated) { client.Value.DisposeSafely(); } } _clientPool.DisposeSafely(); } /// /// Generate a secure hash for the authorization headers. /// /// Time based hash of user token and timestamp. public static string CreateSecureHash(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(); } /// /// Will read the organization account status /// /// The target organization id, if null will return default organization public Account ReadAccount(string organizationId = null) { var request = new RestRequest("account/read", Method.POST) { RequestFormat = DataFormat.Json }; if (organizationId != null) { request.AddParameter("application/json", JsonConvert.SerializeObject(new { organizationId }), ParameterType.RequestBody); } ApiConnection.TryRequest(request, out Account account); return account; } /// /// Fetch organization data from web API /// /// /// public Organization ReadOrganization(string organizationId = null) { var request = new RestRequest("organizations/read", Method.POST) { RequestFormat = DataFormat.Json }; if (organizationId != null) { request.AddParameter("application/json", JsonConvert.SerializeObject(new { organizationId }), ParameterType.RequestBody); } ApiConnection.TryRequest(request, out OrganizationResponse response); return response.Organization; } /// /// Estimate optimization with the specified parameters via QuantConnect.com API /// /// Project ID of the project the optimization belongs to /// Name of the optimization /// Target of the optimization, see examples in /// Target extremum of the optimization, for example "max" or "min" /// Optimization target value /// Optimization strategy, /// Optimization compile ID /// Optimization parameters /// Optimization constraints /// Estimate object from the API. public Estimate EstimateOptimization( int projectId, string name, string target, string targetTo, decimal? targetValue, string strategy, string compileId, HashSet parameters, IReadOnlyList constraints) { var request = new RestRequest("optimizations/estimate", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId, name, target, targetTo, targetValue, strategy, compileId, parameters, constraints }, SerializerSettings), ParameterType.RequestBody); ApiConnection.TryRequest(request, out EstimateResponseWrapper response); return response.Estimate; } /// /// Create an optimization with the specified parameters via QuantConnect.com API /// /// Project ID of the project the optimization belongs to /// Name of the optimization /// Target of the optimization, see examples in /// Target extremum of the optimization, for example "max" or "min" /// Optimization target value /// Optimization strategy, /// Optimization compile ID /// Optimization parameters /// Optimization constraints /// Estimated cost for optimization /// Optimization node type /// Number of parallel nodes for optimization /// BaseOptimization object from the API. public OptimizationSummary CreateOptimization( int projectId, string name, string target, string targetTo, decimal? targetValue, string strategy, string compileId, HashSet parameters, IReadOnlyList constraints, decimal estimatedCost, string nodeType, int parallelNodes) { var request = new RestRequest("optimizations/create", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId, name, target, targetTo, targetValue, strategy, compileId, parameters, constraints, estimatedCost, nodeType, parallelNodes }, SerializerSettings), ParameterType.RequestBody); ApiConnection.TryRequest(request, out OptimizationList result); return result.Optimizations.FirstOrDefault(); } /// /// List all the optimizations for a project /// /// Project id we'd like to get a list of optimizations for /// A list of BaseOptimization objects, public List ListOptimizations(int projectId) { var request = new RestRequest("optimizations/list", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { projectId, }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out OptimizationList result); return result.Optimizations; } /// /// Read an optimization /// /// Optimization id for the optimization we want to read /// public Optimization ReadOptimization(string optimizationId) { var request = new RestRequest("optimizations/read", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { optimizationId }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out OptimizationResponseWrapper response); return response.Optimization; } /// /// Abort an optimization /// /// Optimization id for the optimization we want to abort /// public RestResponse AbortOptimization(string optimizationId) { var request = new RestRequest("optimizations/abort", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { optimizationId }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out RestResponse result); return result; } /// /// Update an optimization /// /// Optimization id we want to update /// Name we'd like to assign to the optimization /// public RestResponse UpdateOptimization(string optimizationId, string name = null) { var request = new RestRequest("optimizations/update", Method.POST) { RequestFormat = DataFormat.Json }; var obj = new JObject { { "optimizationId", optimizationId } }; if (name.HasValue()) { obj.Add("name", name); } request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody); ApiConnection.TryRequest(request, out RestResponse result); return result; } /// /// Delete an optimization /// /// Optimization id for the optimization we want to delete /// public RestResponse DeleteOptimization(string optimizationId) { var request = new RestRequest("optimizations/delete", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { optimizationId }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out RestResponse result); return result; } /// /// Download the object store files associated with the given organization ID and key /// /// Organization ID we would like to get the Object Store files from /// Keys for the Object Store files /// Folder in which the object store files will be stored /// True if the object store files were retrieved correctly, false otherwise public bool GetObjectStore(string organizationId, List keys, string destinationFolder = null) { var request = new RestRequest("object/get", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("application/json", JsonConvert.SerializeObject(new { organizationId, keys }), ParameterType.RequestBody); ApiConnection.TryRequest(request, out GetObjectStoreResponse result); if (result == null || !result.Success) { Log.Error($"Api.GetObjectStore(): Failed to get the jobId to request the download URL for the object store files." + (result != null ? $" Errors: {string.Join(",", result.Errors)}" : "")); return false; } var jobId = result.JobId; var getUrlRequest = new RestRequest("object/get", Method.POST) { RequestFormat = DataFormat.Json }; getUrlRequest.AddParameter("application/json", JsonConvert.SerializeObject(new { organizationId, jobId }), ParameterType.RequestBody); var frontier = DateTime.UtcNow + TimeSpan.FromMinutes(5); while (string.IsNullOrEmpty(result?.Url) && (DateTime.UtcNow < frontier)) { Thread.Sleep(3000); ApiConnection.TryRequest(getUrlRequest, out result); } if (result == null || string.IsNullOrEmpty(result.Url)) { Log.Error($"Api.GetObjectStore(): Failed to get the download URL from the jobId {jobId}." + (result != null ? $" Errors: {string.Join(",", result.Errors)}" : "")); return false; } var directory = destinationFolder ?? Directory.GetCurrentDirectory(); var client = BorrowClient(); try { if (client.Value.Timeout != TimeSpan.FromMinutes(20)) { client.Value.Timeout = TimeSpan.FromMinutes(20); } // Download the file var uri = new Uri(result.Url); using var byteArray = client.Value.GetByteArrayAsync(uri); Compression.UnzipToFolder(byteArray.Result, directory); } catch (Exception e) { Log.Error($"Api.GetObjectStore(): Failed to download zip for path ({directory}). Error: {e.Message}"); return false; } finally { ReturnClient(client); } return true; } /// /// Get Object Store properties given the organization ID and the Object Store key /// /// Organization ID we would like to get the Object Store from /// Key for the Object Store file /// /// It does not work when the object store is a directory public PropertiesObjectStoreResponse GetObjectStoreProperties(string organizationId, string key) { var request = new RestRequest("object/properties", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("organizationId", organizationId); request.AddParameter("key", key); ApiConnection.TryRequest(request, out PropertiesObjectStoreResponse result); if (result == null || !result.Success) { Log.Error($"Api.ObjectStore(): Failed to get the properties for the object store key {key}." + (result != null ? $" Errors: {string.Join(",", result.Errors)}" : "")); } return result; } /// /// Upload files to the Object Store /// /// Organization ID we would like to upload the file to /// Key to the Object Store file /// File (as an array of bytes) to be uploaded /// public RestResponse SetObjectStore(string organizationId, string key, byte[] objectData) { var request = new RestRequest("object/set", Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("organizationId", organizationId); request.AddParameter("key", key); request.AddFileBytes("objectData", objectData, "objectData"); request.AlwaysMultipartFormData = true; ApiConnection.TryRequest(request, out RestResponse result); return result; } /// /// Request to delete Object Store metadata of a specific organization and key /// /// Organization ID we would like to delete the Object Store file from /// Key to the Object Store file /// public RestResponse DeleteObjectStore(string organizationId, string key) { var request = new RestRequest("object/delete", Method.POST) { RequestFormat = DataFormat.Json }; var obj = new Dictionary { { "organizationId", organizationId }, { "key", key } }; request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody); ApiConnection.TryRequest(request, out RestResponse result); return result; } /// /// Request to list Object Store files of a specific organization and path /// /// Organization ID we would like to list the Object Store files from /// Path to the Object Store files /// public ListObjectStoreResponse ListObjectStore(string organizationId, string path) { var request = new RestRequest("object/list", Method.POST) { RequestFormat = DataFormat.Json }; var obj = new Dictionary { { "organizationId", organizationId }, { "path", path } }; request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody); ApiConnection.TryRequest(request, out ListObjectStoreResponse result); return result; } /// /// Helper method to normalize path for api data requests /// /// Filepath to format /// The data folder to use /// Normalized path public static string FormatPathForDataRequest(string filePath, string dataFolder = null) { if (filePath == null) { Log.Error("Api.FormatPathForDataRequest(): Cannot format null string"); return null; } dataFolder ??= Globals.DataFolder; // Normalize windows paths to linux format dataFolder = dataFolder.Replace("\\", "/", StringComparison.InvariantCulture); filePath = filePath.Replace("\\", "/", StringComparison.InvariantCulture); // First remove data root directory from path for request if included if (filePath.StartsWith(dataFolder, StringComparison.InvariantCulture)) { filePath = filePath.Substring(dataFolder.Length); } // Trim '/' from start, this can cause issues for _dataFolders without final directory separator in the config filePath = filePath.TrimStart('/'); return filePath; } /// /// Helper method that will execute the given api request and throw an exception if it fails /// private T MakeRequestOrThrow(RestRequest request, string callerName) where T : RestResponse { if (!ApiConnection.TryRequest(request, out T result)) { var errors = string.Empty; if (result != null && result.Errors != null && result.Errors.Count > 0) { errors = $". Errors: ['{string.Join(",", result.Errors)}']"; } throw new WebException($"{callerName} api request failed{errors}"); } return result; } /// /// Borrows and HTTP client from the pool /// private Lazy BorrowClient() { using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(10)); return _clientPool.Take(cancellationTokenSource.Token); } /// /// Returns the HTTP client to the pool /// private void ReturnClient(Lazy client) { _clientPool.Add(client); } } }