Lean  $LEAN_TAG$
Api.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using System;
17 using System.Collections.Generic;
18 using System.IO;
19 using System.Linq;
20 using System.Net;
21 using System.Net.Http;
22 using Newtonsoft.Json;
23 using Newtonsoft.Json.Linq;
24 using RestSharp;
25 using RestSharp.Extensions;
27 using QuantConnect.Logging;
30 using QuantConnect.Orders;
32 using QuantConnect.Util;
34 using Python.Runtime;
35 using System.Threading;
36 using System.Net.Http.Headers;
37 using System.Collections.Concurrent;
38 using System.Text;
39 using Newtonsoft.Json.Serialization;
40 
42 {
43  /// <summary>
44  /// QuantConnect.com Interaction Via API.
45  /// </summary>
46  public class Api : IApi, IDownloadProvider
47  {
48  private readonly BlockingCollection<Lazy<HttpClient>> _clientPool;
49  private string _dataFolder;
50 
51  /// <summary>
52  /// Serializer settings to use
53  /// </summary>
54  protected JsonSerializerSettings SerializerSettings { get; set; } = new()
55  {
56  ContractResolver = new DefaultContractResolver
57  {
58  NamingStrategy = new CamelCaseNamingStrategy
59  {
60  ProcessDictionaryKeys = false,
61  OverrideSpecifiedNames = true
62  }
63  }
64  };
65 
66  /// <summary>
67  /// Returns the underlying API connection
68  /// </summary>
69  protected ApiConnection ApiConnection { get; private set; }
70 
71  /// <summary>
72  /// Creates a new instance of <see cref="Api"/>
73  /// </summary>
74  public Api()
75  {
76  _clientPool = new BlockingCollection<Lazy<HttpClient>>(new ConcurrentQueue<Lazy<HttpClient>>(), 5);
77  for (int i = 0; i < _clientPool.BoundedCapacity; i++)
78  {
79  _clientPool.Add(new Lazy<HttpClient>());
80  }
81  }
82 
83  /// <summary>
84  /// Initialize the API with the given variables
85  /// </summary>
86  public virtual void Initialize(int userId, string token, string dataFolder)
87  {
88  ApiConnection = new ApiConnection(userId, token);
89  _dataFolder = dataFolder?.Replace("\\", "/", StringComparison.InvariantCulture);
90 
91  //Allow proper decoding of orders from the API.
92  JsonConvert.DefaultSettings = () => new JsonSerializerSettings
93  {
94  Converters = { new OrderJsonConverter() }
95  };
96  }
97 
98  /// <summary>
99  /// Check if Api is successfully connected with correct credentials
100  /// </summary>
102 
103  /// <summary>
104  /// Create a project with the specified name and language via QuantConnect.com API
105  /// </summary>
106  /// <param name="name">Project name</param>
107  /// <param name="language">Programming language to use</param>
108  /// <param name="organizationId">Optional param for specifying organization to create project under.
109  /// If none provided web defaults to preferred.</param>
110  /// <returns>Project object from the API.</returns>
111 
112  public ProjectResponse CreateProject(string name, Language language, string organizationId = null)
113  {
114  var request = new RestRequest("projects/create", Method.POST)
115  {
116  RequestFormat = DataFormat.Json
117  };
118 
119  // Only include organization Id if its not null or empty
120  string jsonParams;
121  if (string.IsNullOrEmpty(organizationId))
122  {
123  jsonParams = JsonConvert.SerializeObject(new
124  {
125  name,
126  language
127  });
128  }
129  else
130  {
131  jsonParams = JsonConvert.SerializeObject(new
132  {
133  name,
134  language,
135  organizationId
136  });
137  }
138 
139  request.AddParameter("application/json", jsonParams, ParameterType.RequestBody);
140 
141  ApiConnection.TryRequest(request, out ProjectResponse result);
142  return result;
143  }
144 
145  /// <summary>
146  /// Get details about a single project
147  /// </summary>
148  /// <param name="projectId">Id of the project</param>
149  /// <returns><see cref="ProjectResponse"/> that contains information regarding the project</returns>
150 
151  public ProjectResponse ReadProject(int projectId)
152  {
153  var request = new RestRequest("projects/read", Method.POST)
154  {
155  RequestFormat = DataFormat.Json
156  };
157 
158  request.AddParameter("application/json", JsonConvert.SerializeObject(new
159  {
160  projectId
161  }), ParameterType.RequestBody);
162 
163  ApiConnection.TryRequest(request, out ProjectResponse result);
164  return result;
165  }
166 
167  /// <summary>
168  /// List details of all projects
169  /// </summary>
170  /// <returns><see cref="ProjectResponse"/> that contains information regarding the project</returns>
171 
173  {
174  var request = new RestRequest("projects/read", Method.POST)
175  {
176  RequestFormat = DataFormat.Json
177  };
178 
179  ApiConnection.TryRequest(request, out ProjectResponse result);
180  return result;
181  }
182 
183 
184  /// <summary>
185  /// Add a file to a project
186  /// </summary>
187  /// <param name="projectId">The project to which the file should be added</param>
188  /// <param name="name">The name of the new file</param>
189  /// <param name="content">The content of the new file</param>
190  /// <returns><see cref="ProjectFilesResponse"/> that includes information about the newly created file</returns>
191 
192  public RestResponse AddProjectFile(int projectId, string name, string content)
193  {
194  var request = new RestRequest("files/create", Method.POST)
195  {
196  RequestFormat = DataFormat.Json
197  };
198 
199  request.AddParameter("application/json", JsonConvert.SerializeObject(new
200  {
201  projectId,
202  name,
203  content
204  }), ParameterType.RequestBody);
205 
206  ApiConnection.TryRequest(request, out RestResponse result);
207  return result;
208  }
209 
210 
211  /// <summary>
212  /// Update the name of a file
213  /// </summary>
214  /// <param name="projectId">Project id to which the file belongs</param>
215  /// <param name="oldFileName">The current name of the file</param>
216  /// <param name="newFileName">The new name for the file</param>
217  /// <returns><see cref="RestResponse"/> indicating success</returns>
218 
219  public RestResponse UpdateProjectFileName(int projectId, string oldFileName, string newFileName)
220  {
221  var request = new RestRequest("files/update", Method.POST)
222  {
223  RequestFormat = DataFormat.Json
224  };
225 
226  request.AddParameter("application/json", JsonConvert.SerializeObject(new
227  {
228  projectId,
229  name = oldFileName,
230  newName = newFileName
231  }), ParameterType.RequestBody);
232 
233  ApiConnection.TryRequest(request, out RestResponse result);
234  return result;
235  }
236 
237 
238  /// <summary>
239  /// Update the contents of a file
240  /// </summary>
241  /// <param name="projectId">Project id to which the file belongs</param>
242  /// <param name="fileName">The name of the file that should be updated</param>
243  /// <param name="newFileContents">The new contents of the file</param>
244  /// <returns><see cref="RestResponse"/> indicating success</returns>
245 
246  public RestResponse UpdateProjectFileContent(int projectId, string fileName, string newFileContents)
247  {
248  var request = new RestRequest("files/update", Method.POST)
249  {
250  RequestFormat = DataFormat.Json
251  };
252 
253  request.AddParameter("application/json", JsonConvert.SerializeObject(new
254  {
255  projectId,
256  name = fileName,
257  content = newFileContents
258  }), ParameterType.RequestBody);
259 
260  ApiConnection.TryRequest(request, out RestResponse result);
261  return result;
262  }
263 
264 
265  /// <summary>
266  /// Read all files in a project
267  /// </summary>
268  /// <param name="projectId">Project id to which the file belongs</param>
269  /// <returns><see cref="ProjectFilesResponse"/> that includes the information about all files in the project</returns>
270 
272  {
273  var request = new RestRequest("files/read", Method.POST)
274  {
275  RequestFormat = DataFormat.Json
276  };
277 
278  request.AddParameter("application/json", JsonConvert.SerializeObject(new
279  {
280  projectId
281  }), ParameterType.RequestBody);
282 
283  ApiConnection.TryRequest(request, out ProjectFilesResponse result);
284  return result;
285  }
286 
287  /// <summary>
288  /// Read all nodes in a project.
289  /// </summary>
290  /// <param name="projectId">Project id to which the nodes refer</param>
291  /// <returns><see cref="ProjectNodesResponse"/> that includes the information about all nodes in the project</returns>
293  {
294  var request = new RestRequest("projects/nodes/read", Method.POST)
295  {
296  RequestFormat = DataFormat.Json
297  };
298 
299  request.AddParameter("application/json", JsonConvert.SerializeObject(new
300  {
301  projectId
302  }), ParameterType.RequestBody);
303 
304  ApiConnection.TryRequest(request, out ProjectNodesResponse result);
305  return result;
306  }
307 
308  /// <summary>
309  /// Update the active state of some nodes to true.
310  /// If you don't provide any nodes, all the nodes become inactive and AutoSelectNode is true.
311  /// </summary>
312  /// <param name="projectId">Project id to which the nodes refer</param>
313  /// <param name="nodes">List of node ids to update</param>
314  /// <returns><see cref="ProjectNodesResponse"/> that includes the information about all nodes in the project</returns>
315  public ProjectNodesResponse UpdateProjectNodes(int projectId, string[] nodes)
316  {
317  var request = new RestRequest("projects/nodes/update", Method.POST)
318  {
319  RequestFormat = DataFormat.Json
320  };
321 
322  request.AddParameter("application/json", JsonConvert.SerializeObject(new
323  {
324  projectId,
325  nodes
326  }), ParameterType.RequestBody);
327 
328  ApiConnection.TryRequest(request, out ProjectNodesResponse result);
329  return result;
330  }
331 
332  /// <summary>
333  /// Read a file in a project
334  /// </summary>
335  /// <param name="projectId">Project id to which the file belongs</param>
336  /// <param name="fileName">The name of the file</param>
337  /// <returns><see cref="ProjectFilesResponse"/> that includes the file information</returns>
338 
339  public ProjectFilesResponse ReadProjectFile(int projectId, string fileName)
340  {
341  var request = new RestRequest("files/read", Method.POST)
342  {
343  RequestFormat = DataFormat.Json
344  };
345 
346  request.AddParameter("application/json", JsonConvert.SerializeObject(new
347  {
348  projectId,
349  name = fileName
350  }), ParameterType.RequestBody);
351 
352  ApiConnection.TryRequest(request, out ProjectFilesResponse result);
353  return result;
354  }
355 
356  /// <summary>
357  /// Gets a list of LEAN versions with their corresponding basic descriptions
358  /// </summary>
360  {
361  var request = new RestRequest("lean/versions/read", Method.POST)
362  {
363  RequestFormat = DataFormat.Json
364  };
365 
366  ApiConnection.TryRequest(request, out VersionsResponse result);
367  return result;
368  }
369 
370  /// <summary>
371  /// Delete a file in a project
372  /// </summary>
373  /// <param name="projectId">Project id to which the file belongs</param>
374  /// <param name="name">The name of the file that should be deleted</param>
375  /// <returns><see cref="RestResponse"/> that includes the information about all files in the project</returns>
376 
377  public RestResponse DeleteProjectFile(int projectId, string name)
378  {
379  var request = new RestRequest("files/delete", Method.POST)
380  {
381  RequestFormat = DataFormat.Json
382  };
383 
384  request.AddParameter("application/json", JsonConvert.SerializeObject(new
385  {
386  projectId,
387  name,
388  }), ParameterType.RequestBody);
389 
390  ApiConnection.TryRequest(request, out RestResponse result);
391  return result;
392  }
393 
394  /// <summary>
395  /// Delete a project
396  /// </summary>
397  /// <param name="projectId">Project id we own and wish to delete</param>
398  /// <returns>RestResponse indicating success</returns>
399 
400  public RestResponse DeleteProject(int projectId)
401  {
402  var request = new RestRequest("projects/delete", Method.POST)
403  {
404  RequestFormat = DataFormat.Json
405  };
406 
407  request.AddParameter("application/json", JsonConvert.SerializeObject(new
408  {
409  projectId
410  }), ParameterType.RequestBody);
411 
412  ApiConnection.TryRequest(request, out RestResponse result);
413  return result;
414  }
415 
416  /// <summary>
417  /// Create a new compile job request for this project id.
418  /// </summary>
419  /// <param name="projectId">Project id we wish to compile.</param>
420  /// <returns>Compile object result</returns>
421 
422  public Compile CreateCompile(int projectId)
423  {
424  var request = new RestRequest("compile/create", Method.POST)
425  {
426  RequestFormat = DataFormat.Json
427  };
428 
429  request.AddParameter("application/json", JsonConvert.SerializeObject(new
430  {
431  projectId
432  }), ParameterType.RequestBody);
433 
434  ApiConnection.TryRequest(request, out Compile result);
435  return result;
436  }
437 
438  /// <summary>
439  /// Read a compile packet job result.
440  /// </summary>
441  /// <param name="projectId">Project id we sent for compile</param>
442  /// <param name="compileId">Compile id return from the creation request</param>
443  /// <returns><see cref="Compile"/></returns>
444 
445  public Compile ReadCompile(int projectId, string compileId)
446  {
447  var request = new RestRequest("compile/read", Method.POST)
448  {
449  RequestFormat = DataFormat.Json
450  };
451 
452  request.AddParameter("application/json", JsonConvert.SerializeObject(new
453  {
454  projectId,
455  compileId
456  }), ParameterType.RequestBody);
457 
458  ApiConnection.TryRequest(request, out Compile result);
459  return result;
460  }
461 
462  /// <summary>
463  /// Sends a notification
464  /// </summary>
465  /// <param name="notification">The notification to send</param>
466  /// <param name="projectId">The project id</param>
467  /// <returns><see cref="RestResponse"/> containing success response and errors</returns>
468  public virtual RestResponse SendNotification(Notification notification, int projectId)
469  {
470  throw new NotImplementedException($"{nameof(Api)} does not support sending notifications");
471  }
472 
473  /// <summary>
474  /// Create a new backtest request and get the id.
475  /// </summary>
476  /// <param name="projectId">Id for the project to backtest</param>
477  /// <param name="compileId">Compile id for the project</param>
478  /// <param name="backtestName">Name for the new backtest</param>
479  /// <returns><see cref="Backtest"/>t</returns>
480 
481  public Backtest CreateBacktest(int projectId, string compileId, string backtestName)
482  {
483  var request = new RestRequest("backtests/create", Method.POST)
484  {
485  RequestFormat = DataFormat.Json
486  };
487 
488  request.AddParameter("application/json", JsonConvert.SerializeObject(new
489  {
490  projectId,
491  compileId,
492  backtestName
493  }), ParameterType.RequestBody);
494 
495  ApiConnection.TryRequest(request, out BacktestResponseWrapper result);
496 
497  // Use API Response values for Backtest Values
498  result.Backtest.Success = result.Success;
499  result.Backtest.Errors = result.Errors;
500 
501  // Return only the backtest object
502  return result.Backtest;
503  }
504 
505  /// <summary>
506  /// Read out a backtest in the project id specified.
507  /// </summary>
508  /// <param name="projectId">Project id to read</param>
509  /// <param name="backtestId">Specific backtest id to read</param>
510  /// <param name="getCharts">True will return backtest charts</param>
511  /// <returns><see cref="Backtest"/></returns>
512 
513  public Backtest ReadBacktest(int projectId, string backtestId, bool getCharts = true)
514  {
515  var request = new RestRequest("backtests/read", Method.POST)
516  {
517  RequestFormat = DataFormat.Json
518  };
519 
520  request.AddParameter("application/json", JsonConvert.SerializeObject(new
521  {
522  projectId,
523  backtestId
524  }), ParameterType.RequestBody);
525 
526  ApiConnection.TryRequest(request, out BacktestResponseWrapper result);
527 
528  if (!result.Success)
529  {
530  // place an empty place holder so we can return any errors back to the user and not just null
531  result.Backtest = new Backtest { BacktestId = backtestId };
532  }
533  // Go fetch the charts if the backtest is completed and success
534  else if (getCharts && result.Backtest.Completed)
535  {
536  // For storing our collected charts
537  var updatedCharts = new Dictionary<string, Chart>();
538 
539  // Create backtest requests for each chart that is empty
540  foreach (var chart in result.Backtest.Charts)
541  {
542  if (!chart.Value.Series.IsNullOrEmpty())
543  {
544  continue;
545  }
546 
547  var chartRequest = new RestRequest("backtests/read", Method.POST)
548  {
549  RequestFormat = DataFormat.Json
550  };
551 
552  chartRequest.AddParameter("application/json", JsonConvert.SerializeObject(new
553  {
554  projectId,
555  backtestId,
556  chart = chart.Key
557  }), ParameterType.RequestBody);
558 
559  // Add this chart to our updated collection
560  if (ApiConnection.TryRequest(chartRequest, out BacktestResponseWrapper chartResponse) && chartResponse.Success)
561  {
562  updatedCharts.Add(chart.Key, chartResponse.Backtest.Charts[chart.Key]);
563  }
564  }
565 
566  // Update our result
567  foreach(var updatedChart in updatedCharts)
568  {
569  result.Backtest.Charts[updatedChart.Key] = updatedChart.Value;
570  }
571  }
572 
573  // Use API Response values for Backtest Values
574  result.Backtest.Success = result.Success;
575  result.Backtest.Errors = result.Errors;
576 
577  // Return only the backtest object
578  return result.Backtest;
579  }
580 
581  /// <summary>
582  /// Returns the orders of the specified backtest and project id.
583  /// </summary>
584  /// <param name="projectId">Id of the project from which to read the orders</param>
585  /// <param name="backtestId">Id of the backtest from which to read the orders</param>
586  /// <param name="start">Starting index of the orders to be fetched. Required if end > 100</param>
587  /// <param name="end">Last index of the orders to be fetched. Note that end - start must be less than 100</param>
588  /// <remarks>Will throw an <see cref="WebException"/> if there are any API errors</remarks>
589  /// <returns>The list of <see cref="Order"/></returns>
590 
591  public List<ApiOrderResponse> ReadBacktestOrders(int projectId, string backtestId, int start = 0, int end = 100)
592  {
593  var request = new RestRequest("backtests/orders/read", Method.POST)
594  {
595  RequestFormat = DataFormat.Json
596  };
597 
598  request.AddParameter("application/json", JsonConvert.SerializeObject(new
599  {
600  start,
601  end,
602  projectId,
603  backtestId
604  }), ParameterType.RequestBody);
605 
606  return MakeRequestOrThrow<OrdersResponseWrapper>(request, nameof(ReadBacktestOrders)).Orders;
607  }
608 
609  /// <summary>
610  /// Returns a requested chart object from a backtest
611  /// </summary>
612  /// <param name="projectId">Project ID of the request</param>
613  /// <param name="name">The requested chart name</param>
614  /// <param name="start">The Utc start seconds timestamp of the request</param>
615  /// <param name="end">The Utc end seconds timestamp of the request</param>
616  /// <param name="count">The number of data points to request</param>
617  /// <param name="backtestId">Associated Backtest ID for this chart request</param>
618  /// <returns>The chart</returns>
619  public ReadChartResponse ReadBacktestChart(int projectId, string name, int start, int end, uint count, string backtestId)
620  {
621  var request = new RestRequest("backtests/chart/read", Method.POST)
622  {
623  RequestFormat = DataFormat.Json
624  };
625 
626  request.AddParameter("application/json", JsonConvert.SerializeObject(new
627  {
628  projectId,
629  name,
630  start,
631  end,
632  count,
633  backtestId,
634  }), ParameterType.RequestBody);
635 
636  ReadChartResponse result;
637  ApiConnection.TryRequest(request, out result);
638 
639  var finish = DateTime.UtcNow.AddMinutes(1);
640  while (DateTime.UtcNow < finish && result.Chart == null)
641  {
642  Thread.Sleep(5000);
643  ApiConnection.TryRequest(request, out result);
644  }
645 
646  return result;
647  }
648 
649  /// <summary>
650  /// Update a backtest name
651  /// </summary>
652  /// <param name="projectId">Project for the backtest we want to update</param>
653  /// <param name="backtestId">Backtest id we want to update</param>
654  /// <param name="name">Name we'd like to assign to the backtest</param>
655  /// <param name="note">Note attached to the backtest</param>
656  /// <returns><see cref="RestResponse"/></returns>
657 
658  public RestResponse UpdateBacktest(int projectId, string backtestId, string name = "", string note = "")
659  {
660  var request = new RestRequest("backtests/update", Method.POST)
661  {
662  RequestFormat = DataFormat.Json
663  };
664 
665  request.AddParameter("application/json", JsonConvert.SerializeObject(new
666  {
667  projectId,
668  backtestId,
669  name,
670  note
671  }), ParameterType.RequestBody);
672 
673  ApiConnection.TryRequest(request, out RestResponse result);
674  return result;
675  }
676 
677  /// <summary>
678  /// List all the backtest summaries for a project
679  /// </summary>
680  /// <param name="projectId">Project id we'd like to get a list of backtest for</param>
681  /// <param name="includeStatistics">True for include statistics in the response, false otherwise</param>
682  /// <returns><see cref="BacktestList"/></returns>
683 
684  public BacktestSummaryList ListBacktests(int projectId, bool includeStatistics = true)
685  {
686  var request = new RestRequest("backtests/list", Method.POST)
687  {
688  RequestFormat = DataFormat.Json
689  };
690 
691  var obj = new Dictionary<string, object>()
692  {
693  { "projectId", projectId },
694  { "includeStatistics", includeStatistics }
695  };
696 
697  request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
698 
699  ApiConnection.TryRequest(request, out BacktestSummaryList result);
700  return result;
701  }
702 
703  /// <summary>
704  /// Delete a backtest from the specified project and backtestId.
705  /// </summary>
706  /// <param name="projectId">Project for the backtest we want to delete</param>
707  /// <param name="backtestId">Backtest id we want to delete</param>
708  /// <returns><see cref="RestResponse"/></returns>
709 
710  public RestResponse DeleteBacktest(int projectId, string backtestId)
711  {
712  var request = new RestRequest("backtests/delete", Method.POST)
713  {
714  RequestFormat = DataFormat.Json
715  };
716 
717  request.AddParameter("application/json", JsonConvert.SerializeObject(new
718  {
719  projectId,
720  backtestId
721  }), ParameterType.RequestBody);
722 
723  ApiConnection.TryRequest(request, out RestResponse result);
724  return result;
725  }
726 
727  /// <summary>
728  /// Updates the tags collection for a backtest
729  /// </summary>
730  /// <param name="projectId">Project for the backtest we want to update</param>
731  /// <param name="backtestId">Backtest id we want to update</param>
732  /// <param name="tags">The new backtest tags</param>
733  /// <returns><see cref="RestResponse"/></returns>
734  public RestResponse UpdateBacktestTags(int projectId, string backtestId, IReadOnlyCollection<string> tags)
735  {
736  var request = new RestRequest("backtests/tags/update", Method.POST)
737  {
738  RequestFormat = DataFormat.Json
739  };
740 
741  request.AddParameter("application/json", JsonConvert.SerializeObject(new
742  {
743  projectId,
744  backtestId,
745  tags
746  }), ParameterType.RequestBody);
747 
748  ApiConnection.TryRequest(request, out RestResponse result);
749  return result;
750  }
751 
752  /// <summary>
753  /// Create a live algorithm.
754  /// </summary>
755  /// <param name="projectId">Id of the project on QuantConnect</param>
756  /// <param name="compileId">Id of the compilation on QuantConnect</param>
757  /// <param name="nodeId">Id of the node that will run the algorithm</param>
758  /// <param name="brokerageSettings">Dictionary with brokerage specific settings. Each brokerage requires certain specific credentials
759  /// in order to process the given orders. Each key in this dictionary represents a required field/credential
760  /// to provide to the brokerage API and its value represents the value of that field. For example: "brokerageSettings: {
761  /// "id": "Binance", "binance-api-secret": "123ABC", "binance-api-key": "ABC123"}. It is worth saying,
762  /// that this dictionary must always contain an entry whose key is "id" and its value is the name of the brokerage
763  /// (see <see cref="Brokerages.BrokerageName"/>)</param>
764  /// <param name="versionId">The version of the Lean used to run the algorithm.
765  /// -1 is master, however, sometimes this can create problems with live deployments.
766  /// If you experience problems using, try specifying the version of Lean you would like to use.</param>
767  /// <param name="dataProviders">Dictionary with data providers credentials. Each data provider requires certain credentials
768  /// in order to retrieve data from their API. Each key in this dictionary describes a data provider name
769  /// and its corresponding value is another dictionary with the required key-value pairs of credential
770  /// names and values. For example: "dataProviders: { "InteractiveBrokersBrokerage" : { "id": 12345, "environment" : "paper",
771  /// "username": "testUsername", "password": "testPassword"}}"</param>
772  /// <returns>Information regarding the new algorithm <see cref="CreateLiveAlgorithmResponse"/></returns>
774  string compileId,
775  string nodeId,
776  Dictionary<string, object> brokerageSettings,
777  string versionId = "-1",
778  Dictionary<string, object> dataProviders = null)
779  {
780  var request = new RestRequest("live/create", Method.POST)
781  {
782  RequestFormat = DataFormat.Json
783  };
784 
785  request.AddParameter("application/json", JsonConvert.SerializeObject(
787  (projectId,
788  compileId,
789  nodeId,
790  brokerageSettings,
791  versionId,
792  dataProviders
793  )
794  ), ParameterType.RequestBody);
795 
796  ApiConnection.TryRequest(request, out CreateLiveAlgorithmResponse result);
797  return result;
798  }
799 
800  /// <summary>
801  /// Create a live algorithm.
802  /// </summary>
803  /// <param name="projectId">Id of the project on QuantConnect</param>
804  /// <param name="compileId">Id of the compilation on QuantConnect</param>
805  /// <param name="nodeId">Id of the node that will run the algorithm</param>
806  /// <param name="brokerageSettings">Python Dictionary with brokerage specific settings. Each brokerage requires certain specific credentials
807  /// in order to process the given orders. Each key in this dictionary represents a required field/credential
808  /// to provide to the brokerage API and its value represents the value of that field. For example: "brokerageSettings: {
809  /// "id": "Binance", "binance-api-secret": "123ABC", "binance-api-key": "ABC123"}. It is worth saying,
810  /// that this dictionary must always contain an entry whose key is "id" and its value is the name of the brokerage
811  /// (see <see cref="Brokerages.BrokerageName"/>)</param>
812  /// <param name="versionId">The version of the Lean used to run the algorithm.
813  /// -1 is master, however, sometimes this can create problems with live deployments.
814  /// If you experience problems using, try specifying the version of Lean you would like to use.</param>
815  /// <param name="dataProviders">Python Dictionary with data providers credentials. Each data provider requires certain credentials
816  /// in order to retrieve data from their API. Each key in this dictionary describes a data provider name
817  /// and its corresponding value is another dictionary with the required key-value pairs of credential
818  /// names and values. For example: "dataProviders: { "InteractiveBrokersBrokerage" : { "id": 12345, "environment" : "paper",
819  /// "username": "testUsername", "password": "testPassword"}}"</param>
820  /// <returns>Information regarding the new algorithm <see cref="CreateLiveAlgorithmResponse"/></returns>
821 
822  public CreateLiveAlgorithmResponse CreateLiveAlgorithm(int projectId, string compileId, string nodeId, PyObject brokerageSettings, string versionId = "-1", PyObject dataProviders = null)
823  {
824  return CreateLiveAlgorithm(projectId, compileId, nodeId, ConvertToDictionary(brokerageSettings), versionId, dataProviders != null ? ConvertToDictionary(dataProviders) : null);
825  }
826 
827  /// <summary>
828  /// Converts a given Python dictionary into a C# <see cref="Dictionary{string, object}"/>
829  /// </summary>
830  /// <param name="brokerageSettings">Python dictionary to be converted</param>
831  private static Dictionary<string, object> ConvertToDictionary(PyObject brokerageSettings)
832  {
833  using (Py.GIL())
834  {
835  var stringBrokerageSettings = brokerageSettings.ToString();
836  return JsonConvert.DeserializeObject<Dictionary<string, object>>(stringBrokerageSettings);
837  }
838  }
839 
840  /// <summary>
841  /// Get a list of live running algorithms for user
842  /// </summary>
843  /// <param name="status">Filter the statuses of the algorithms returned from the api</param>
844  /// <param name="startTime">Earliest launched time of the algorithms returned by the Api</param>
845  /// <param name="endTime">Latest launched time of the algorithms returned by the Api</param>
846  /// <returns><see cref="LiveList"/></returns>
847 
849  DateTime? startTime = null,
850  DateTime? endTime = null)
851  {
852  // Only the following statuses are supported by the Api
853  if (status.HasValue &&
854  status != AlgorithmStatus.Running &&
855  status != AlgorithmStatus.RuntimeError &&
856  status != AlgorithmStatus.Stopped &&
857  status != AlgorithmStatus.Liquidated)
858  {
859  throw new ArgumentException(
860  "The Api only supports Algorithm Statuses of Running, Stopped, RuntimeError and Liquidated");
861  }
862 
863  var request = new RestRequest("live/list", Method.POST)
864  {
865  RequestFormat = DataFormat.Json
866  };
867 
868  var epochStartTime = startTime == null ? 0 : Time.DateTimeToUnixTimeStamp(startTime.Value);
869  var epochEndTime = endTime == null ? Time.DateTimeToUnixTimeStamp(DateTime.UtcNow) : Time.DateTimeToUnixTimeStamp(endTime.Value);
870 
871  JObject obj = new JObject
872  {
873  { "start", epochStartTime },
874  { "end", epochEndTime }
875  };
876 
877  if (status.HasValue)
878  {
879  obj.Add("status", status.ToString());
880  }
881 
882  request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
883 
884  ApiConnection.TryRequest(request, out LiveList result);
885  return result;
886  }
887 
888  /// <summary>
889  /// Read out a live algorithm in the project id specified.
890  /// </summary>
891  /// <param name="projectId">Project id to read</param>
892  /// <param name="deployId">Specific instance id to read</param>
893  /// <returns><see cref="LiveAlgorithmResults"/></returns>
894 
895  public LiveAlgorithmResults ReadLiveAlgorithm(int projectId, string deployId)
896  {
897  var request = new RestRequest("live/read", Method.POST)
898  {
899  RequestFormat = DataFormat.Json
900  };
901 
902  request.AddParameter("application/json", JsonConvert.SerializeObject(new
903  {
904  projectId,
905  deployId
906  }), ParameterType.RequestBody);
907 
908  ApiConnection.TryRequest(request, out LiveAlgorithmResults result);
909  return result;
910  }
911 
912  /// <summary>
913  /// Read out the portfolio state of a live algorithm
914  /// </summary>
915  /// <param name="projectId">Id of the project from which to read the live algorithm</param>
916  /// <returns><see cref="PortfolioResponse"/></returns>
917  public PortfolioResponse ReadLivePortfolio(int projectId)
918  {
919  var request = new RestRequest("live/portfolio/read", Method.POST)
920  {
921  RequestFormat = DataFormat.Json
922  };
923 
924  request.AddParameter("application/json", JsonConvert.SerializeObject(new
925  {
926  projectId
927  }), ParameterType.RequestBody);
928 
929  ApiConnection.TryRequest(request, out PortfolioResponse result);
930  return result;
931  }
932 
933  /// <summary>
934  /// Returns the orders of the specified project id live algorithm.
935  /// </summary>
936  /// <param name="projectId">Id of the project from which to read the live orders</param>
937  /// <param name="start">Starting index of the orders to be fetched. Required if end > 100</param>
938  /// <param name="end">Last index of the orders to be fetched. Note that end - start must be less than 100</param>
939  /// <remarks>Will throw an <see cref="WebException"/> if there are any API errors</remarks>
940  /// <returns>The list of <see cref="Order"/></returns>
941 
942  public List<ApiOrderResponse> ReadLiveOrders(int projectId, int start = 0, int end = 100)
943  {
944  var request = new RestRequest("live/orders/read", Method.POST)
945  {
946  RequestFormat = DataFormat.Json
947  };
948 
949  request.AddParameter("application/json", JsonConvert.SerializeObject(new
950  {
951  start,
952  end,
953  projectId
954  }), ParameterType.RequestBody);
955 
956  return MakeRequestOrThrow<OrdersResponseWrapper>(request, nameof(ReadLiveOrders)).Orders;
957  }
958 
959  /// <summary>
960  /// Liquidate a live algorithm from the specified project and deployId.
961  /// </summary>
962  /// <param name="projectId">Project for the live instance we want to stop</param>
963  /// <returns><see cref="RestResponse"/></returns>
964 
965  public RestResponse LiquidateLiveAlgorithm(int projectId)
966  {
967  var request = new RestRequest("live/update/liquidate", Method.POST)
968  {
969  RequestFormat = DataFormat.Json
970  };
971 
972  request.AddParameter("application/json", JsonConvert.SerializeObject(new
973  {
974  projectId
975  }), ParameterType.RequestBody);
976 
977  ApiConnection.TryRequest(request, out RestResponse result);
978  return result;
979  }
980 
981  /// <summary>
982  /// Stop a live algorithm from the specified project and deployId.
983  /// </summary>
984  /// <param name="projectId">Project for the live instance we want to stop</param>
985  /// <returns><see cref="RestResponse"/></returns>
986 
987  public RestResponse StopLiveAlgorithm(int projectId)
988  {
989  var request = new RestRequest("live/update/stop", Method.POST)
990  {
991  RequestFormat = DataFormat.Json
992  };
993 
994  request.AddParameter("application/json", JsonConvert.SerializeObject(new
995  {
996  projectId
997  }), ParameterType.RequestBody);
998 
999  ApiConnection.TryRequest(request, out RestResponse result);
1000  return result;
1001  }
1002 
1003  /// <summary>
1004  /// Gets the logs of a specific live algorithm
1005  /// </summary>
1006  /// <param name="projectId">Project Id of the live running algorithm</param>
1007  /// <param name="algorithmId">Algorithm Id of the live running algorithm</param>
1008  /// <param name="startLine">Start line of logs to read</param>
1009  /// <param name="endLine">End line of logs to read</param>
1010  /// <returns><see cref="LiveLog"/> List of strings that represent the logs of the algorithm</returns>
1011  public LiveLog ReadLiveLogs(int projectId, string algorithmId, int startLine, int endLine)
1012  {
1013  var logLinesNumber = endLine - startLine;
1014  if (logLinesNumber > 250)
1015  {
1016  throw new ArgumentException($"The maximum number of log lines allowed is 250. But the number of log lines was {logLinesNumber}.");
1017  }
1018 
1019  var request = new RestRequest("live/logs/read", Method.POST)
1020  {
1021  RequestFormat = DataFormat.Json
1022  };
1023 
1024  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1025  {
1026  format = "json",
1027  projectId,
1028  algorithmId,
1029  startLine,
1030  endLine,
1031  }), ParameterType.RequestBody);
1032 
1033  ApiConnection.TryRequest(request, out LiveLog result);
1034  return result;
1035  }
1036 
1037  /// <summary>
1038  /// Returns a chart object from a live algorithm
1039  /// </summary>
1040  /// <param name="projectId">Project ID of the request</param>
1041  /// <param name="name">The requested chart name</param>
1042  /// <param name="start">The Utc start seconds timestamp of the request</param>
1043  /// <param name="end">The Utc end seconds timestamp of the request</param>
1044  /// <param name="count">The number of data points to request</param>
1045  /// <returns>The chart</returns>
1046  public ReadChartResponse ReadLiveChart(int projectId, string name, int start, int end, uint count)
1047  {
1048  var request = new RestRequest("live/chart/read", Method.POST)
1049  {
1050  RequestFormat = DataFormat.Json
1051  };
1052 
1053  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1054  {
1055  projectId,
1056  name,
1057  start,
1058  end,
1059  count
1060  }), ParameterType.RequestBody);
1061 
1062  ReadChartResponse result = default;
1063  ApiConnection.TryRequest(request, out result);
1064 
1065  var finish = DateTime.UtcNow.AddMinutes(1);
1066  while(DateTime.UtcNow < finish && result.Chart == null)
1067  {
1068  Thread.Sleep(5000);
1069  ApiConnection.TryRequest(request, out result);
1070  }
1071  return result;
1072  }
1073 
1074  /// <summary>
1075  /// Read out the insights of a live algorithm
1076  /// </summary>
1077  /// <param name="projectId">Id of the project from which to read the live algorithm</param>
1078  /// <param name="start">Starting index of the insights to be fetched</param>
1079  /// <param name="end">Last index of the insights to be fetched. Note that end - start must be less than 100</param>
1080  /// <returns><see cref="InsightResponse"/></returns>
1081  /// <exception cref="ArgumentException"></exception>
1082  public InsightResponse ReadLiveInsights(int projectId, int start = 0, int end = 0)
1083  {
1084  var request = new RestRequest("live/insights/read", Method.POST)
1085  {
1086  RequestFormat = DataFormat.Json,
1087  };
1088 
1089  var diff = end - start;
1090  if (diff > 100)
1091  {
1092  throw new ArgumentException($"The difference between the start and end index of the insights must be smaller than 100, but it was {diff}.");
1093  }
1094  else if (end == 0)
1095  {
1096  end = start + 100;
1097  }
1098 
1099  JObject obj = new JObject
1100  {
1101  { "projectId", projectId },
1102  { "start", start },
1103  { "end", end },
1104  };
1105 
1106  request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
1107 
1108  ApiConnection.TryRequest(request, out InsightResponse result);
1109  return result;
1110  }
1111 
1112  /// <summary>
1113  /// Gets the link to the downloadable data.
1114  /// </summary>
1115  /// <param name="filePath">File path representing the data requested</param>
1116  /// <param name="organizationId">Organization to download from</param>
1117  /// <returns><see cref="DataLink"/> to the downloadable data.</returns>
1118  public DataLink ReadDataLink(string filePath, string organizationId)
1119  {
1120  if (filePath == null)
1121  {
1122  throw new ArgumentException("Api.ReadDataLink(): Filepath must not be null");
1123  }
1124 
1125  // Prepare filePath for request
1126  filePath = FormatPathForDataRequest(filePath);
1127 
1128  var request = new RestRequest("data/read", Method.POST)
1129  {
1130  RequestFormat = DataFormat.Json
1131  };
1132 
1133  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1134  {
1135  format = "link",
1136  filePath,
1137  organizationId
1138  }), ParameterType.RequestBody);
1139 
1140  ApiConnection.TryRequest(request, out DataLink result);
1141  return result;
1142  }
1143 
1144  /// <summary>
1145  /// Get valid data entries for a given filepath from data/list
1146  /// </summary>
1147  /// <returns></returns>
1148  public DataList ReadDataDirectory(string filePath)
1149  {
1150  if (filePath == null)
1151  {
1152  throw new ArgumentException("Api.ReadDataDirectory(): Filepath must not be null");
1153  }
1154 
1155  // Prepare filePath for request
1156  filePath = FormatPathForDataRequest(filePath);
1157 
1158  // Verify the filePath for this request is at least three directory deep
1159  // (requirement of endpoint)
1160  if (filePath.Count(x => x == '/') < 3)
1161  {
1162  throw new ArgumentException($"Api.ReadDataDirectory(): Data directory requested must be at least" +
1163  $" three directories deep. FilePath: {filePath}");
1164  }
1165 
1166  var request = new RestRequest("data/list", Method.POST)
1167  {
1168  RequestFormat = DataFormat.Json
1169  };
1170 
1171  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1172  {
1173  filePath
1174  }), ParameterType.RequestBody);
1175 
1176  ApiConnection.TryRequest(request, out DataList result);
1177  return result;
1178  }
1179 
1180  /// <summary>
1181  /// Gets data prices from data/prices
1182  /// </summary>
1183  public DataPricesList ReadDataPrices(string organizationId)
1184  {
1185  var request = new RestRequest("data/prices", Method.POST)
1186  {
1187  RequestFormat = DataFormat.Json
1188  };
1189 
1190  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1191  {
1192  organizationId
1193  }), ParameterType.RequestBody);
1194 
1195  ApiConnection.TryRequest(request, out DataPricesList result);
1196  return result;
1197  }
1198 
1199  /// <summary>
1200  /// Read out the report of a backtest in the project id specified.
1201  /// </summary>
1202  /// <param name="projectId">Project id to read</param>
1203  /// <param name="backtestId">Specific backtest id to read</param>
1204  /// <returns><see cref="BacktestReport"/></returns>
1205  public BacktestReport ReadBacktestReport(int projectId, string backtestId)
1206  {
1207  var request = new RestRequest("backtests/read/report", Method.POST)
1208  {
1209  RequestFormat = DataFormat.Json
1210  };
1211 
1212  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1213  {
1214  backtestId,
1215  projectId
1216  }), ParameterType.RequestBody);
1217 
1218  BacktestReport report = new BacktestReport();
1219  var finish = DateTime.UtcNow.AddMinutes(1);
1220  while (DateTime.UtcNow < finish && !report.Success)
1221  {
1222  Thread.Sleep(10000);
1223  ApiConnection.TryRequest(request, out report);
1224  }
1225  return report;
1226  }
1227 
1228  /// <summary>
1229  /// Method to purchase and download data from QuantConnect
1230  /// </summary>
1231  /// <param name="filePath">File path representing the data requested</param>
1232  /// <param name="organizationId">Organization to buy the data with</param>
1233  /// <returns>A <see cref="bool"/> indicating whether the data was successfully downloaded or not.</returns>
1234 
1235  public bool DownloadData(string filePath, string organizationId)
1236  {
1237  // Get a link to the data
1238  var dataLink = ReadDataLink(filePath, organizationId);
1239 
1240  // Make sure the link was successfully retrieved
1241  if (!dataLink.Success)
1242  {
1243  Log.Trace($"Api.DownloadData(): Failed to get link for {filePath}. " +
1244  $"Errors: {string.Join(',', dataLink.Errors)}");
1245  return false;
1246  }
1247 
1248  // Make sure the directory exist before writing
1249  var directory = Path.GetDirectoryName(filePath);
1250  if (!Directory.Exists(directory))
1251  {
1252  Directory.CreateDirectory(directory);
1253  }
1254 
1255  var client = BorrowClient();
1256  try
1257  {
1258  // Download the file
1259  var uri = new Uri(dataLink.Link);
1260  using var dataStream = client.Value.GetStreamAsync(uri);
1261 
1262  using var fileStream = new FileStream(FileExtension.ToNormalizedPath(filePath), FileMode.Create);
1263  dataStream.Result.CopyTo(fileStream);
1264  }
1265  catch
1266  {
1267  Log.Error($"Api.DownloadData(): Failed to download zip for path ({filePath})");
1268  return false;
1269  }
1270  finally
1271  {
1272  ReturnClient(client);
1273  }
1274 
1275  return true;
1276  }
1277 
1278  /// <summary>
1279  /// Get the algorithm status from the user with this algorithm id.
1280  /// </summary>
1281  /// <param name="algorithmId">String algorithm id we're searching for.</param>
1282  /// <returns>Algorithm status enum</returns>
1283 
1284  public virtual AlgorithmControl GetAlgorithmStatus(string algorithmId)
1285  {
1286  return new AlgorithmControl()
1287  {
1288  ChartSubscription = "*"
1289  };
1290  }
1291 
1292  /// <summary>
1293  /// Algorithm passes back its current status to the UX.
1294  /// </summary>
1295  /// <param name="status">Status of the current algorithm</param>
1296  /// <param name="algorithmId">String algorithm id we're setting.</param>
1297  /// <param name="message">Message for the algorithm status event</param>
1298  /// <returns>Algorithm status enum</returns>
1299 
1300  public virtual void SetAlgorithmStatus(string algorithmId, AlgorithmStatus status, string message = "")
1301  {
1302  //
1303  }
1304 
1305  /// <summary>
1306  /// Send the statistics to storage for performance tracking.
1307  /// </summary>
1308  /// <param name="algorithmId">Identifier for algorithm</param>
1309  /// <param name="unrealized">Unrealized gainloss</param>
1310  /// <param name="fees">Total fees</param>
1311  /// <param name="netProfit">Net profi</param>
1312  /// <param name="holdings">Algorithm holdings</param>
1313  /// <param name="equity">Total equity</param>
1314  /// <param name="netReturn">Net return for the deployment</param>
1315  /// <param name="volume">Volume traded</param>
1316  /// <param name="trades">Total trades since inception</param>
1317  /// <param name="sharpe">Sharpe ratio since inception</param>
1318 
1319  public virtual void SendStatistics(string algorithmId, decimal unrealized, decimal fees, decimal netProfit, decimal holdings, decimal equity, decimal netReturn, decimal volume, int trades, double sharpe)
1320  {
1321  //
1322  }
1323 
1324  /// <summary>
1325  /// Send an email to the user associated with the specified algorithm id
1326  /// </summary>
1327  /// <param name="algorithmId">The algorithm id</param>
1328  /// <param name="subject">The email subject</param>
1329  /// <param name="body">The email message body</param>
1330 
1331  public virtual void SendUserEmail(string algorithmId, string subject, string body)
1332  {
1333  //
1334  }
1335 
1336  /// <summary>
1337  /// Local implementation for downloading data to algorithms
1338  /// </summary>
1339  /// <param name="address">URL to download</param>
1340  /// <param name="headers">KVP headers</param>
1341  /// <param name="userName">Username for basic authentication</param>
1342  /// <param name="password">Password for basic authentication</param>
1343  /// <returns></returns>
1344  public virtual string Download(string address, IEnumerable<KeyValuePair<string, string>> headers, string userName, string password)
1345  {
1346  return Encoding.UTF8.GetString(DownloadBytes(address, headers, userName, password));
1347  }
1348 
1349  /// <summary>
1350  /// Local implementation for downloading data to algorithms
1351  /// </summary>
1352  /// <param name="address">URL to download</param>
1353  /// <param name="headers">KVP headers</param>
1354  /// <param name="userName">Username for basic authentication</param>
1355  /// <param name="password">Password for basic authentication</param>
1356  /// <returns>A stream from which the data can be read</returns>
1357  /// <remarks>Stream.Close() most be called to avoid running out of resources</remarks>
1358  public virtual byte[] DownloadBytes(string address, IEnumerable<KeyValuePair<string, string>> headers, string userName, string password)
1359  {
1360  var client = BorrowClient();
1361  try
1362  {
1363  client.Value.DefaultRequestHeaders.Clear();
1364 
1365  // Add a user agent header in case the requested URI contains a query.
1366  client.Value.DefaultRequestHeaders.TryAddWithoutValidation("user-agent", "QCAlgorithm.Download(): User Agent Header");
1367 
1368  if (headers != null)
1369  {
1370  foreach (var header in headers)
1371  {
1372  client.Value.DefaultRequestHeaders.Add(header.Key, header.Value);
1373  }
1374  }
1375 
1376  if (!userName.IsNullOrEmpty() || !password.IsNullOrEmpty())
1377  {
1378  var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{userName}:{password}"));
1379  client.Value.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
1380  }
1381 
1382  return client.Value.GetByteArrayAsync(new Uri(address)).Result;
1383  }
1384  catch (Exception exception)
1385  {
1386  var message = $"Api.DownloadBytes(): Failed to download data from {address}";
1387  if (!userName.IsNullOrEmpty() || !password.IsNullOrEmpty())
1388  {
1389  message += $" with username: {userName} and password {password}";
1390  }
1391 
1392  throw new WebException($"{message}. Please verify the source for missing http:// or https://", exception);
1393  }
1394  finally
1395  {
1396  client.Value.DefaultRequestHeaders.Clear();
1397  ReturnClient(client);
1398  }
1399  }
1400 
1401  /// <summary>
1402  /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
1403  /// </summary>
1404  /// <filterpriority>2</filterpriority>
1405  public virtual void Dispose()
1406  {
1407  // Dispose of the HttpClient pool
1408  _clientPool.CompleteAdding();
1409  foreach (var client in _clientPool.GetConsumingEnumerable())
1410  {
1411  if (client.IsValueCreated)
1412  {
1413  client.Value.DisposeSafely();
1414  }
1415  }
1416  _clientPool.DisposeSafely();
1417  }
1418 
1419  /// <summary>
1420  /// Generate a secure hash for the authorization headers.
1421  /// </summary>
1422  /// <returns>Time based hash of user token and timestamp.</returns>
1423  public static string CreateSecureHash(int timestamp, string token)
1424  {
1425  // Create a new hash using current UTC timestamp.
1426  // Hash must be generated fresh each time.
1427  var data = $"{token}:{timestamp.ToStringInvariant()}";
1428  return data.ToSHA256();
1429  }
1430 
1431  /// <summary>
1432  /// Will read the organization account status
1433  /// </summary>
1434  /// <param name="organizationId">The target organization id, if null will return default organization</param>
1435  public Account ReadAccount(string organizationId = null)
1436  {
1437  var request = new RestRequest("account/read", Method.POST)
1438  {
1439  RequestFormat = DataFormat.Json
1440  };
1441 
1442  if (organizationId != null)
1443  {
1444  request.AddParameter("application/json", JsonConvert.SerializeObject(new { organizationId }), ParameterType.RequestBody);
1445  }
1446 
1447  ApiConnection.TryRequest(request, out Account account);
1448  return account;
1449  }
1450 
1451  /// <summary>
1452  /// Fetch organization data from web API
1453  /// </summary>
1454  /// <param name="organizationId"></param>
1455  /// <returns></returns>
1456  public Organization ReadOrganization(string organizationId = null)
1457  {
1458  var request = new RestRequest("organizations/read", Method.POST)
1459  {
1460  RequestFormat = DataFormat.Json
1461  };
1462 
1463  if (organizationId != null)
1464  {
1465  request.AddParameter("application/json", JsonConvert.SerializeObject(new { organizationId }), ParameterType.RequestBody);
1466  }
1467 
1468  ApiConnection.TryRequest(request, out OrganizationResponse response);
1469  return response.Organization;
1470  }
1471 
1472  /// <summary>
1473  /// Estimate optimization with the specified parameters via QuantConnect.com API
1474  /// </summary>
1475  /// <param name="projectId">Project ID of the project the optimization belongs to</param>
1476  /// <param name="name">Name of the optimization</param>
1477  /// <param name="target">Target of the optimization, see examples in <see cref="PortfolioStatistics"/></param>
1478  /// <param name="targetTo">Target extremum of the optimization, for example "max" or "min"</param>
1479  /// <param name="targetValue">Optimization target value</param>
1480  /// <param name="strategy">Optimization strategy, <see cref="GridSearchOptimizationStrategy"/></param>
1481  /// <param name="compileId">Optimization compile ID</param>
1482  /// <param name="parameters">Optimization parameters</param>
1483  /// <param name="constraints">Optimization constraints</param>
1484  /// <returns>Estimate object from the API.</returns>
1486  int projectId,
1487  string name,
1488  string target,
1489  string targetTo,
1490  decimal? targetValue,
1491  string strategy,
1492  string compileId,
1493  HashSet<OptimizationParameter> parameters,
1494  IReadOnlyList<Constraint> constraints)
1495  {
1496  var request = new RestRequest("optimizations/estimate", Method.POST)
1497  {
1498  RequestFormat = DataFormat.Json
1499  };
1500 
1501  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1502  {
1503  projectId,
1504  name,
1505  target,
1506  targetTo,
1507  targetValue,
1508  strategy,
1509  compileId,
1510  parameters,
1511  constraints
1512  }, SerializerSettings), ParameterType.RequestBody);
1513 
1514  ApiConnection.TryRequest(request, out EstimateResponseWrapper response);
1515  return response.Estimate;
1516  }
1517 
1518  /// <summary>
1519  /// Create an optimization with the specified parameters via QuantConnect.com API
1520  /// </summary>
1521  /// <param name="projectId">Project ID of the project the optimization belongs to</param>
1522  /// <param name="name">Name of the optimization</param>
1523  /// <param name="target">Target of the optimization, see examples in <see cref="PortfolioStatistics"/></param>
1524  /// <param name="targetTo">Target extremum of the optimization, for example "max" or "min"</param>
1525  /// <param name="targetValue">Optimization target value</param>
1526  /// <param name="strategy">Optimization strategy, <see cref="GridSearchOptimizationStrategy"/></param>
1527  /// <param name="compileId">Optimization compile ID</param>
1528  /// <param name="parameters">Optimization parameters</param>
1529  /// <param name="constraints">Optimization constraints</param>
1530  /// <param name="estimatedCost">Estimated cost for optimization</param>
1531  /// <param name="nodeType">Optimization node type <see cref="OptimizationNodes"/></param>
1532  /// <param name="parallelNodes">Number of parallel nodes for optimization</param>
1533  /// <returns>BaseOptimization object from the API.</returns>
1535  int projectId,
1536  string name,
1537  string target,
1538  string targetTo,
1539  decimal? targetValue,
1540  string strategy,
1541  string compileId,
1542  HashSet<OptimizationParameter> parameters,
1543  IReadOnlyList<Constraint> constraints,
1544  decimal estimatedCost,
1545  string nodeType,
1546  int parallelNodes)
1547  {
1548  var request = new RestRequest("optimizations/create", Method.POST)
1549  {
1550  RequestFormat = DataFormat.Json
1551  };
1552 
1553  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1554  {
1555  projectId,
1556  name,
1557  target,
1558  targetTo,
1559  targetValue,
1560  strategy,
1561  compileId,
1562  parameters,
1563  constraints,
1564  estimatedCost,
1565  nodeType,
1566  parallelNodes
1567  }, SerializerSettings), ParameterType.RequestBody);
1568 
1569  ApiConnection.TryRequest(request, out OptimizationList result);
1570  return result.Optimizations.FirstOrDefault();
1571  }
1572 
1573  /// <summary>
1574  /// List all the optimizations for a project
1575  /// </summary>
1576  /// <param name="projectId">Project id we'd like to get a list of optimizations for</param>
1577  /// <returns>A list of BaseOptimization objects, <see cref="BaseOptimization"/></returns>
1578  public List<OptimizationSummary> ListOptimizations(int projectId)
1579  {
1580  var request = new RestRequest("optimizations/list", Method.POST)
1581  {
1582  RequestFormat = DataFormat.Json
1583  };
1584 
1585  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1586  {
1587  projectId,
1588  }), ParameterType.RequestBody);
1589 
1590  ApiConnection.TryRequest(request, out OptimizationList result);
1591  return result.Optimizations;
1592  }
1593 
1594  /// <summary>
1595  /// Read an optimization
1596  /// </summary>
1597  /// <param name="optimizationId">Optimization id for the optimization we want to read</param>
1598  /// <returns><see cref="Optimization"/></returns>
1599  public Optimization ReadOptimization(string optimizationId)
1600  {
1601  var request = new RestRequest("optimizations/read", Method.POST)
1602  {
1603  RequestFormat = DataFormat.Json
1604  };
1605 
1606  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1607  {
1608  optimizationId
1609  }), ParameterType.RequestBody);
1610 
1611  ApiConnection.TryRequest(request, out OptimizationResponseWrapper response);
1612  return response.Optimization;
1613  }
1614 
1615  /// <summary>
1616  /// Abort an optimization
1617  /// </summary>
1618  /// <param name="optimizationId">Optimization id for the optimization we want to abort</param>
1619  /// <returns><see cref="RestResponse"/></returns>
1620  public RestResponse AbortOptimization(string optimizationId)
1621  {
1622  var request = new RestRequest("optimizations/abort", Method.POST)
1623  {
1624  RequestFormat = DataFormat.Json
1625  };
1626 
1627  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1628  {
1629  optimizationId
1630  }), ParameterType.RequestBody);
1631 
1632  ApiConnection.TryRequest(request, out RestResponse result);
1633  return result;
1634  }
1635 
1636  /// <summary>
1637  /// Update an optimization
1638  /// </summary>
1639  /// <param name="optimizationId">Optimization id we want to update</param>
1640  /// <param name="name">Name we'd like to assign to the optimization</param>
1641  /// <returns><see cref="RestResponse"/></returns>
1642  public RestResponse UpdateOptimization(string optimizationId, string name = null)
1643  {
1644  var request = new RestRequest("optimizations/update", Method.POST)
1645  {
1646  RequestFormat = DataFormat.Json
1647  };
1648 
1649  var obj = new JObject
1650  {
1651  { "optimizationId", optimizationId }
1652  };
1653 
1654  if (name.HasValue())
1655  {
1656  obj.Add("name", name);
1657  }
1658 
1659  request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
1660 
1661  ApiConnection.TryRequest(request, out RestResponse result);
1662  return result;
1663  }
1664 
1665  /// <summary>
1666  /// Delete an optimization
1667  /// </summary>
1668  /// <param name="optimizationId">Optimization id for the optimization we want to delete</param>
1669  /// <returns><see cref="RestResponse"/></returns>
1670  public RestResponse DeleteOptimization(string optimizationId)
1671  {
1672  var request = new RestRequest("optimizations/delete", Method.POST)
1673  {
1674  RequestFormat = DataFormat.Json
1675  };
1676 
1677  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1678  {
1679  optimizationId
1680  }), ParameterType.RequestBody);
1681 
1682  ApiConnection.TryRequest(request, out RestResponse result);
1683  return result;
1684  }
1685 
1686  /// <summary>
1687  /// Download the object store files associated with the given organization ID and key
1688  /// </summary>
1689  /// <param name="organizationId">Organization ID we would like to get the Object Store files from</param>
1690  /// <param name="keys">Keys for the Object Store files</param>
1691  /// <param name="destinationFolder">Folder in which the object store files will be stored</param>
1692  /// <returns>True if the object store files were retrieved correctly, false otherwise</returns>
1693  public bool GetObjectStore(string organizationId, List<string> keys, string destinationFolder = null)
1694  {
1695  var request = new RestRequest("object/get", Method.POST)
1696  {
1697  RequestFormat = DataFormat.Json
1698  };
1699 
1700  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1701  {
1702  organizationId,
1703  keys
1704  }), ParameterType.RequestBody);
1705 
1706  ApiConnection.TryRequest(request, out GetObjectStoreResponse result);
1707 
1708  if (result == null || !result.Success)
1709  {
1710  Log.Error($"Api.GetObjectStore(): Failed to get the jobId to request the download URL for the object store files."
1711  + (result != null ? $" Errors: {string.Join(",", result.Errors)}" : ""));
1712  return false;
1713  }
1714 
1715  var jobId = result.JobId;
1716  var getUrlRequest = new RestRequest("object/get", Method.POST)
1717  {
1718  RequestFormat = DataFormat.Json
1719  };
1720  getUrlRequest.AddParameter("application/json", JsonConvert.SerializeObject(new
1721  {
1722  organizationId,
1723  jobId
1724  }), ParameterType.RequestBody);
1725 
1726  var frontier = DateTime.UtcNow + TimeSpan.FromMinutes(5);
1727  while (string.IsNullOrEmpty(result?.Url) && (DateTime.UtcNow < frontier))
1728  {
1729  Thread.Sleep(3000);
1730  ApiConnection.TryRequest(getUrlRequest, out result);
1731  }
1732 
1733  if (result == null || string.IsNullOrEmpty(result.Url))
1734  {
1735  Log.Error($"Api.GetObjectStore(): Failed to get the download URL from the jobId {jobId}."
1736  + (result != null ? $" Errors: {string.Join(",", result.Errors)}" : ""));
1737  return false;
1738  }
1739 
1740  var directory = destinationFolder ?? Directory.GetCurrentDirectory();
1741  var client = BorrowClient();
1742 
1743  try
1744  {
1745  if (client.Value.Timeout != TimeSpan.FromMinutes(20))
1746  {
1747  client.Value.Timeout = TimeSpan.FromMinutes(20);
1748  }
1749 
1750  // Download the file
1751  var uri = new Uri(result.Url);
1752  using var byteArray = client.Value.GetByteArrayAsync(uri);
1753 
1754  Compression.UnzipToFolder(byteArray.Result, directory);
1755  }
1756  catch (Exception e)
1757  {
1758  Log.Error($"Api.GetObjectStore(): Failed to download zip for path ({directory}). Error: {e.Message}");
1759  return false;
1760  }
1761  finally
1762  {
1763  ReturnClient(client);
1764  }
1765 
1766  return true;
1767  }
1768 
1769  /// <summary>
1770  /// Get Object Store properties given the organization ID and the Object Store key
1771  /// </summary>
1772  /// <param name="organizationId">Organization ID we would like to get the Object Store from</param>
1773  /// <param name="key">Key for the Object Store file</param>
1774  /// <returns><see cref="PropertiesObjectStoreResponse"/></returns>
1775  /// <remarks>It does not work when the object store is a directory</remarks>
1776  public PropertiesObjectStoreResponse GetObjectStoreProperties(string organizationId, string key)
1777  {
1778  var request = new RestRequest("object/properties", Method.POST)
1779  {
1780  RequestFormat = DataFormat.Json
1781  };
1782 
1783  request.AddParameter("organizationId", organizationId);
1784  request.AddParameter("key", key);
1785 
1786  ApiConnection.TryRequest(request, out PropertiesObjectStoreResponse result);
1787 
1788  if (result == null || !result.Success)
1789  {
1790  Log.Error($"Api.ObjectStore(): Failed to get the properties for the object store key {key}." + (result != null ? $" Errors: {string.Join(",", result.Errors)}" : ""));
1791  }
1792  return result;
1793  }
1794 
1795  /// <summary>
1796  /// Upload files to the Object Store
1797  /// </summary>
1798  /// <param name="organizationId">Organization ID we would like to upload the file to</param>
1799  /// <param name="key">Key to the Object Store file</param>
1800  /// <param name="objectData">File (as an array of bytes) to be uploaded</param>
1801  /// <returns><see cref="RestResponse"/></returns>
1802  public RestResponse SetObjectStore(string organizationId, string key, byte[] objectData)
1803  {
1804  var request = new RestRequest("object/set", Method.POST)
1805  {
1806  RequestFormat = DataFormat.Json
1807  };
1808 
1809  request.AddParameter("organizationId", organizationId);
1810  request.AddParameter("key", key);
1811  request.AddFileBytes("objectData", objectData, "objectData");
1812  request.AlwaysMultipartFormData = true;
1813 
1814  ApiConnection.TryRequest(request, out RestResponse result);
1815  return result;
1816  }
1817 
1818  /// <summary>
1819  /// Request to delete Object Store metadata of a specific organization and key
1820  /// </summary>
1821  /// <param name="organizationId">Organization ID we would like to delete the Object Store file from</param>
1822  /// <param name="key">Key to the Object Store file</param>
1823  /// <returns><see cref="RestResponse"/></returns>
1824  public RestResponse DeleteObjectStore(string organizationId, string key)
1825  {
1826  var request = new RestRequest("object/delete", Method.POST)
1827  {
1828  RequestFormat = DataFormat.Json
1829  };
1830 
1831  var obj = new Dictionary<string, object>
1832  {
1833  { "organizationId", organizationId },
1834  { "key", key }
1835  };
1836 
1837  request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
1838 
1839  ApiConnection.TryRequest(request, out RestResponse result);
1840  return result;
1841  }
1842 
1843  /// <summary>
1844  /// Request to list Object Store files of a specific organization and path
1845  /// </summary>
1846  /// <param name="organizationId">Organization ID we would like to list the Object Store files from</param>
1847  /// <param name="path">Path to the Object Store files</param>
1848  /// <returns><see cref="ListObjectStoreResponse"/></returns>
1849  public ListObjectStoreResponse ListObjectStore(string organizationId, string path)
1850  {
1851  var request = new RestRequest("object/list", Method.POST)
1852  {
1853  RequestFormat = DataFormat.Json
1854  };
1855 
1856  var obj = new Dictionary<string, object>
1857  {
1858  { "organizationId", organizationId },
1859  { "path", path }
1860  };
1861 
1862  request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
1863 
1864  ApiConnection.TryRequest(request, out ListObjectStoreResponse result);
1865  return result;
1866  }
1867 
1868  /// <summary>
1869  /// Helper method to normalize path for api data requests
1870  /// </summary>
1871  /// <param name="filePath">Filepath to format</param>
1872  /// <param name="dataFolder">The data folder to use</param>
1873  /// <returns>Normalized path</returns>
1874  public static string FormatPathForDataRequest(string filePath, string dataFolder = null)
1875  {
1876  if (filePath == null)
1877  {
1878  Log.Error("Api.FormatPathForDataRequest(): Cannot format null string");
1879  return null;
1880  }
1881 
1882  dataFolder ??= Globals.DataFolder;
1883  // Normalize windows paths to linux format
1884  dataFolder = dataFolder.Replace("\\", "/", StringComparison.InvariantCulture);
1885  filePath = filePath.Replace("\\", "/", StringComparison.InvariantCulture);
1886 
1887  // First remove data root directory from path for request if included
1888  if (filePath.StartsWith(dataFolder, StringComparison.InvariantCulture))
1889  {
1890  filePath = filePath.Substring(dataFolder.Length);
1891  }
1892 
1893  // Trim '/' from start, this can cause issues for _dataFolders without final directory separator in the config
1894  filePath = filePath.TrimStart('/');
1895  return filePath;
1896  }
1897 
1898  /// <summary>
1899  /// Helper method that will execute the given api request and throw an exception if it fails
1900  /// </summary>
1901  private T MakeRequestOrThrow<T>(RestRequest request, string callerName)
1902  where T : RestResponse
1903  {
1904  if (!ApiConnection.TryRequest(request, out T result))
1905  {
1906  var errors = string.Empty;
1907  if (result != null && result.Errors != null && result.Errors.Count > 0)
1908  {
1909  errors = $". Errors: ['{string.Join(",", result.Errors)}']";
1910  }
1911  throw new WebException($"{callerName} api request failed{errors}");
1912  }
1913 
1914  return result;
1915  }
1916 
1917  /// <summary>
1918  /// Borrows and HTTP client from the pool
1919  /// </summary>
1920  private Lazy<HttpClient> BorrowClient()
1921  {
1922  using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(10));
1923  return _clientPool.Take(cancellationTokenSource.Token);
1924  }
1925 
1926  /// <summary>
1927  /// Returns the HTTP client to the pool
1928  /// </summary>
1929  private void ReturnClient(Lazy<HttpClient> client)
1930  {
1931  _clientPool.Add(client);
1932  }
1933  }
1934 }