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