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  /// Read out the insights of a backtest
754  /// </summary>
755  /// <param name="projectId">Id of the project from which to read the backtest</param>
756  /// <param name="backtestId">Backtest id from which we want to get the insights</param>
757  /// <param name="start">Starting index of the insights to be fetched</param>
758  /// <param name="end">Last index of the insights to be fetched. Note that end - start must be less than 100</param>
759  /// <returns><see cref="InsightResponse"/></returns>
760  /// <exception cref="ArgumentException"></exception>
761  public InsightResponse ReadBacktestInsights(int projectId, string backtestId, int start = 0, int end = 0)
762  {
763  var request = new RestRequest("backtests/insights/read", Method.POST)
764  {
765  RequestFormat = DataFormat.Json,
766  };
767 
768  var diff = end - start;
769  if (diff > 100)
770  {
771  throw new ArgumentException($"The difference between the start and end index of the insights must be smaller than 100, but it was {diff}.");
772  }
773  else if (end == 0)
774  {
775  end = start + 100;
776  }
777 
778  JObject obj = new()
779  {
780  { "projectId", projectId },
781  { "backtestId", backtestId },
782  { "start", start },
783  { "end", end },
784  };
785 
786  request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
787 
788  ApiConnection.TryRequest(request, out InsightResponse result);
789  return result;
790  }
791 
792  /// <summary>
793  /// Create a live algorithm.
794  /// </summary>
795  /// <param name="projectId">Id of the project on QuantConnect</param>
796  /// <param name="compileId">Id of the compilation on QuantConnect</param>
797  /// <param name="nodeId">Id of the node that will run the algorithm</param>
798  /// <param name="brokerageSettings">Dictionary with brokerage specific settings. Each brokerage requires certain specific credentials
799  /// in order to process the given orders. Each key in this dictionary represents a required field/credential
800  /// to provide to the brokerage API and its value represents the value of that field. For example: "brokerageSettings: {
801  /// "id": "Binance", "binance-api-secret": "123ABC", "binance-api-key": "ABC123"}. It is worth saying,
802  /// that this dictionary must always contain an entry whose key is "id" and its value is the name of the brokerage
803  /// (see <see cref="Brokerages.BrokerageName"/>)</param>
804  /// <param name="versionId">The version of the Lean used to run the algorithm.
805  /// -1 is master, however, sometimes this can create problems with live deployments.
806  /// If you experience problems using, try specifying the version of Lean you would like to use.</param>
807  /// <param name="dataProviders">Dictionary with data providers credentials. Each data provider requires certain credentials
808  /// in order to retrieve data from their API. Each key in this dictionary describes a data provider name
809  /// and its corresponding value is another dictionary with the required key-value pairs of credential
810  /// names and values. For example: "dataProviders: { "InteractiveBrokersBrokerage" : { "id": 12345, "environment" : "paper",
811  /// "username": "testUsername", "password": "testPassword"}}"</param>
812  /// <returns>Information regarding the new algorithm <see cref="CreateLiveAlgorithmResponse"/></returns>
814  string compileId,
815  string nodeId,
816  Dictionary<string, object> brokerageSettings,
817  string versionId = "-1",
818  Dictionary<string, object> dataProviders = null)
819  {
820  var request = new RestRequest("live/create", Method.POST)
821  {
822  RequestFormat = DataFormat.Json
823  };
824 
825  request.AddParameter("application/json", JsonConvert.SerializeObject(
827  (projectId,
828  compileId,
829  nodeId,
830  brokerageSettings,
831  versionId,
832  dataProviders
833  )
834  ), ParameterType.RequestBody);
835 
836  ApiConnection.TryRequest(request, out CreateLiveAlgorithmResponse result);
837  return result;
838  }
839 
840  /// <summary>
841  /// Create a live algorithm.
842  /// </summary>
843  /// <param name="projectId">Id of the project on QuantConnect</param>
844  /// <param name="compileId">Id of the compilation on QuantConnect</param>
845  /// <param name="nodeId">Id of the node that will run the algorithm</param>
846  /// <param name="brokerageSettings">Python Dictionary with brokerage specific settings. Each brokerage requires certain specific credentials
847  /// in order to process the given orders. Each key in this dictionary represents a required field/credential
848  /// to provide to the brokerage API and its value represents the value of that field. For example: "brokerageSettings: {
849  /// "id": "Binance", "binance-api-secret": "123ABC", "binance-api-key": "ABC123"}. It is worth saying,
850  /// that this dictionary must always contain an entry whose key is "id" and its value is the name of the brokerage
851  /// (see <see cref="Brokerages.BrokerageName"/>)</param>
852  /// <param name="versionId">The version of the Lean used to run the algorithm.
853  /// -1 is master, however, sometimes this can create problems with live deployments.
854  /// If you experience problems using, try specifying the version of Lean you would like to use.</param>
855  /// <param name="dataProviders">Python Dictionary with data providers credentials. Each data provider requires certain credentials
856  /// in order to retrieve data from their API. Each key in this dictionary describes a data provider name
857  /// and its corresponding value is another dictionary with the required key-value pairs of credential
858  /// names and values. For example: "dataProviders: { "InteractiveBrokersBrokerage" : { "id": 12345, "environment" : "paper",
859  /// "username": "testUsername", "password": "testPassword"}}"</param>
860  /// <returns>Information regarding the new algorithm <see cref="CreateLiveAlgorithmResponse"/></returns>
861 
862  public CreateLiveAlgorithmResponse CreateLiveAlgorithm(int projectId, string compileId, string nodeId, PyObject brokerageSettings, string versionId = "-1", PyObject dataProviders = null)
863  {
864  return CreateLiveAlgorithm(projectId, compileId, nodeId, ConvertToDictionary(brokerageSettings), versionId, dataProviders != null ? ConvertToDictionary(dataProviders) : null);
865  }
866 
867  /// <summary>
868  /// Converts a given Python dictionary into a C# <see cref="Dictionary{string, object}"/>
869  /// </summary>
870  /// <param name="brokerageSettings">Python dictionary to be converted</param>
871  private static Dictionary<string, object> ConvertToDictionary(PyObject brokerageSettings)
872  {
873  using (Py.GIL())
874  {
875  var stringBrokerageSettings = brokerageSettings.ToString();
876  return JsonConvert.DeserializeObject<Dictionary<string, object>>(stringBrokerageSettings);
877  }
878  }
879 
880  /// <summary>
881  /// Get a list of live running algorithms for user
882  /// </summary>
883  /// <param name="status">Filter the statuses of the algorithms returned from the api</param>
884  /// <param name="startTime">Earliest launched time of the algorithms returned by the Api</param>
885  /// <param name="endTime">Latest launched time of the algorithms returned by the Api</param>
886  /// <returns><see cref="LiveList"/></returns>
887 
889  DateTime? startTime = null,
890  DateTime? endTime = null)
891  {
892  // Only the following statuses are supported by the Api
893  if (status.HasValue &&
894  status != AlgorithmStatus.Running &&
895  status != AlgorithmStatus.RuntimeError &&
896  status != AlgorithmStatus.Stopped &&
897  status != AlgorithmStatus.Liquidated)
898  {
899  throw new ArgumentException(
900  "The Api only supports Algorithm Statuses of Running, Stopped, RuntimeError and Liquidated");
901  }
902 
903  var request = new RestRequest("live/list", Method.POST)
904  {
905  RequestFormat = DataFormat.Json
906  };
907 
908  var epochStartTime = startTime == null ? 0 : Time.DateTimeToUnixTimeStamp(startTime.Value);
909  var epochEndTime = endTime == null ? Time.DateTimeToUnixTimeStamp(DateTime.UtcNow) : Time.DateTimeToUnixTimeStamp(endTime.Value);
910 
911  JObject obj = new JObject
912  {
913  { "start", epochStartTime },
914  { "end", epochEndTime }
915  };
916 
917  if (status.HasValue)
918  {
919  obj.Add("status", status.ToString());
920  }
921 
922  request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
923 
924  ApiConnection.TryRequest(request, out LiveList result);
925  return result;
926  }
927 
928  /// <summary>
929  /// Read out a live algorithm in the project id specified.
930  /// </summary>
931  /// <param name="projectId">Project id to read</param>
932  /// <param name="deployId">Specific instance id to read</param>
933  /// <returns><see cref="LiveAlgorithmResults"/></returns>
934 
935  public LiveAlgorithmResults ReadLiveAlgorithm(int projectId, string deployId)
936  {
937  var request = new RestRequest("live/read", Method.POST)
938  {
939  RequestFormat = DataFormat.Json
940  };
941 
942  request.AddParameter("application/json", JsonConvert.SerializeObject(new
943  {
944  projectId,
945  deployId
946  }), ParameterType.RequestBody);
947 
948  ApiConnection.TryRequest(request, out LiveAlgorithmResults result);
949  return result;
950  }
951 
952  /// <summary>
953  /// Read out the portfolio state of a live algorithm
954  /// </summary>
955  /// <param name="projectId">Id of the project from which to read the live algorithm</param>
956  /// <returns><see cref="PortfolioResponse"/></returns>
957  public PortfolioResponse ReadLivePortfolio(int projectId)
958  {
959  var request = new RestRequest("live/portfolio/read", Method.POST)
960  {
961  RequestFormat = DataFormat.Json
962  };
963 
964  request.AddParameter("application/json", JsonConvert.SerializeObject(new
965  {
966  projectId
967  }), ParameterType.RequestBody);
968 
969  ApiConnection.TryRequest(request, out PortfolioResponse result);
970  return result;
971  }
972 
973  /// <summary>
974  /// Returns the orders of the specified project id live algorithm.
975  /// </summary>
976  /// <param name="projectId">Id of the project from which to read the live orders</param>
977  /// <param name="start">Starting index of the orders to be fetched. Required if end > 100</param>
978  /// <param name="end">Last index of the orders to be fetched. Note that end - start must be less than 100</param>
979  /// <remarks>Will throw an <see cref="WebException"/> if there are any API errors</remarks>
980  /// <returns>The list of <see cref="Order"/></returns>
981 
982  public List<ApiOrderResponse> ReadLiveOrders(int projectId, int start = 0, int end = 100)
983  {
984  var request = new RestRequest("live/orders/read", Method.POST)
985  {
986  RequestFormat = DataFormat.Json
987  };
988 
989  request.AddParameter("application/json", JsonConvert.SerializeObject(new
990  {
991  start,
992  end,
993  projectId
994  }), ParameterType.RequestBody);
995 
996  return MakeRequestOrThrow<OrdersResponseWrapper>(request, nameof(ReadLiveOrders)).Orders;
997  }
998 
999  /// <summary>
1000  /// Liquidate a live algorithm from the specified project and deployId.
1001  /// </summary>
1002  /// <param name="projectId">Project for the live instance we want to stop</param>
1003  /// <returns><see cref="RestResponse"/></returns>
1004 
1005  public RestResponse LiquidateLiveAlgorithm(int projectId)
1006  {
1007  var request = new RestRequest("live/update/liquidate", Method.POST)
1008  {
1009  RequestFormat = DataFormat.Json
1010  };
1011 
1012  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1013  {
1014  projectId
1015  }), ParameterType.RequestBody);
1016 
1017  ApiConnection.TryRequest(request, out RestResponse result);
1018  return result;
1019  }
1020 
1021  /// <summary>
1022  /// Stop a live algorithm from the specified project and deployId.
1023  /// </summary>
1024  /// <param name="projectId">Project for the live instance we want to stop</param>
1025  /// <returns><see cref="RestResponse"/></returns>
1026  public RestResponse StopLiveAlgorithm(int projectId)
1027  {
1028  var request = new RestRequest("live/update/stop", Method.POST)
1029  {
1030  RequestFormat = DataFormat.Json
1031  };
1032 
1033  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1034  {
1035  projectId
1036  }), ParameterType.RequestBody);
1037 
1038  ApiConnection.TryRequest(request, out RestResponse result);
1039  return result;
1040  }
1041 
1042  /// <summary>
1043  /// Create a live command
1044  /// </summary>
1045  /// <param name="projectId">Project for the live instance we want to run the command against</param>
1046  /// <param name="command">The command to run</param>
1047  /// <returns><see cref="RestResponse"/></returns>
1048  public RestResponse CreateLiveCommand(int projectId, object command)
1049  {
1050  var request = new RestRequest("live/commands/create", Method.POST)
1051  {
1052  RequestFormat = DataFormat.Json
1053  };
1054 
1055  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1056  {
1057  projectId,
1058  command
1059  }), ParameterType.RequestBody);
1060 
1061  ApiConnection.TryRequest(request, out RestResponse result);
1062  return result;
1063  }
1064 
1065  /// <summary>
1066  /// Gets the logs of a specific live algorithm
1067  /// </summary>
1068  /// <param name="projectId">Project Id of the live running algorithm</param>
1069  /// <param name="algorithmId">Algorithm Id of the live running algorithm</param>
1070  /// <param name="startLine">Start line of logs to read</param>
1071  /// <param name="endLine">End line of logs to read</param>
1072  /// <returns><see cref="LiveLog"/> List of strings that represent the logs of the algorithm</returns>
1073  public LiveLog ReadLiveLogs(int projectId, string algorithmId, int startLine, int endLine)
1074  {
1075  var logLinesNumber = endLine - startLine;
1076  if (logLinesNumber > 250)
1077  {
1078  throw new ArgumentException($"The maximum number of log lines allowed is 250. But the number of log lines was {logLinesNumber}.");
1079  }
1080 
1081  var request = new RestRequest("live/logs/read", Method.POST)
1082  {
1083  RequestFormat = DataFormat.Json
1084  };
1085 
1086  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1087  {
1088  format = "json",
1089  projectId,
1090  algorithmId,
1091  startLine,
1092  endLine,
1093  }), ParameterType.RequestBody);
1094 
1095  ApiConnection.TryRequest(request, out LiveLog result);
1096  return result;
1097  }
1098 
1099  /// <summary>
1100  /// Returns a chart object from a live algorithm
1101  /// </summary>
1102  /// <param name="projectId">Project ID of the request</param>
1103  /// <param name="name">The requested chart name</param>
1104  /// <param name="start">The Utc start seconds timestamp of the request</param>
1105  /// <param name="end">The Utc end seconds timestamp of the request</param>
1106  /// <param name="count">The number of data points to request</param>
1107  /// <returns>The chart</returns>
1108  public ReadChartResponse ReadLiveChart(int projectId, string name, int start, int end, uint count)
1109  {
1110  var request = new RestRequest("live/chart/read", Method.POST)
1111  {
1112  RequestFormat = DataFormat.Json
1113  };
1114 
1115  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1116  {
1117  projectId,
1118  name,
1119  start,
1120  end,
1121  count
1122  }), ParameterType.RequestBody);
1123 
1124  ReadChartResponse result = default;
1125  ApiConnection.TryRequest(request, out result);
1126 
1127  var finish = DateTime.UtcNow.AddMinutes(1);
1128  while(DateTime.UtcNow < finish && result.Chart == null)
1129  {
1130  Thread.Sleep(5000);
1131  ApiConnection.TryRequest(request, out result);
1132  }
1133  return result;
1134  }
1135 
1136  /// <summary>
1137  /// Read out the insights of a live algorithm
1138  /// </summary>
1139  /// <param name="projectId">Id of the project from which to read the live algorithm</param>
1140  /// <param name="start">Starting index of the insights to be fetched</param>
1141  /// <param name="end">Last index of the insights to be fetched. Note that end - start must be less than 100</param>
1142  /// <returns><see cref="InsightResponse"/></returns>
1143  /// <exception cref="ArgumentException"></exception>
1144  public InsightResponse ReadLiveInsights(int projectId, int start = 0, int end = 0)
1145  {
1146  var request = new RestRequest("live/insights/read", Method.POST)
1147  {
1148  RequestFormat = DataFormat.Json,
1149  };
1150 
1151  var diff = end - start;
1152  if (diff > 100)
1153  {
1154  throw new ArgumentException($"The difference between the start and end index of the insights must be smaller than 100, but it was {diff}.");
1155  }
1156  else if (end == 0)
1157  {
1158  end = start + 100;
1159  }
1160 
1161  JObject obj = new JObject
1162  {
1163  { "projectId", projectId },
1164  { "start", start },
1165  { "end", end },
1166  };
1167 
1168  request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
1169 
1170  ApiConnection.TryRequest(request, out InsightResponse result);
1171  return result;
1172  }
1173 
1174  /// <summary>
1175  /// Gets the link to the downloadable data.
1176  /// </summary>
1177  /// <param name="filePath">File path representing the data requested</param>
1178  /// <param name="organizationId">Organization to download from</param>
1179  /// <returns><see cref="DataLink"/> to the downloadable data.</returns>
1180  public DataLink ReadDataLink(string filePath, string organizationId)
1181  {
1182  if (filePath == null)
1183  {
1184  throw new ArgumentException("Api.ReadDataLink(): Filepath must not be null");
1185  }
1186 
1187  // Prepare filePath for request
1188  filePath = FormatPathForDataRequest(filePath);
1189 
1190  var request = new RestRequest("data/read", Method.POST)
1191  {
1192  RequestFormat = DataFormat.Json
1193  };
1194 
1195  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1196  {
1197  format = "link",
1198  filePath,
1199  organizationId
1200  }), ParameterType.RequestBody);
1201 
1202  ApiConnection.TryRequest(request, out DataLink result);
1203  return result;
1204  }
1205 
1206  /// <summary>
1207  /// Get valid data entries for a given filepath from data/list
1208  /// </summary>
1209  /// <returns></returns>
1210  public DataList ReadDataDirectory(string filePath)
1211  {
1212  if (filePath == null)
1213  {
1214  throw new ArgumentException("Api.ReadDataDirectory(): Filepath must not be null");
1215  }
1216 
1217  // Prepare filePath for request
1218  filePath = FormatPathForDataRequest(filePath);
1219 
1220  // Verify the filePath for this request is at least three directory deep
1221  // (requirement of endpoint)
1222  if (filePath.Count(x => x == '/') < 3)
1223  {
1224  throw new ArgumentException($"Api.ReadDataDirectory(): Data directory requested must be at least" +
1225  $" three directories deep. FilePath: {filePath}");
1226  }
1227 
1228  var request = new RestRequest("data/list", Method.POST)
1229  {
1230  RequestFormat = DataFormat.Json
1231  };
1232 
1233  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1234  {
1235  filePath
1236  }), ParameterType.RequestBody);
1237 
1238  ApiConnection.TryRequest(request, out DataList result);
1239  return result;
1240  }
1241 
1242  /// <summary>
1243  /// Gets data prices from data/prices
1244  /// </summary>
1245  public DataPricesList ReadDataPrices(string organizationId)
1246  {
1247  var request = new RestRequest("data/prices", Method.POST)
1248  {
1249  RequestFormat = DataFormat.Json
1250  };
1251 
1252  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1253  {
1254  organizationId
1255  }), ParameterType.RequestBody);
1256 
1257  ApiConnection.TryRequest(request, out DataPricesList result);
1258  return result;
1259  }
1260 
1261  /// <summary>
1262  /// Read out the report of a backtest in the project id specified.
1263  /// </summary>
1264  /// <param name="projectId">Project id to read</param>
1265  /// <param name="backtestId">Specific backtest id to read</param>
1266  /// <returns><see cref="BacktestReport"/></returns>
1267  public BacktestReport ReadBacktestReport(int projectId, string backtestId)
1268  {
1269  var request = new RestRequest("backtests/read/report", Method.POST)
1270  {
1271  RequestFormat = DataFormat.Json
1272  };
1273 
1274  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1275  {
1276  backtestId,
1277  projectId
1278  }), ParameterType.RequestBody);
1279 
1280  BacktestReport report = new BacktestReport();
1281  var finish = DateTime.UtcNow.AddMinutes(1);
1282  while (DateTime.UtcNow < finish && !report.Success)
1283  {
1284  Thread.Sleep(10000);
1285  ApiConnection.TryRequest(request, out report);
1286  }
1287  return report;
1288  }
1289 
1290  /// <summary>
1291  /// Method to purchase and download data from QuantConnect
1292  /// </summary>
1293  /// <param name="filePath">File path representing the data requested</param>
1294  /// <param name="organizationId">Organization to buy the data with</param>
1295  /// <returns>A <see cref="bool"/> indicating whether the data was successfully downloaded or not.</returns>
1296 
1297  public bool DownloadData(string filePath, string organizationId)
1298  {
1299  // Get a link to the data
1300  var dataLink = ReadDataLink(filePath, organizationId);
1301 
1302  // Make sure the link was successfully retrieved
1303  if (!dataLink.Success)
1304  {
1305  Log.Trace($"Api.DownloadData(): Failed to get link for {filePath}. " +
1306  $"Errors: {string.Join(',', dataLink.Errors)}");
1307  return false;
1308  }
1309 
1310  // Make sure the directory exist before writing
1311  var directory = Path.GetDirectoryName(filePath);
1312  if (!Directory.Exists(directory))
1313  {
1314  Directory.CreateDirectory(directory);
1315  }
1316 
1317  var client = BorrowClient();
1318  try
1319  {
1320  // Download the file
1321  var uri = new Uri(dataLink.Link);
1322  using var dataStream = client.Value.GetStreamAsync(uri);
1323 
1324  using var fileStream = new FileStream(FileExtension.ToNormalizedPath(filePath), FileMode.Create);
1325  dataStream.Result.CopyTo(fileStream);
1326  }
1327  catch
1328  {
1329  Log.Error($"Api.DownloadData(): Failed to download zip for path ({filePath})");
1330  return false;
1331  }
1332  finally
1333  {
1334  ReturnClient(client);
1335  }
1336 
1337  return true;
1338  }
1339 
1340  /// <summary>
1341  /// Get the algorithm status from the user with this algorithm id.
1342  /// </summary>
1343  /// <param name="algorithmId">String algorithm id we're searching for.</param>
1344  /// <returns>Algorithm status enum</returns>
1345 
1346  public virtual AlgorithmControl GetAlgorithmStatus(string algorithmId)
1347  {
1348  return new AlgorithmControl()
1349  {
1350  ChartSubscription = "*"
1351  };
1352  }
1353 
1354  /// <summary>
1355  /// Algorithm passes back its current status to the UX.
1356  /// </summary>
1357  /// <param name="status">Status of the current algorithm</param>
1358  /// <param name="algorithmId">String algorithm id we're setting.</param>
1359  /// <param name="message">Message for the algorithm status event</param>
1360  /// <returns>Algorithm status enum</returns>
1361 
1362  public virtual void SetAlgorithmStatus(string algorithmId, AlgorithmStatus status, string message = "")
1363  {
1364  //
1365  }
1366 
1367  /// <summary>
1368  /// Send the statistics to storage for performance tracking.
1369  /// </summary>
1370  /// <param name="algorithmId">Identifier for algorithm</param>
1371  /// <param name="unrealized">Unrealized gainloss</param>
1372  /// <param name="fees">Total fees</param>
1373  /// <param name="netProfit">Net profi</param>
1374  /// <param name="holdings">Algorithm holdings</param>
1375  /// <param name="equity">Total equity</param>
1376  /// <param name="netReturn">Net return for the deployment</param>
1377  /// <param name="volume">Volume traded</param>
1378  /// <param name="trades">Total trades since inception</param>
1379  /// <param name="sharpe">Sharpe ratio since inception</param>
1380 
1381  public virtual void SendStatistics(string algorithmId, decimal unrealized, decimal fees, decimal netProfit, decimal holdings, decimal equity, decimal netReturn, decimal volume, int trades, double sharpe)
1382  {
1383  //
1384  }
1385 
1386  /// <summary>
1387  /// Send an email to the user associated with the specified algorithm id
1388  /// </summary>
1389  /// <param name="algorithmId">The algorithm id</param>
1390  /// <param name="subject">The email subject</param>
1391  /// <param name="body">The email message body</param>
1392 
1393  public virtual void SendUserEmail(string algorithmId, string subject, string body)
1394  {
1395  //
1396  }
1397 
1398  /// <summary>
1399  /// Local implementation for downloading data to algorithms
1400  /// </summary>
1401  /// <param name="address">URL to download</param>
1402  /// <param name="headers">KVP headers</param>
1403  /// <param name="userName">Username for basic authentication</param>
1404  /// <param name="password">Password for basic authentication</param>
1405  /// <returns></returns>
1406  public virtual string Download(string address, IEnumerable<KeyValuePair<string, string>> headers, string userName, string password)
1407  {
1408  return Encoding.UTF8.GetString(DownloadBytes(address, headers, userName, password));
1409  }
1410 
1411  /// <summary>
1412  /// Local implementation for downloading data to algorithms
1413  /// </summary>
1414  /// <param name="address">URL to download</param>
1415  /// <param name="headers">KVP headers</param>
1416  /// <param name="userName">Username for basic authentication</param>
1417  /// <param name="password">Password for basic authentication</param>
1418  /// <returns>A stream from which the data can be read</returns>
1419  /// <remarks>Stream.Close() most be called to avoid running out of resources</remarks>
1420  public virtual byte[] DownloadBytes(string address, IEnumerable<KeyValuePair<string, string>> headers, string userName, string password)
1421  {
1422  var client = BorrowClient();
1423  try
1424  {
1425  client.Value.DefaultRequestHeaders.Clear();
1426 
1427  // Add a user agent header in case the requested URI contains a query.
1428  client.Value.DefaultRequestHeaders.TryAddWithoutValidation("user-agent", "QCAlgorithm.Download(): User Agent Header");
1429 
1430  if (headers != null)
1431  {
1432  foreach (var header in headers)
1433  {
1434  client.Value.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value);
1435  }
1436  }
1437 
1438  if (!userName.IsNullOrEmpty() || !password.IsNullOrEmpty())
1439  {
1440  var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{userName}:{password}"));
1441  client.Value.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
1442  }
1443 
1444  return client.Value.GetByteArrayAsync(new Uri(address)).Result;
1445  }
1446  catch (Exception exception)
1447  {
1448  var message = $"Api.DownloadBytes(): Failed to download data from {address}";
1449  if (!userName.IsNullOrEmpty() || !password.IsNullOrEmpty())
1450  {
1451  message += $" with username: {userName} and password {password}";
1452  }
1453 
1454  throw new WebException($"{message}. Please verify the source for missing http:// or https://", exception);
1455  }
1456  finally
1457  {
1458  client.Value.DefaultRequestHeaders.Clear();
1459  ReturnClient(client);
1460  }
1461  }
1462 
1463  /// <summary>
1464  /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
1465  /// </summary>
1466  /// <filterpriority>2</filterpriority>
1467  public virtual void Dispose()
1468  {
1469  // Dispose of the HttpClient pool
1470  _clientPool.CompleteAdding();
1471  foreach (var client in _clientPool.GetConsumingEnumerable())
1472  {
1473  if (client.IsValueCreated)
1474  {
1475  client.Value.DisposeSafely();
1476  }
1477  }
1478  _clientPool.DisposeSafely();
1479  }
1480 
1481  /// <summary>
1482  /// Generate a secure hash for the authorization headers.
1483  /// </summary>
1484  /// <returns>Time based hash of user token and timestamp.</returns>
1485  public static string CreateSecureHash(int timestamp, string token)
1486  {
1487  // Create a new hash using current UTC timestamp.
1488  // Hash must be generated fresh each time.
1489  var data = $"{token}:{timestamp.ToStringInvariant()}";
1490  return data.ToSHA256();
1491  }
1492 
1493  /// <summary>
1494  /// Will read the organization account status
1495  /// </summary>
1496  /// <param name="organizationId">The target organization id, if null will return default organization</param>
1497  public Account ReadAccount(string organizationId = null)
1498  {
1499  var request = new RestRequest("account/read", Method.POST)
1500  {
1501  RequestFormat = DataFormat.Json
1502  };
1503 
1504  if (organizationId != null)
1505  {
1506  request.AddParameter("application/json", JsonConvert.SerializeObject(new { organizationId }), ParameterType.RequestBody);
1507  }
1508 
1509  ApiConnection.TryRequest(request, out Account account);
1510  return account;
1511  }
1512 
1513  /// <summary>
1514  /// Fetch organization data from web API
1515  /// </summary>
1516  /// <param name="organizationId"></param>
1517  /// <returns></returns>
1518  public Organization ReadOrganization(string organizationId = null)
1519  {
1520  var request = new RestRequest("organizations/read", Method.POST)
1521  {
1522  RequestFormat = DataFormat.Json
1523  };
1524 
1525  if (organizationId != null)
1526  {
1527  request.AddParameter("application/json", JsonConvert.SerializeObject(new { organizationId }), ParameterType.RequestBody);
1528  }
1529 
1530  ApiConnection.TryRequest(request, out OrganizationResponse response);
1531  return response.Organization;
1532  }
1533 
1534  /// <summary>
1535  /// Estimate optimization with the specified parameters via QuantConnect.com API
1536  /// </summary>
1537  /// <param name="projectId">Project ID of the project the optimization belongs to</param>
1538  /// <param name="name">Name of the optimization</param>
1539  /// <param name="target">Target of the optimization, see examples in <see cref="PortfolioStatistics"/></param>
1540  /// <param name="targetTo">Target extremum of the optimization, for example "max" or "min"</param>
1541  /// <param name="targetValue">Optimization target value</param>
1542  /// <param name="strategy">Optimization strategy, <see cref="QuantConnect.Optimizer.Strategies.GridSearchOptimizationStrategy"/></param>
1543  /// <param name="compileId">Optimization compile ID</param>
1544  /// <param name="parameters">Optimization parameters</param>
1545  /// <param name="constraints">Optimization constraints</param>
1546  /// <returns>Estimate object from the API.</returns>
1548  int projectId,
1549  string name,
1550  string target,
1551  string targetTo,
1552  decimal? targetValue,
1553  string strategy,
1554  string compileId,
1555  HashSet<OptimizationParameter> parameters,
1556  IReadOnlyList<Constraint> constraints)
1557  {
1558  var request = new RestRequest("optimizations/estimate", Method.POST)
1559  {
1560  RequestFormat = DataFormat.Json
1561  };
1562 
1563  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1564  {
1565  projectId,
1566  name,
1567  target,
1568  targetTo,
1569  targetValue,
1570  strategy,
1571  compileId,
1572  parameters,
1573  constraints
1574  }, SerializerSettings), ParameterType.RequestBody);
1575 
1576  ApiConnection.TryRequest(request, out EstimateResponseWrapper response);
1577  return response.Estimate;
1578  }
1579 
1580  /// <summary>
1581  /// Create an optimization with the specified parameters via QuantConnect.com API
1582  /// </summary>
1583  /// <param name="projectId">Project ID of the project the optimization belongs to</param>
1584  /// <param name="name">Name of the optimization</param>
1585  /// <param name="target">Target of the optimization, see examples in <see cref="PortfolioStatistics"/></param>
1586  /// <param name="targetTo">Target extremum of the optimization, for example "max" or "min"</param>
1587  /// <param name="targetValue">Optimization target value</param>
1588  /// <param name="strategy">Optimization strategy, <see cref="QuantConnect.Optimizer.Strategies.GridSearchOptimizationStrategy"/></param>
1589  /// <param name="compileId">Optimization compile ID</param>
1590  /// <param name="parameters">Optimization parameters</param>
1591  /// <param name="constraints">Optimization constraints</param>
1592  /// <param name="estimatedCost">Estimated cost for optimization</param>
1593  /// <param name="nodeType">Optimization node type <see cref="OptimizationNodes"/></param>
1594  /// <param name="parallelNodes">Number of parallel nodes for optimization</param>
1595  /// <returns>BaseOptimization object from the API.</returns>
1597  int projectId,
1598  string name,
1599  string target,
1600  string targetTo,
1601  decimal? targetValue,
1602  string strategy,
1603  string compileId,
1604  HashSet<OptimizationParameter> parameters,
1605  IReadOnlyList<Constraint> constraints,
1606  decimal estimatedCost,
1607  string nodeType,
1608  int parallelNodes)
1609  {
1610  var request = new RestRequest("optimizations/create", Method.POST)
1611  {
1612  RequestFormat = DataFormat.Json
1613  };
1614 
1615  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1616  {
1617  projectId,
1618  name,
1619  target,
1620  targetTo,
1621  targetValue,
1622  strategy,
1623  compileId,
1624  parameters,
1625  constraints,
1626  estimatedCost,
1627  nodeType,
1628  parallelNodes
1629  }, SerializerSettings), ParameterType.RequestBody);
1630 
1631  ApiConnection.TryRequest(request, out OptimizationList result);
1632  return result.Optimizations.FirstOrDefault();
1633  }
1634 
1635  /// <summary>
1636  /// List all the optimizations for a project
1637  /// </summary>
1638  /// <param name="projectId">Project id we'd like to get a list of optimizations for</param>
1639  /// <returns>A list of BaseOptimization objects, <see cref="BaseOptimization"/></returns>
1640  public List<OptimizationSummary> ListOptimizations(int projectId)
1641  {
1642  var request = new RestRequest("optimizations/list", Method.POST)
1643  {
1644  RequestFormat = DataFormat.Json
1645  };
1646 
1647  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1648  {
1649  projectId,
1650  }), ParameterType.RequestBody);
1651 
1652  ApiConnection.TryRequest(request, out OptimizationList result);
1653  return result.Optimizations;
1654  }
1655 
1656  /// <summary>
1657  /// Read an optimization
1658  /// </summary>
1659  /// <param name="optimizationId">Optimization id for the optimization we want to read</param>
1660  /// <returns><see cref="Optimization"/></returns>
1661  public Optimization ReadOptimization(string optimizationId)
1662  {
1663  var request = new RestRequest("optimizations/read", Method.POST)
1664  {
1665  RequestFormat = DataFormat.Json
1666  };
1667 
1668  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1669  {
1670  optimizationId
1671  }), ParameterType.RequestBody);
1672 
1673  ApiConnection.TryRequest(request, out OptimizationResponseWrapper response);
1674  return response.Optimization;
1675  }
1676 
1677  /// <summary>
1678  /// Abort an optimization
1679  /// </summary>
1680  /// <param name="optimizationId">Optimization id for the optimization we want to abort</param>
1681  /// <returns><see cref="RestResponse"/></returns>
1682  public RestResponse AbortOptimization(string optimizationId)
1683  {
1684  var request = new RestRequest("optimizations/abort", Method.POST)
1685  {
1686  RequestFormat = DataFormat.Json
1687  };
1688 
1689  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1690  {
1691  optimizationId
1692  }), ParameterType.RequestBody);
1693 
1694  ApiConnection.TryRequest(request, out RestResponse result);
1695  return result;
1696  }
1697 
1698  /// <summary>
1699  /// Update an optimization
1700  /// </summary>
1701  /// <param name="optimizationId">Optimization id we want to update</param>
1702  /// <param name="name">Name we'd like to assign to the optimization</param>
1703  /// <returns><see cref="RestResponse"/></returns>
1704  public RestResponse UpdateOptimization(string optimizationId, string name = null)
1705  {
1706  var request = new RestRequest("optimizations/update", Method.POST)
1707  {
1708  RequestFormat = DataFormat.Json
1709  };
1710 
1711  var obj = new JObject
1712  {
1713  { "optimizationId", optimizationId }
1714  };
1715 
1716  if (name.HasValue())
1717  {
1718  obj.Add("name", name);
1719  }
1720 
1721  request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
1722 
1723  ApiConnection.TryRequest(request, out RestResponse result);
1724  return result;
1725  }
1726 
1727  /// <summary>
1728  /// Delete an optimization
1729  /// </summary>
1730  /// <param name="optimizationId">Optimization id for the optimization we want to delete</param>
1731  /// <returns><see cref="RestResponse"/></returns>
1732  public RestResponse DeleteOptimization(string optimizationId)
1733  {
1734  var request = new RestRequest("optimizations/delete", Method.POST)
1735  {
1736  RequestFormat = DataFormat.Json
1737  };
1738 
1739  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1740  {
1741  optimizationId
1742  }), ParameterType.RequestBody);
1743 
1744  ApiConnection.TryRequest(request, out RestResponse result);
1745  return result;
1746  }
1747 
1748  /// <summary>
1749  /// Download the object store files associated with the given organization ID and key
1750  /// </summary>
1751  /// <param name="organizationId">Organization ID we would like to get the Object Store files from</param>
1752  /// <param name="keys">Keys for the Object Store files</param>
1753  /// <param name="destinationFolder">Folder in which the object store files will be stored</param>
1754  /// <returns>True if the object store files were retrieved correctly, false otherwise</returns>
1755  public bool GetObjectStore(string organizationId, List<string> keys, string destinationFolder = null)
1756  {
1757  var request = new RestRequest("object/get", Method.POST)
1758  {
1759  RequestFormat = DataFormat.Json
1760  };
1761 
1762  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1763  {
1764  organizationId,
1765  keys
1766  }), ParameterType.RequestBody);
1767 
1768  ApiConnection.TryRequest(request, out GetObjectStoreResponse result);
1769 
1770  if (result == null || !result.Success)
1771  {
1772  Log.Error($"Api.GetObjectStore(): Failed to get the jobId to request the download URL for the object store files."
1773  + (result != null ? $" Errors: {string.Join(",", result.Errors)}" : ""));
1774  return false;
1775  }
1776 
1777  var jobId = result.JobId;
1778  var getUrlRequest = new RestRequest("object/get", Method.POST)
1779  {
1780  RequestFormat = DataFormat.Json
1781  };
1782  getUrlRequest.AddParameter("application/json", JsonConvert.SerializeObject(new
1783  {
1784  organizationId,
1785  jobId
1786  }), ParameterType.RequestBody);
1787 
1788  var frontier = DateTime.UtcNow + TimeSpan.FromMinutes(5);
1789  while (string.IsNullOrEmpty(result?.Url) && (DateTime.UtcNow < frontier))
1790  {
1791  Thread.Sleep(3000);
1792  ApiConnection.TryRequest(getUrlRequest, out result);
1793  }
1794 
1795  if (result == null || string.IsNullOrEmpty(result.Url))
1796  {
1797  Log.Error($"Api.GetObjectStore(): Failed to get the download URL from the jobId {jobId}."
1798  + (result != null ? $" Errors: {string.Join(",", result.Errors)}" : ""));
1799  return false;
1800  }
1801 
1802  var directory = destinationFolder ?? Directory.GetCurrentDirectory();
1803  var client = BorrowClient();
1804 
1805  try
1806  {
1807  if (client.Value.Timeout != TimeSpan.FromMinutes(20))
1808  {
1809  client.Value.Timeout = TimeSpan.FromMinutes(20);
1810  }
1811 
1812  // Download the file
1813  var uri = new Uri(result.Url);
1814  using var byteArray = client.Value.GetByteArrayAsync(uri);
1815 
1816  Compression.UnzipToFolder(byteArray.Result, directory);
1817  }
1818  catch (Exception e)
1819  {
1820  Log.Error($"Api.GetObjectStore(): Failed to download zip for path ({directory}). Error: {e.Message}");
1821  return false;
1822  }
1823  finally
1824  {
1825  ReturnClient(client);
1826  }
1827 
1828  return true;
1829  }
1830 
1831  /// <summary>
1832  /// Get Object Store properties given the organization ID and the Object Store key
1833  /// </summary>
1834  /// <param name="organizationId">Organization ID we would like to get the Object Store from</param>
1835  /// <param name="key">Key for the Object Store file</param>
1836  /// <returns><see cref="PropertiesObjectStoreResponse"/></returns>
1837  /// <remarks>It does not work when the object store is a directory</remarks>
1838  public PropertiesObjectStoreResponse GetObjectStoreProperties(string organizationId, string key)
1839  {
1840  var request = new RestRequest("object/properties", Method.POST)
1841  {
1842  RequestFormat = DataFormat.Json
1843  };
1844 
1845  request.AddParameter("organizationId", organizationId);
1846  request.AddParameter("key", key);
1847 
1848  ApiConnection.TryRequest(request, out PropertiesObjectStoreResponse result);
1849 
1850  if (result == null || !result.Success)
1851  {
1852  Log.Error($"Api.ObjectStore(): Failed to get the properties for the object store key {key}." + (result != null ? $" Errors: {string.Join(",", result.Errors)}" : ""));
1853  }
1854  return result;
1855  }
1856 
1857  /// <summary>
1858  /// Upload files to the Object Store
1859  /// </summary>
1860  /// <param name="organizationId">Organization ID we would like to upload the file to</param>
1861  /// <param name="key">Key to the Object Store file</param>
1862  /// <param name="objectData">File (as an array of bytes) to be uploaded</param>
1863  /// <returns><see cref="RestResponse"/></returns>
1864  public RestResponse SetObjectStore(string organizationId, string key, byte[] objectData)
1865  {
1866  var request = new RestRequest("object/set", Method.POST)
1867  {
1868  RequestFormat = DataFormat.Json
1869  };
1870 
1871  request.AddParameter("organizationId", organizationId);
1872  request.AddParameter("key", key);
1873  request.AddFileBytes("objectData", objectData, "objectData");
1874  request.AlwaysMultipartFormData = true;
1875 
1876  ApiConnection.TryRequest(request, out RestResponse result);
1877  return result;
1878  }
1879 
1880  /// <summary>
1881  /// Request to delete Object Store metadata of a specific organization and key
1882  /// </summary>
1883  /// <param name="organizationId">Organization ID we would like to delete the Object Store file from</param>
1884  /// <param name="key">Key to the Object Store file</param>
1885  /// <returns><see cref="RestResponse"/></returns>
1886  public RestResponse DeleteObjectStore(string organizationId, string key)
1887  {
1888  var request = new RestRequest("object/delete", Method.POST)
1889  {
1890  RequestFormat = DataFormat.Json
1891  };
1892 
1893  var obj = new Dictionary<string, object>
1894  {
1895  { "organizationId", organizationId },
1896  { "key", key }
1897  };
1898 
1899  request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
1900 
1901  ApiConnection.TryRequest(request, out RestResponse result);
1902  return result;
1903  }
1904 
1905  /// <summary>
1906  /// Request to list Object Store files of a specific organization and path
1907  /// </summary>
1908  /// <param name="organizationId">Organization ID we would like to list the Object Store files from</param>
1909  /// <param name="path">Path to the Object Store files</param>
1910  /// <returns><see cref="ListObjectStoreResponse"/></returns>
1911  public ListObjectStoreResponse ListObjectStore(string organizationId, string path)
1912  {
1913  var request = new RestRequest("object/list", Method.POST)
1914  {
1915  RequestFormat = DataFormat.Json
1916  };
1917 
1918  var obj = new Dictionary<string, object>
1919  {
1920  { "organizationId", organizationId },
1921  { "path", path }
1922  };
1923 
1924  request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
1925 
1926  ApiConnection.TryRequest(request, out ListObjectStoreResponse result);
1927  return result;
1928  }
1929 
1930  /// <summary>
1931  /// Helper method to normalize path for api data requests
1932  /// </summary>
1933  /// <param name="filePath">Filepath to format</param>
1934  /// <param name="dataFolder">The data folder to use</param>
1935  /// <returns>Normalized path</returns>
1936  public static string FormatPathForDataRequest(string filePath, string dataFolder = null)
1937  {
1938  if (filePath == null)
1939  {
1940  Log.Error("Api.FormatPathForDataRequest(): Cannot format null string");
1941  return null;
1942  }
1943 
1944  dataFolder ??= Globals.DataFolder;
1945  // Normalize windows paths to linux format
1946  dataFolder = dataFolder.Replace("\\", "/", StringComparison.InvariantCulture);
1947  filePath = filePath.Replace("\\", "/", StringComparison.InvariantCulture);
1948 
1949  // First remove data root directory from path for request if included
1950  if (filePath.StartsWith(dataFolder, StringComparison.InvariantCulture))
1951  {
1952  filePath = filePath.Substring(dataFolder.Length);
1953  }
1954 
1955  // Trim '/' from start, this can cause issues for _dataFolders without final directory separator in the config
1956  filePath = filePath.TrimStart('/');
1957  return filePath;
1958  }
1959 
1960  /// <summary>
1961  /// Helper method that will execute the given api request and throw an exception if it fails
1962  /// </summary>
1963  private T MakeRequestOrThrow<T>(RestRequest request, string callerName)
1964  where T : RestResponse
1965  {
1966  if (!ApiConnection.TryRequest(request, out T result))
1967  {
1968  var errors = string.Empty;
1969  if (result != null && result.Errors != null && result.Errors.Count > 0)
1970  {
1971  errors = $". Errors: ['{string.Join(",", result.Errors)}']";
1972  }
1973  throw new WebException($"{callerName} api request failed{errors}");
1974  }
1975 
1976  return result;
1977  }
1978 
1979  /// <summary>
1980  /// Borrows and HTTP client from the pool
1981  /// </summary>
1982  private Lazy<HttpClient> BorrowClient()
1983  {
1984  using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(10));
1985  return _clientPool.Take(cancellationTokenSource.Token);
1986  }
1987 
1988  /// <summary>
1989  /// Returns the HTTP client to the pool
1990  /// </summary>
1991  private void ReturnClient(Lazy<HttpClient> client)
1992  {
1993  _clientPool.Add(client);
1994  }
1995  }
1996 }